chore: merge main

This commit is contained in:
Scott Wilson
2025-12-09 10:49:52 -08:00
123 changed files with 1985 additions and 4022 deletions

View File

@@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => {
const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken();
const params = new URLSearchParams(window.location.search);
if (config.headers) {
if (signupTempToken) {
// eslint-disable-next-line no-param-reassign
@@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`;
}
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);
}
}
}
return config;

View File

@@ -61,7 +61,8 @@ export const leaveConfirmDefaultMessage =
export enum SessionStorageKeys {
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
AUTH_CONSENT = "AUTH_CONSENT"
AUTH_CONSENT = "AUTH_CONSENT",
MFA_TEMP_TOKEN = "MFA_TEMP_TOKEN"
}
export const secretTagsColors = [

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useRouteContext, useSearch } from "@tanstack/react-router";
import { useRouteContext } from "@tanstack/react-router";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
@@ -10,28 +10,23 @@ export const useOrganization = () => {
select: (el) => el.organizationId
});
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { data: currentOrg } = useSuspenseQuery({
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId),
staleTime: Infinity
});
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
const org = useMemo(
() => ({
currentOrg: {
...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id,
parentOrgId: currentOrg.id
parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
},
isSubOrganization: Boolean(currentOrg.subOrganization),
isRootOrganization: !currentOrg.subOrganization
isSubOrganization,
isRootOrganization: !isSubOrganization
}),
[currentOrg, subOrganization]
[currentOrg]
);
return org;

View File

@@ -58,10 +58,12 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return data;
};
export const selectOrganization = async (data: {
export type SelectOrganizationParams = {
organizationId: string;
userAgent?: UserAgentType;
}) => {
};
export const selectOrganization = async (data: SelectOrganizationParams) => {
const { data: res } = await apiRequest.post<{
token: string;
isMfaEnabled: boolean;
@@ -73,7 +75,7 @@ export const selectOrganization = async (data: {
export const useSelectOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
mutationFn: async (details: SelectOrganizationParams) => {
const data = await selectOrganization(details);
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.

View File

@@ -1,6 +1,7 @@
export type GetAuthTokenAPI = {
token: string;
organizationId?: string;
subOrganizationId?: string;
};
export enum UserEncryptionVersion {

View File

@@ -42,7 +42,7 @@ export const organizationKeys = {
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }],
getOrgById: (orgId: string) => ["organization", { orgId }],
getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"]
};
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
const {
data: { organization }
} = await apiRequest.get<{
organization: Organization & { subOrganization?: { id: string; name: string } };
organization: Organization;
}>(`/api/v1/organization/${id}`);
return organization;
};

View File

@@ -30,6 +30,8 @@ export type Organization = {
maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
parentOrgId: string | null;
rootOrgId: string | null;
};
export type UpdateOrgDTO = {

View File

@@ -3,14 +3,16 @@ import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types";
export const projectKeys = {
getProjectById: (projectId: string) => ["projects", { projectId }] as const,
allProjectQueries: () => ["projects"] as const,
getProjectById: (projectId: string) =>
[...projectKeys.allProjectQueries(), { projectId }] as const,
getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const,
getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const,
getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"],
getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"],
getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"],
getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"],
getAllUserProjects: () => ["projects"] as const,
getAllUserProjects: () => [...projectKeys.allProjectQueries()] as const,
getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const,
getProjectUsers: (
projectId: string,
@@ -28,7 +30,8 @@ export const projectKeys = {
// allows invalidation using above key without knowing params
getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) =>
[...projectKeys.getProjectIdentityMemberships(projectId), params] as const,
searchProject: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const,
searchProject: (dto: TSearchProjectsDTO) =>
[...projectKeys.allProjectQueries(), "search-projects", dto] as const,
getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const,
getProjectGroupMembershipDetails: (projectId: string, groupId: string) =>
[{ projectId, groupId }, "project-group-membership-details"] as const,

View File

@@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => {
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
"/api/v1/sub-organizations",
dto,
{
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
}
dto
);
return data;
},

View File

@@ -80,7 +80,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
return "Free";
};
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
const getFormattedSupportEmailLink = (variables: {
org_id: string;
domain: string;
root_org_id?: string;
}) => {
const email = "support@infisical.com";
const body = `Hello Infisical Support Team,
@@ -94,6 +98,7 @@ Issue Details:
Account Info:
- Organization ID: ${variables.org_id}
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
- Domain: ${variables.domain}
Thank you,
@@ -169,6 +174,10 @@ export const Navbar = () => {
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
const rootOrg = isSubOrganization
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
: currentOrg;
useEffect(() => {
if (isModalIntrusive) {
setShowCardDeclinedModal(true);
@@ -182,10 +191,20 @@ export const Navbar = () => {
}
}, [subscription, isBillingPage, isModalIntrusive]);
const handleOrgChange = async (orgId: string) => {
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
const handleOrgSelection = async ({
organizationId,
navigateTo,
onSuccess
}: {
organizationId?: string;
navigateTo?: string;
onSuccess?: () => void | Promise<void>;
}) => {
if (!organizationId) return;
if (organizationId === currentOrg.id) return;
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
@@ -193,14 +212,58 @@ export const Navbar = () => {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
setMfaSuccessCallback(() => async () => {
await handleOrgSelection({ organizationId, onSuccess });
});
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken });
await navigateUserToOrg({ navigate, organizationId, navigateTo });
queryClient.removeQueries({ queryKey: projectKeys.allProjectQueries() });
if (onSuccess) {
await onSuccess();
}
};
const handleNavigateToRootOrgBilling = async () => {
const navigateToBilling = () => {
navigate({
to: "/organizations/$orgId/billing",
params: { orgId: rootOrg.id }
});
};
const onSuccess = () => {
setShowCardDeclinedModal(false);
};
if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess });
} else {
await navigateToBilling();
}
};
const handleNavigateToAdminConsole = async () => {
const navigateToAdminConsole = () => {
navigate({
to: "/admin"
});
};
if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, navigateTo: "/admin" });
} else {
navigateToAdminConsole();
}
};
const { mutateAsync } = useGetOrgTrialUrl();
@@ -272,7 +335,7 @@ export const Navbar = () => {
return;
}
handleOrgChange(org?.id);
handleOrgSelection({ organizationId: org?.id });
};
return (
@@ -314,17 +377,20 @@ export const Navbar = () => {
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button"
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
await handleOrgSelection({
organizationId: currentOrg.rootOrgId as string
});
} else {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
}
}}
>
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
<span className="truncate">{currentOrg?.name}</span>
<span className="truncate">{rootOrg?.name}</span>
<Badge variant="org" className="hidden lg:inline-flex">
Organization
</Badge>
@@ -397,13 +463,7 @@ export const Navbar = () => {
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
className="cursor-pointer font-normal"
key={subOrg.id}
>
@@ -458,79 +518,93 @@ export const Navbar = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
{currentOrg.subOrganization && (
{isSubOrganization && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Badge
asChild
isTruncatable
variant="sub-org"
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge(
"gap-x-1.5 text-sm",
isProjectScope &&
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
)}
>
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
<SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
</Link>
</Badge>
<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>
<ChevronRight size={18} className="mt-[3px] mr-3 text-mineshaft-400/70" />
<div
className={twMerge(
"relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
!isProjectScope && isSubOrganization
? "border-sub-org/15 bg-gradient-to-b from-sub-org/10 to-sub-org/[0.075]"
: "border-transparent"
)}
>
{/* scott: the below is used to hide the top border from the org nav bar */}
{!isProjectScope && isSubOrganization && (
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
<div className="h-full bg-sub-org/[0.075]" />
</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
)}
<DropdownMenu modal={false}>
<div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
<button
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button"
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
params: { orgId: currentOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
if (isSubOrganization) {
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)}
<SubOrgIcon className={twMerge("size-[14px] shrink-0 text-sub-org")} />
<span className="truncate">{currentOrg?.name}</span>
<Badge variant="sub-org" className="hidden lg:inline-flex">
Sub-Organization
</Badge>
</button>
</div>
<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" }}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
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>
</div>
</>
)}
{isProjectScope && (
@@ -550,11 +624,11 @@ export const Navbar = () => {
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => {
if (!subscription || !currentOrg) return;
if (!subscription || !rootOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
orgId: rootOrg.id,
success_url: window.location.href
});
@@ -575,6 +649,7 @@ export const Navbar = () => {
<Link
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
onClick={handleNavigateToAdminConsole}
>
<InstanceIcon className="inline-block size-3.5" />
<span className="ml-2 hidden md:inline-block">Server Console</span>
@@ -612,7 +687,8 @@ export const Navbar = () => {
text === "Email Support"
? getUrl({
org_id: currentOrg.id,
domain: window.location.origin
domain: window.location.origin,
...(isSubOrganization && { root_org_id: rootOrg.id })
})
: getUrl();
@@ -772,19 +848,13 @@ export const Navbar = () => {
</div>
<div className="mt-4">
<div className="flex space-x-3">
<Link
to="/organizations/$orgId/billing"
params={{ orgId: currentOrg.id }}
className="inline-flex"
<Button
colorSchema="primary"
variant="solid"
onClick={handleNavigateToRootOrgBilling}
>
<Button
colorSchema="primary"
variant="solid"
onClick={() => setShowCardDeclinedModal(false)}
>
Update Payment Method
</Button>
</Link>
Update Payment Method
</Button>
{!isModalIntrusive && (
<Button
colorSchema="secondary"
@@ -810,6 +880,7 @@ export const Navbar = () => {
onClose={() => {
setShowSubOrgForm(false);
}}
handleOrgSelection={handleOrgSelection}
/>
</div>
</ModalContent>

View File

@@ -1,15 +1,18 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateSubOrganization } from "@app/hooks/api";
import { selectOrganization } from "@app/hooks/api/auth/queries";
import { slugSchema } from "@app/lib/schemas";
type ContentProps = {
onClose: () => void;
handleOrgSelection: (params: { organizationId: string }) => void;
};
const AddOrgSchema = z.object({
@@ -18,7 +21,8 @@ const AddOrgSchema = z.object({
type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
export const NewSubOrganizationForm = ({ onClose, handleOrgSelection }: ContentProps) => {
const { currentOrg, isSubOrganization } = useOrganization();
const createSubOrg = useCreateSubOrganization();
const {
@@ -32,10 +36,16 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
resolver: zodResolver(AddOrgSchema)
});
const navigate = useNavigate();
const router = useRouter();
const onSubmit = async ({ name }: FormData) => {
if (isSubOrganization && currentOrg.rootOrgId) {
const { token } = await selectOrganization({
organizationId: currentOrg.rootOrgId
});
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
}
const { organization } = await createSubOrg.mutateAsync({
name
});
@@ -46,11 +56,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
});
onClose();
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: organization.id }
});
await router.invalidate({ sync: true }).catch(() => null);
await handleOrgSelection({ organizationId: organization.id });
};
return (

View File

@@ -1,5 +1,6 @@
import { Link, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2";
@@ -21,7 +22,14 @@ export const OrgNavBar = ({ isHidden }: Props) => {
return (
<div className="bg-mineshaft-900">
{!isHidden && (
<div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5">
<div
className={twMerge(
"dark flex w-full flex-col overflow-x-hidden border-y bg-gradient-to-b px-4 pt-0.5",
isRootOrganization
? "border-t-org/15 border-b-org/5 from-org/[0.075] to-org/[0.025]"
: "border-t-sub-org/15 border-b-sub-org/5 from-sub-org/[0.075] to-sub-org/[0.025]"
)}
>
<motion.div
key="menu-org-items"
initial={{ x: -150 }}

View File

@@ -8,7 +8,7 @@ import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
export const AssumePrivilegeModeBanner = () => {
const { isSubOrganization, currentOrg } = useOrganization();
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
const { assumedPrivilegeDetails } = useProjectPermission();
@@ -37,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => {
},
{
onSuccess: () => {
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`;
const url = getProjectHomePage(currentProject.type, currentProject.environments);
window.location.assign(
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
);

View File

@@ -157,20 +157,11 @@ const ProjectSelectInner = () => {
params: {
projectId: workspace.id,
orgId: workspace.orgId
},
search: {
subOrganization: currentOrg?.subOrganization?.name
}
});
const urlInstance = new URL(
`${window.location.origin}${url.to.replaceAll("$orgId", url.params.orgId).replaceAll("$projectId", url.params.projectId)}`
);
if (currentOrg?.subOrganization) {
urlInstance.searchParams.set(
"subOrganization",
currentOrg.subOrganization.name
);
}
window.location.assign(urlInstance);
}}
icon={

View File

@@ -5,7 +5,17 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
type NavigateUserToOrgParams = {
navigate: NavigateFn;
organizationId?: string;
navigateTo?: string;
};
export const navigateUserToOrg = async ({
navigate,
organizationId,
navigateTo
}: NavigateUserToOrgParams) => {
const userOrgs = await fetchOrganizations();
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
@@ -13,7 +23,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
navigate({
to: "/organizations/$orgId/projects",
to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: organizationId }
});
return;
@@ -24,7 +34,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
navigate({
to: "/organizations/$orgId/projects",
to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: userOrg }
});
} else {

View File

@@ -115,7 +115,7 @@ export const PasswordStep = ({
return;
}
await navigateUserToOrg(navigate, organizationId);
await navigateUserToOrg({ navigate, organizationId });
};
await finishWithOrgWorkflow();
@@ -131,7 +131,7 @@ export const PasswordStep = ({
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(navigate);
await navigateUserToOrg({ navigate });
}
}
} catch (err: any) {
@@ -233,7 +233,7 @@ export const PasswordStep = ({
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(navigate);
await navigateUserToOrg({ navigate });
}
}
} else {
@@ -254,7 +254,7 @@ export const PasswordStep = ({
// case: organization ID is present from the provider auth token -- navigate directly to the org
if (organizationId) {
await navigateUserToOrg(navigate, organizationId);
await navigateUserToOrg({ navigate, organizationId });
}
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page
@@ -264,7 +264,7 @@ export const PasswordStep = ({
if (userOrgs.length > 0) {
navigateToSelectOrganization(undefined, isAdminLogin);
} else {
await navigateUserToOrg(navigate);
await navigateUserToOrg({ navigate });
}
}
}
@@ -316,7 +316,7 @@ export const PasswordStep = ({
return (
<EmailDuplicationConfirmation
onRemoveDuplicateLater={() =>
navigateUserToOrg(navigate, organizationId).catch(() =>
navigateUserToOrg({ navigate, organizationId }).catch(() =>
createNotification({ text: "Failed to navigate user", type: "error" })
)
}

View File

@@ -47,6 +47,7 @@ export const SelectOrganizationSection = () => {
const orgId = queryParams.get("org_id");
const callbackPort = queryParams.get("callback_port");
const isAdminLogin = queryParams.get("is_admin_login") === "true";
const mfaPending = queryParams.get("mfa_pending") === "true";
const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId);
const logout = useLogoutUser(true);
@@ -188,7 +189,7 @@ export const SelectOrganizationSection = () => {
navigate({ to: "/cli-redirect" });
// cli page
} else {
navigateUserToOrg(navigate, organization.id);
navigateUserToOrg({ navigate, organizationId: organization.id });
}
},
[selectOrg]
@@ -201,7 +202,7 @@ export const SelectOrganizationSection = () => {
const decodedJwt = jwtDecode(authToken) as any;
if (decodedJwt?.organizationId) {
navigateUserToOrg(navigate, decodedJwt.organizationId);
navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId });
}
}
@@ -236,10 +237,22 @@ export const SelectOrganizationSection = () => {
}, [organizations.isPending, organizations.data]);
useEffect(() => {
if (mfaPending && defaultSelectedOrg) {
const storedMfaToken = sessionStorage.getItem(SessionStorageKeys.MFA_TEMP_TOKEN);
if (storedMfaToken) {
sessionStorage.removeItem(SessionStorageKeys.MFA_TEMP_TOKEN);
SecurityClient.setMfaToken(storedMfaToken);
setIsInitialOrgCheckLoading(false);
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleSelectOrganization(defaultSelectedOrg));
return;
}
}
if (defaultSelectedOrg) {
handleSelectOrganization(defaultSelectedOrg);
}
}, [defaultSelectedOrg]);
}, [defaultSelectedOrg, mfaPending]);
if (
userLoading ||

View File

@@ -8,7 +8,8 @@ export const SelectOrganizationPageQueryParams = z.object({
org_id: z.string().optional().catch(""),
callback_port: z.coerce.number().optional().catch(undefined),
is_admin_login: z.boolean().optional().catch(false),
force: z.boolean().optional()
force: z.boolean().optional(),
mfa_pending: z.boolean().optional().catch(false)
});
export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({
@@ -16,7 +17,12 @@ export const Route = createFileRoute("/_restrict-login-signup/login/select-organ
validateSearch: zodValidator(SelectOrganizationPageQueryParams),
search: {
middlewares: [
stripSearchParams({ org_id: "", callback_port: undefined, is_admin_login: false })
stripSearchParams({
org_id: "",
callback_port: undefined,
is_admin_login: false,
mfa_pending: false
})
]
}
});

View File

@@ -18,7 +18,7 @@ const tabs = [
export const SettingsPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>

View File

@@ -19,7 +19,7 @@ const tabs = [
export const SettingsPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -39,7 +39,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>

View File

@@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({
});
});
return { organizationId: data.organizationId as string, isAuthenticated: true, user };
const isSubOrganization = !!data.subOrganizationId;
return {
organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string),
isAuthenticated: true,
user
};
}
});

View File

@@ -1,7 +1,12 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, isRedirect, redirect } from "@tanstack/react-router";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { SessionStorageKeys } from "@app/const";
import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
import { projectKeys } from "@app/hooks/api/projects";
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations";
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
// Route context to fill in organization's data like details, subscription etc
@@ -15,6 +20,44 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({
organizationId = context.organizationId!;
}
if ((params as { orgId?: string })?.orgId && context.organizationId) {
const urlOrgId = (params as { orgId: string }).orgId;
const currentTokenOrgId = context.organizationId;
if (urlOrgId !== currentTokenOrgId) {
try {
const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId });
if (isMfaEnabled) {
sessionStorage.setItem(SessionStorageKeys.MFA_TEMP_TOKEN, token);
throw redirect({
to: "/login/select-organization",
search: { org_id: urlOrgId, mfa_pending: true }
});
}
if (!isMfaEnabled && token) {
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() });
await context.queryClient.fetchQuery({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
});
}
} catch (error) {
if (isRedirect(error)) {
throw error;
}
console.warn("Failed to automatically exchange token for organization:", error);
}
}
}
await context.queryClient.ensureQueryData({
queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId)

View File

@@ -117,9 +117,10 @@ export const Route = createFileRoute("/_restrict-login-signup")({
return;
throw redirect({ to: "/login/select-organization" });
}
const orgId = data.subOrganizationId || data.organizationId;
throw redirect({
to: "/organizations/$orgId/projects",
params: { orgId: data.organizationId }
params: { orgId }
});
},
component: AuthConsentWrapper

View File

@@ -3,7 +3,8 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
@@ -84,8 +85,20 @@ export const AccessManagementPage = () => {
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
/>
description={`Manage fine-grained access for users, groups, roles, and machine identities within your ${isSubOrganization ? "sub-" : ""}organization resources.`}
>
{isSubOrganization && (
<Link
to="/organizations/$orgId/access-management"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization access control?
</Link>
)}
</PageHeader>
{!currentOrg.shouldUseNewPrivilegeSystem && (
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">

View File

@@ -6,7 +6,12 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import {
OrgPermissionGroupActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -15,6 +20,7 @@ import { OrgGroupsTable } from "./OrgGroupsTable";
export const OrgGroupsSection = () => {
const { subscription } = useSubscription();
const { isSubOrganization } = useOrganization();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -51,7 +57,9 @@ export const OrgGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Organization Groups</p>
<p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Groups
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
</div>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
@@ -63,7 +71,7 @@ export const OrgGroupsSection = () => {
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Organization Group
Create {isSubOrganization ? "Sub-" : ""}Organization Group
</Button>
)}
</OrgPermissionCan>

View File

@@ -71,7 +71,7 @@ enum GroupsOrderBy {
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { isPending, data: groups = [] } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
@@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization groups..."
placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization groups...`}
/>
<TableContainer className="mt-4">
<Table>
@@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
</Th>
<Th>
<div className="flex items-center">
Organization Role
{isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
@@ -389,8 +389,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<EmptyState
title={
groups.length
? "No organization groups match search..."
: "No organization groups found"
? `No ${isSubOrganization ? "sub-" : ""}organization groups match search...`
: `No ${isSubOrganization ? "sub-" : ""}organization groups found`
}
icon={groups.length ? faSearch : faUsers}
/>

View File

@@ -1,13 +1,13 @@
import { useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { LinkIcon, PlusIcon } from "lucide-react";
import { InfoIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import { Button, DeleteActionModal, Modal, ModalContent, Tooltip } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import {
OrgPermissionIdentityActions,
@@ -30,9 +30,8 @@ import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm";
import { OrgIdentityModal } from "./OrgIdentityModal";
enum IdentityWizardSteps {
SelectAction = "select-action",
LinkIdentity = "link-identity",
OrganizationIdentity = "project-identity"
CreateIdentity = "create-identity",
LinkIdentity = "link-identity"
}
export const IdentitySection = withPermission(
@@ -41,7 +40,7 @@ export const IdentitySection = withPermission(
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.SelectAction);
const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.CreateIdentity);
const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity();
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
@@ -100,7 +99,7 @@ export const IdentitySection = withPermission(
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-1 items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">
Organization Machine Identities
{isSubOrganization ? "Sub-" : ""}Organization Machine Identities
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
</div>
@@ -124,7 +123,7 @@ export const IdentitySection = withPermission(
}
if (!isSubOrganization) {
setWizardStep(IdentityWizardSteps.OrganizationIdentity);
setWizardStep(IdentityWizardSteps.CreateIdentity);
}
handlePopUpOpen("identity");
@@ -197,7 +196,7 @@ export const IdentitySection = withPermission(
onOpenChange={(open) => {
handlePopUpToggle("identity", open);
if (!open) {
setWizardStep(IdentityWizardSteps.SelectAction);
setWizardStep(IdentityWizardSteps.CreateIdentity);
}
}}
>
@@ -214,80 +213,84 @@ export const IdentitySection = withPermission(
: undefined
}
>
<AnimatePresence mode="wait">
{wizardStep === IdentityWizardSteps.SelectAction && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div
className="cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.OrganizationIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.OrganizationIdentity);
}
{isSubOrganization && (
<div className="mb-4 flex items-center justify-center gap-x-2">
<div className="flex w-3/4 gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
setWizardStep(IdentityWizardSteps.CreateIdentity);
}}
size="xs"
className={twMerge(
"min-w-[2.4rem] flex-1 rounded border-none hover:bg-mineshaft-600",
wizardStep === IdentityWizardSteps.CreateIdentity
? "bg-mineshaft-500"
: "bg-transparent"
)}
>
<div className="flex items-center gap-2">
<PlusIcon size="1rem" />
<div>Create Machine Identity</div>
</div>
<div className="mt-2 text-xs text-mineshaft-300">
Create a new machine identity specifically for this sub-organization. This
machine identity will be managed at the sub-organization level.
</div>
</div>
<div
className="mt-4 cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.LinkIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.LinkIdentity);
}
Create New
</Button>
<Button
variant="outline_bg"
onClick={() => {
setWizardStep(IdentityWizardSteps.LinkIdentity);
}}
size="xs"
className={twMerge(
"min-w-[2.4rem] flex-1 rounded border-none hover:bg-mineshaft-600",
wizardStep === IdentityWizardSteps.LinkIdentity
? "bg-mineshaft-500"
: "bg-transparent"
)}
>
<div className="flex items-center gap-2">
<LinkIcon size="1rem" />
<div>Assign Existing Machine Identity</div>
</div>
<div className="mt-2 text-xs text-mineshaft-300">
Assign an existing machine identity from your parent organization. The machine
identity will continue to be managed at its original scope.
</div>
</div>
</motion.div>
)}
{wizardStep === IdentityWizardSteps.OrganizationIdentity && (
<motion.div
key="identity-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
Assign Existing
</Button>
</div>
<Tooltip
className="max-w-sm"
position="right"
align="start"
content={
<>
<p className="mb-2 text-mineshaft-300">
You can add machine identities to your sub-organization in one of two ways:
</p>
<ul className="ml-3.5 flex list-disc flex-col gap-y-4">
<li className="text-mineshaft-200">
<strong className="font-medium text-mineshaft-100">Create New</strong> -
Create a new machine identity specifically for this sub-organization. This
machine identity will be managed at the sub-organization level.
<p className="mt-2">
This method is recommended for autonomous teams that need to manage
machine identity authentication.
</p>
</li>
<li>
<strong className="font-medium text-mineshaft-100">
Assign Existing
</strong>{" "}
Assign an existing machine identity from your parent organization. The
machine identity will continue to be managed at its original scope.
<p className="mt-2">
This method is recommended for organizations that need to maintain
centralized control.
</p>
</li>
</ul>
</>
}
>
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</motion.div>
)}
{wizardStep === IdentityWizardSteps.LinkIdentity && (
<motion.div
key="link-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<OrgIdentityLinkForm onClose={() => handlePopUpClose("identity")} />
</motion.div>
)}
</AnimatePresence>
<InfoIcon size={16} className="text-mineshaft-400" />
</Tooltip>
</div>
)}
{wizardStep === IdentityWizardSteps.CreateIdentity && (
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
)}
{wizardStep === IdentityWizardSteps.LinkIdentity && (
<OrgIdentityLinkForm onClose={() => handlePopUpClose("identity")} />
)}
</ModalContent>
</Modal>
<DeleteActionModal

View File

@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Filter Organization Machine Identities by Role
Filter {isSubOrganization ? "Sub-" : ""}Organization Machine Identities by Role
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
@@ -229,7 +229,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search machine identities by name..."
placeholder={`Search ${isSubOrganization ? "sub-organization" : "organization"} machine identities by name...`}
/>
</div>
<TableContainer>
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</Th>
<Th>
<div className="flex items-center">
Organization Role
{isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
@@ -304,7 +304,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
to: "/organizations/$orgId/identities/$identityId",
params: {
identityId: id,
orgId
orgId: currentOrg.id
}
})
}
@@ -455,8 +455,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<EmptyState
title={
debouncedSearch.trim().length > 0 || filter.roles?.length > 0
? "No machine identities match search filter"
: "No machine identities have been created in this organization"
? `No ${isSubOrganization ? "sub-" : ""}organization machine identities match search filter`
: `No machine identities have been created in this ${isSubOrganization ? "sub-" : ""}organization`
}
icon={faServer}
/>

View File

@@ -205,7 +205,9 @@ export const OrgMembersSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Organization Users</p>
<p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Users
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
@@ -242,7 +244,7 @@ export const OrgMembersSection = () => {
isOpen={popUp.addMemberToSubOrg.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)}
>
<ModalContent title="Add member from your organization">
<ModalContent title="Add member from your organization" bodyClassName="overflow-visible">
<AddSubOrgMemberModal onClose={() => handlePopUpClose("addMemberToSubOrg")} />
</ModalContent>
</Modal>

View File

@@ -336,7 +336,7 @@ export const OrgMembersTable = ({
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Filter Organization Users by Role
Filter {isSubOrganization ? "Sub-" : ""}Organization Users by Role
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
@@ -365,7 +365,7 @@ export const OrgMembersTable = ({
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization users..."
placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization users...`}
/>
</div>
<TableContainer className="mt-4">
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Organization Role
{isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
@@ -727,8 +727,8 @@ export const OrgMembersTable = ({
<EmptyState
title={
members.length
? "No organization users match search..."
: "No organization users found"
? `No ${isSubOrganization ? "sub-" : ""}organization users match search...`
: `No ${isSubOrganization ? "sub-" : ""}organization users found`
}
icon={members.length ? faSearch : faUsers}
/>

View File

@@ -207,7 +207,7 @@ export const OrgRoleTable = () => {
}}
isDisabled={!isAllowed}
>
Add Organization Role
Add {isSubOrganization ? "Sub-" : ""}Organization Role
</Button>
)}
</OrgPermissionCan>
@@ -216,7 +216,7 @@ export const OrgRoleTable = () => {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization roles..."
placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization roles...`}
className="flex-1"
containerClassName="mb-4"
/>

View File

@@ -1,4 +1,6 @@
import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
@@ -6,7 +8,7 @@ import { useOrganization } from "@app/context";
import { LogsSection } from "./components";
export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization();
const { isSubOrganization, currentOrg } = useOrganization();
return (
<div className="h-full bg-bunker-800">
@@ -21,7 +23,19 @@ export const AuditLogsPage = () => {
scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
description="Audit logs for security and compliance teams to monitor information access."
/>
>
{isSubOrganization && (
<Link
to="/organizations/$orgId/audit-logs"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization audit logs?
</Link>
)}
</PageHeader>
<LogsSection pageView />
</div>
</div>

View File

@@ -88,7 +88,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Organization Groups
{isSubOrganization ? "Sub-" : ""}Organization Groups
</Link>
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
@@ -8,7 +10,7 @@ import { OrgTabGroup } from "./components";
export const SettingsPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
const { isSubOrganization, currentOrg } = useOrganization();
return (
<>
@@ -19,9 +21,21 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl">
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings"
description={`Configure ${isSubOrganization ? "sub-" : ""}organization-wide settings`}
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
/>
>
{isSubOrganization && (
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization settings?
</Link>
)}
</PageHeader>
<OrgTabGroup />
</div>
</div>

View File

@@ -53,7 +53,7 @@ const OrgConfigSection = ({
resolver: zodResolver(orgConfigFormSchema)
});
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id);
const onFormSubmit = async (formData: TKmipOrgConfigForm) => {
@@ -175,7 +175,10 @@ const OrgConfigSection = ({
)}
{!isKmipConfigLoading && !kmipConfig && (
<div className="mt-2">
<div>KMIP has not yet been configured for the organization.</div>
<div>
KMIP has not yet been configured for the {isSubOrganization ? "sub-" : ""}
organization.
</div>
<Button
className="mt-2"
onClick={() => {

View File

@@ -47,7 +47,7 @@ export const OrgDeleteSection = () => {
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={Boolean(!hasOrgRole(OrgMembershipRole.Admin))}
>
{`Delete ${currentOrg.subOrganization?.name ?? currentOrg?.name}`}
{`Delete ${currentOrg?.name}`}
</Button>
</div>
<DeleteActionModal

View File

@@ -34,7 +34,7 @@ import { ExternalKmsItem } from "./ExternalKmsItem";
export const OrgEncryptionTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const { subscription } = useSubscription();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
@@ -90,7 +90,7 @@ export const OrgEncryptionTab = withPermission(
</OrgPermissionCan>
</div>
<p className="mb-4 text-gray-400">
Encrypt your organization&apos;s data with external KMS.
Encrypt your {isSubOrganization ? "sub-" : ""}organization&apos;s data with external KMS.
</p>
<TableContainer>
<Table>

View File

@@ -33,7 +33,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => {
const { handleSubmit, control } = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: currentOrg?.subOrganization?.name || ""
name: currentOrg?.name || ""
}
});
const { mutateAsync, isPending } = useUpdateSubOrganization();
@@ -60,7 +60,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => {
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
<div>
<h2 className="text-md mb-2 text-mineshaft-100">Organization Name</h2>
<h2 className="text-md mb-2 text-mineshaft-100">Sub-Organization Name</h2>
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (

View File

@@ -112,7 +112,7 @@ const Page = withPermission(
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Organization Users
{isSubOrganization ? "Sub-" : ""}Organization Users
</Link>
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}

View File

@@ -11,7 +11,7 @@ import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/Pr
export const SettingsPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -31,7 +31,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general">

View File

@@ -19,7 +19,7 @@ import {
const Page = () => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const { currentProject } = useProject();
const selectedTab = useSearch({
strict: false,
@@ -54,7 +54,8 @@ const Page = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization access control?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization access
control?
</Link>
</PageHeader>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>

View File

@@ -3,12 +3,12 @@ import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2";
import { useProject } from "@app/context";
import { useOrganization, useProject } from "@app/context";
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components";
export const AuditLogsPage = () => {
const { currentProject } = useProject();
const { isSubOrganization } = useOrganization();
return (
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
@@ -29,7 +29,8 @@ export const AuditLogsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization audit logs?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization audit
logs?
</Link>
</PageHeader>
<LogsSection pageView project={currentProject} />

View File

@@ -86,7 +86,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage);
};
const { isSubOrganization, currentOrg } = useOrganization();
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const { data: groupMemberships, isPending } = useListProjectGroupUsers({
@@ -154,7 +154,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
text: "User privilege assumption has started"
});
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`;
const url = getProjectHomePage(currentProject.type, currentProject.environments);
window.location.assign(
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
);

View File

@@ -63,7 +63,7 @@ const Page = () => {
select: (el) => el.identityId as string
});
const { currentProject, projectId } = useProject();
const { currentOrg, isSubOrganization } = useOrganization();
const { currentOrg } = useOrganization();
const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } =
useGetProjectIdentityMembershipV2(projectId, identityId);
@@ -71,8 +71,6 @@ const Page = () => {
const { mutateAsync: removeIdentityMutateAsync } = useDeleteProjectIdentityMembership();
const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId);
const isNonScopedIdentity =
!isProjectIdentity && currentOrg.id !== identityMembershipDetails?.identity?.orgId;
const {
data: identity,
@@ -108,7 +106,7 @@ const Page = () => {
type: "success",
text: "Machine identity privilege assumption has started"
});
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization && isNonScopedIdentity ? `?subOrganization=${currentOrg.slug}` : ""}`;
const url = getProjectHomePage(currentProject.type, currentProject.environments);
window.location.assign(
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
);

View File

@@ -208,7 +208,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
isDisabled={isIdentityEditDisabled}
className={twMerge(
"w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
"bg-mineshaft-600 hover:bg-mineshaft-500 w-full border-none py-2.5 text-xs capitalize",
isTemporary && "text-primary",
isExpired && "text-red-600"
)}
@@ -226,7 +226,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
className="border border-gray-600 pt-4"
>
<div className="flex flex-col space-y-4">
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
<div className="text-mineshaft-300 border-b border-b-gray-700 pb-2 text-sm">
Configure Timed Access
</div>
{isExpired && <Tag colorSchema="red">Expired</Tag>}
@@ -306,7 +306,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<div className="flex items-center space-x-4">
{isDirty && (
<Button
className="mr-4 text-mineshaft-300"
className="text-mineshaft-300 mr-4"
variant="link"
isDisabled={isSubmitting}
isLoading={isSubmitting}
@@ -316,7 +316,11 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
</Button>
)}
<div className="flex items-center">
<AddPoliciesButton isDisabled={isDisabled} projectType={currentProject.type} />
<AddPoliciesButton
isDisabled={isDisabled}
projectType={currentProject.type}
projectId={projectId}
/>
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@ export const Page = () => {
strict: false,
select: (el) => el.membershipId as string
});
const { currentOrg, isSubOrganization } = useOrganization();
const { currentOrg } = useOrganization();
const { currentProject, projectId } = useProject();
const { data: membershipDetails, isPending: isMembershipDetailsLoading } =
@@ -73,7 +73,7 @@ export const Page = () => {
text: "User privilege assumption has started"
});
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`;
const url = getProjectHomePage(currentProject.type, currentProject.environments);
window.location.assign(
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
);

View File

@@ -216,7 +216,11 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} projectType={currentProject.type} />
<AddPoliciesButton
isDisabled={isDisabled}
projectType={currentProject.type}
projectId={projectId}
/>
</div>
</div>
</div>

View File

@@ -21,9 +21,10 @@ import { VaultPolicyImportModal } from "@app/pages/project/RoleDetailsBySlugPage
type Props = {
isDisabled?: boolean;
projectType: ProjectType;
projectId?: string;
};
export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
export const AddPoliciesButton = ({ isDisabled, projectType, projectId }: Props) => {
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addPolicy",
"addPolicyOptions",
@@ -109,6 +110,7 @@ export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => {
</DropdownMenuContent>
</DropdownMenu>
<PolicySelectionModal
projectId={projectId}
type={projectType}
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}

View File

@@ -18,7 +18,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionSub } from "@app/context";
import { useGetWorkspaceIntegrations } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/projects/types";
@@ -34,23 +34,25 @@ type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
type: ProjectType;
projectId?: string;
};
type ContentProps = {
onClose: () => void;
// note(daniel): we allow projectId to be undefined because we use this component for project templates, in which case no project ID will be present.
projectId?: string;
type: ProjectType;
};
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
const Content = ({ onClose, type: projectType }: ContentProps) => {
const Content = ({ onClose, projectId, type: projectType }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>();
const [search, setSearch] = useState("");
const { currentProject, projectId } = useProject();
const isSecretManagerProject = currentProject.type === ProjectType.SecretManager;
const { data: integrations = [] } = useGetWorkspaceIntegrations(projectId, {
enabled: isSecretManagerProject,
const isSecretManagerProject = projectType === ProjectType.SecretManager;
const { data: integrations = [] } = useGetWorkspaceIntegrations(projectId ?? "", {
enabled: Boolean(isSecretManagerProject && projectId),
refetchInterval: false
});
@@ -216,7 +218,7 @@ const Content = ({ onClose, type: projectType }: ContentProps) => {
);
};
export const PolicySelectionModal = ({ isOpen, onOpenChange, type }: Props) => {
export const PolicySelectionModal = ({ isOpen, onOpenChange, type, projectId }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -224,7 +226,7 @@ export const PolicySelectionModal = ({ isOpen, onOpenChange, type }: Props) => {
subTitle="Select one or more policies to add to this role."
className="max-w-3xl"
>
<Content onClose={() => onOpenChange(false)} type={type} />
<Content onClose={() => onOpenChange(false)} type={type} projectId={projectId} />
</ModalContent>
</Modal>
);

View File

@@ -209,7 +209,11 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
Save
</Button>
<div className="ml-2 border-l border-mineshaft-500 pl-4">
<AddPoliciesButton isDisabled={isDisabled} projectType={currentProject.type} />
<AddPoliciesButton
isDisabled={isDisabled}
projectType={currentProject.type}
projectId={projectId}
/>
</div>
</div>
)}

View File

@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { NotificationContainer } from "@app/components/notifications";
import { TooltipProvider } from "@app/components/v2";
@@ -14,15 +13,6 @@ type TRouterContext = {
};
const RootPage = () => {
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
useEffect(() => {
queryClient.invalidateQueries();
}, [subOrganization]);
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>

View File

@@ -4,7 +4,7 @@ import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context";
import { useOrganization, useProject } from "@app/context";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
@@ -15,6 +15,8 @@ import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection"
export const SettingsPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
const { currentProject } = useProject();
const tabs = [
{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab },
@@ -55,7 +57,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}>

View File

@@ -14,7 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
export const SettingsPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general">

View File

@@ -14,7 +14,7 @@ import { ProjectSshTab } from "./components/ProjectSshTab";
export const SettingsPage = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general">