mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
fix: new sub-org creation + sub-org nav styling
This commit is contained in:
@@ -198,10 +198,12 @@ export const Navbar = () => {
|
||||
const handleOrgSelection = async ({
|
||||
organizationId,
|
||||
subOrganizationId,
|
||||
navigateTo,
|
||||
onSuccess
|
||||
}: {
|
||||
organizationId?: string;
|
||||
subOrganizationId?: string;
|
||||
navigateTo?: string;
|
||||
onSuccess?: () => void | Promise<void>;
|
||||
}) => {
|
||||
if (!organizationId && !subOrganizationId) return;
|
||||
@@ -234,7 +236,7 @@ export const Navbar = () => {
|
||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||
|
||||
await router.invalidate();
|
||||
await navigateUserToOrg(navigate, targetId);
|
||||
await navigateUserToOrg({ navigate, organizationId: targetId, navigateTo });
|
||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||
|
||||
if (onSuccess) {
|
||||
@@ -243,21 +245,38 @@ export const Navbar = () => {
|
||||
};
|
||||
|
||||
const handleNavigateToRootOrgBilling = async () => {
|
||||
const navigateToBilling = async () => {
|
||||
const navigateToBilling = () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/billing",
|
||||
params: { orgId: rootOrg.id }
|
||||
});
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
setShowCardDeclinedModal(false);
|
||||
};
|
||||
|
||||
if (currentOrg.id !== rootOrg.id) {
|
||||
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess: navigateToBilling });
|
||||
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();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
@@ -368,14 +387,14 @@ export const Navbar = () => {
|
||||
<button
|
||||
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
onClick={() => {
|
||||
if (isSubOrganization) {
|
||||
handleOrgSelection({ organizationId: currentOrg.rootOrgId as string });
|
||||
}
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||
@@ -467,16 +486,18 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Boolean(subOrganizations.length) && (
|
||||
{Boolean(subOrganizations.length && !isSubOrganization) && (
|
||||
<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>
|
||||
{!isSubOrganization && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
)}{" "}
|
||||
</DropdownSubMenuContent>
|
||||
</DropdownSubMenu>
|
||||
);
|
||||
@@ -509,71 +530,93 @@ export const Navbar = () => {
|
||||
</div>
|
||||
{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.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-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
|
||||
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
)}
|
||||
<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: currentOrg.id }
|
||||
});
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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({ subOrganizationId: 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 && !isSubOrganization) && (
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
)}
|
||||
{!isSubOrganization && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isProjectScope && (
|
||||
@@ -618,6 +661,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>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useCreateSubOrganization } from "@app/hooks/api";
|
||||
import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api";
|
||||
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
@@ -20,6 +24,8 @@ type FormData = z.infer<typeof AddOrgSchema>;
|
||||
|
||||
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
const createSubOrg = useCreateSubOrganization();
|
||||
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -46,11 +52,18 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
});
|
||||
onClose();
|
||||
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: organization.id }
|
||||
const { token } = await selectOrganization({
|
||||
subOrganizationId: organization.id
|
||||
});
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||
await navigateUserToOrg({ navigate, organizationId: organization.id });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" })
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ export const SelectOrganizationSection = () => {
|
||||
navigate({ to: "/cli-redirect" });
|
||||
// cli page
|
||||
} else {
|
||||
navigateUserToOrg(navigate, organization.id);
|
||||
navigateUserToOrg({ navigate, organizationId: organization.id });
|
||||
}
|
||||
},
|
||||
[selectOrg]
|
||||
@@ -201,7 +201,7 @@ export const SelectOrganizationSection = () => {
|
||||
const decodedJwt = jwtDecode(authToken) as any;
|
||||
|
||||
if (decodedJwt?.organizationId) {
|
||||
navigateUserToOrg(navigate, decodedJwt.organizationId);
|
||||
navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user