mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
additions: sub-org ui updates
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ export type TSubOrganization = {
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
parentOrgId: string;
|
||||
};
|
||||
|
||||
export type TCreateSubOrganizationDTO = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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."
|
||||
/>
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user