mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 23:27:35 +00:00
feat: org sidebar first version
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
import { ReactNode, useRef } from "react";
|
||||||
|
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
||||||
|
|
||||||
|
export type LottieProps = {
|
||||||
|
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
||||||
|
children?: ReactNode;
|
||||||
|
icon?: string;
|
||||||
|
iconMode?: Mode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Lottie = ({ children, icon, iconMode, ...props }: LottieProps): JSX.Element => {
|
||||||
|
const iconRef = useRef<DotLottie | null>(null);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => iconRef.current?.play()}
|
||||||
|
onMouseLeave={() => iconRef.current?.stop()}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<DotLottieReact
|
||||||
|
dotLottieRefCallback={(el) => {
|
||||||
|
iconRef.current = el;
|
||||||
|
}}
|
||||||
|
mode={iconMode}
|
||||||
|
src={`/lotties/${icon}.json`}
|
||||||
|
loop
|
||||||
|
className="h-full w-full"
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Lottie } from "./Lottie";
|
||||||
@@ -22,6 +22,7 @@ export * from "./GenericFieldLabel";
|
|||||||
export * from "./HoverCardv2";
|
export * from "./HoverCardv2";
|
||||||
export * from "./IconButton";
|
export * from "./IconButton";
|
||||||
export * from "./Input";
|
export * from "./Input";
|
||||||
|
export * from "./Lottie";
|
||||||
export * from "./Menu";
|
export * from "./Menu";
|
||||||
export * from "./Modal";
|
export * from "./Modal";
|
||||||
export * from "./NoticeBanner";
|
export * from "./NoticeBanner";
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
|
import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||||
@@ -14,9 +13,9 @@ import { usePopUp } from "@app/hooks";
|
|||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||||
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
import { OrgSidebar } from "./components/OrgSidebar";
|
||||||
import { SidebarHeader } from "./components/SidebarHeader";
|
|
||||||
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
||||||
|
import { Navbar } from "./components/NavBar";
|
||||||
|
|
||||||
export const OrganizationLayout = () => {
|
export const OrganizationLayout = () => {
|
||||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||||
@@ -39,74 +38,18 @@ export const OrganizationLayout = () => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isSecretSharingPage = (
|
|
||||||
[
|
|
||||||
linkOptions({ to: "/organization/secret-sharing" }).to,
|
|
||||||
linkOptions({ to: "/organization/secret-sharing/settings" }).to
|
|
||||||
] as string[]
|
|
||||||
).includes(location.pathname);
|
|
||||||
|
|
||||||
const isProjectOverviewOrSettingsPage = (
|
|
||||||
[
|
|
||||||
linkOptions({ to: "/organization/secret-manager/overview" }).to,
|
|
||||||
linkOptions({ to: "/organization/secret-manager/settings" }).to,
|
|
||||||
linkOptions({ to: "/organization/cert-manager/overview" }).to,
|
|
||||||
linkOptions({ to: "/organization/cert-manager/settings" }).to,
|
|
||||||
linkOptions({ to: "/organization/kms/overview" }).to,
|
|
||||||
linkOptions({ to: "/organization/kms/settings" }).to,
|
|
||||||
linkOptions({ to: "/organization/ssh/overview" }).to,
|
|
||||||
linkOptions({ to: "/organization/ssh/settings" }).to,
|
|
||||||
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
|
|
||||||
linkOptions({ to: "/organization/secret-scanning/settings" }).to
|
|
||||||
] as string[]
|
|
||||||
).includes(location.pathname);
|
|
||||||
|
|
||||||
const shouldShowOrgSidebar =
|
|
||||||
location.pathname.startsWith("/organization") &&
|
|
||||||
(!isSecretSharingPage || shouldShowProductsSidebar);
|
|
||||||
|
|
||||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||||
|
|
||||||
let SideBarComponent = <DefaultSideBar />;
|
|
||||||
|
|
||||||
if (isSecretSharingPage) {
|
|
||||||
SideBarComponent = <SecretSharingSideBar />;
|
|
||||||
} else if (isProjectOverviewOrSettingsPage) {
|
|
||||||
SideBarComponent = (
|
|
||||||
<ProjectOverviewSideBar type={location.pathname.split("/")[2] as ProjectType} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Banner />
|
<Banner />
|
||||||
<div
|
<div
|
||||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||||
>
|
>
|
||||||
|
<Navbar />
|
||||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
<MinimizedOrgSidebar />
|
<OrgSidebar />
|
||||||
<AnimatePresence mode="popLayout">
|
|
||||||
{shouldShowOrgSidebar && (
|
|
||||||
<motion.div
|
|
||||||
key="menu-list-items"
|
|
||||||
initial={{ x: -150 }}
|
|
||||||
animate={{ x: 0 }}
|
|
||||||
exit={{ x: -150 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
|
|
||||||
>
|
|
||||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
|
||||||
{!isProjectOverviewOrSettingsPage && (
|
|
||||||
<div className="p-2 pt-3">
|
|
||||||
<SidebarHeader />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{SideBarComponent}
|
|
||||||
</nav>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<main
|
<main
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
|
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
|
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBook,
|
||||||
|
faCheck,
|
||||||
|
faEnvelope,
|
||||||
|
faInfo,
|
||||||
|
faSignOut,
|
||||||
|
faSlash,
|
||||||
|
faSort,
|
||||||
|
faUser,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Mfa } from "@app/components/auth/Mfa";
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Lottie
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { envConfig } from "@app/config/env";
|
||||||
|
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useGetOrganizations,
|
||||||
|
useGetOrgTrialUrl,
|
||||||
|
useLogoutUser,
|
||||||
|
workspaceKeys
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
|
import { SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
|
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||||
|
|
||||||
|
const getPlan = (subscription: SubscriptionPlan) => {
|
||||||
|
if (subscription.groups) return "Enterprise";
|
||||||
|
if (subscription.pitRecovery) return "Pro";
|
||||||
|
return "Free";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||||
|
"Support Forum",
|
||||||
|
"https://infisical.com/slack"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||||
|
"Read Docs",
|
||||||
|
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||||
|
"GitHub Issues",
|
||||||
|
"https://github.com/Infisical/infisical/issues"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||||
|
"Email Support",
|
||||||
|
"mailto:[email protected]"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||||
|
"Instance Admins",
|
||||||
|
"server-admins"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Navbar = () => {
|
||||||
|
const { user } = useUser();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const [openSupport, setOpenSupport] = useState(false);
|
||||||
|
const [openUser, setOpenUser] = useState(false);
|
||||||
|
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||||
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
|
|
||||||
|
const { data: orgs } = useGetOrganizations();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleOrgChange = async (orgId: string) => {
|
||||||
|
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
|
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||||
|
|
||||||
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMfaEnabled) {
|
||||||
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
|
toggleShowMfa.on();
|
||||||
|
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.invalidate();
|
||||||
|
await navigateUserToOrg(navigate, orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = useLogoutUser();
|
||||||
|
const logOutUser = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Logging out...");
|
||||||
|
await logout.mutateAsync();
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldShowMfa) {
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
|
<Mfa
|
||||||
|
email={user.email as string}
|
||||||
|
method={requiredMfaMethod}
|
||||||
|
successCallback={mfaSuccessCallback}
|
||||||
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dark z-10 flex h-12 items-center gap-1 border-b border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 px-4 py-2">
|
||||||
|
<div>
|
||||||
|
<Link to="/organization/secret-manager/overview">
|
||||||
|
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSlash}
|
||||||
|
className="rotate-90 font-medium text-bunker-300"
|
||||||
|
style={{ fontSize: "10px" }}
|
||||||
|
/>
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="flex cursor-pointer items-center gap-2 text-sm text-white">
|
||||||
|
<div className="max-w-32 overflow-hidden text-ellipsis">{currentOrg?.name}</div>
|
||||||
|
<div className="rounded border border-mineshaft-500 p-1 text-xs text-bunker-300">
|
||||||
|
{getPlan(subscription)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
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 capitalize text-mineshaft-400">organizations</div>
|
||||||
|
{orgs?.map((org) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
|
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}>
|
||||||
|
Log Out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<DropdownMenu modal={false} open={openSupport} onOpenChange={setOpenSupport}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => setOpenSupport(true)}
|
||||||
|
onMouseLeave={() => setOpenSupport(false)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faCircleQuestion} size="xl" className="text-white" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
side="bottom"
|
||||||
|
className="p-1"
|
||||||
|
onMouseEnter={() => setOpenSupport(true)}
|
||||||
|
onMouseLeave={() => setOpenSupport(false)}
|
||||||
|
>
|
||||||
|
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
|
||||||
|
if (url === "server-admins" && isInfisicalCloud()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem key={url as string}>
|
||||||
|
{url === "server-admins" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAdminsModal(true)}
|
||||||
|
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||||
|
>
|
||||||
|
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||||
|
{icon}
|
||||||
|
<div className="text-sm">{text}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={String(url)}
|
||||||
|
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||||
|
>
|
||||||
|
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||||
|
{icon}
|
||||||
|
<div className="text-sm">{text}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{envConfig.PLATFORM_VERSION && (
|
||||||
|
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||||
|
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||||
|
Version: {envConfig.PLATFORM_VERSION}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<DropdownMenu modal={false} open={openUser} onOpenChange={setOpenUser}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
onMouseEnter={() => setOpenUser(true)}
|
||||||
|
onMouseLeave={() => setOpenUser(false)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Lottie icon="user" className="w-14" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
className="p-1"
|
||||||
|
onMouseEnter={() => setOpenUser(true)}
|
||||||
|
onMouseLeave={() => setOpenUser(false)}
|
||||||
|
>
|
||||||
|
<div className="cursor-default px-1 py-1">
|
||||||
|
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
|
||||||
|
<div className="p-1 pr-3">
|
||||||
|
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow flex-col text-white">
|
||||||
|
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||||
|
{user?.firstName} {user?.lastName}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-mineshaft-300">{user.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link to="/personal-settings">
|
||||||
|
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Documentation
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/slack"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Join Slack Community
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
|
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||||
|
Log Out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Navbar } from "./Navbar";
|
||||||
+1
-1
@@ -93,7 +93,7 @@ export const INFISICAL_SUPPORT_OPTIONS = [
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MinimizedOrgSidebar = () => {
|
export const OrgSidebar = () => {
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBook,
|
||||||
|
faCheck,
|
||||||
|
faCheckCircle,
|
||||||
|
faCog,
|
||||||
|
faDoorClosed,
|
||||||
|
faEnvelope,
|
||||||
|
faInfinity,
|
||||||
|
faInfo,
|
||||||
|
faInfoCircle,
|
||||||
|
faMoneyBill,
|
||||||
|
faPlug,
|
||||||
|
faSignOut,
|
||||||
|
faUser,
|
||||||
|
faUserCog,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Mfa } from "@app/components/auth/Mfa";
|
||||||
|
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Menu,
|
||||||
|
MenuGroup,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { envConfig } from "@app/config/env";
|
||||||
|
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useGetOrganizations,
|
||||||
|
useGetOrgTrialUrl,
|
||||||
|
useLogoutUser,
|
||||||
|
useSelectOrganization,
|
||||||
|
workspaceKeys
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { authKeys } from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
|
import { SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||||
|
|
||||||
|
import { MenuIconButton } from "../MenuIconButton";
|
||||||
|
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||||
|
|
||||||
|
const getPlan = (subscription: SubscriptionPlan) => {
|
||||||
|
if (subscription.groups) return "Enterprise Plan";
|
||||||
|
if (subscription.pitRecovery) return "Pro Plan";
|
||||||
|
return "Free Plan";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||||
|
"Support Forum",
|
||||||
|
"https://infisical.com/slack"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||||
|
"Read Docs",
|
||||||
|
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||||
|
"GitHub Issues",
|
||||||
|
"https://github.com/Infisical/infisical/issues"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||||
|
"Email Support",
|
||||||
|
"mailto:[email protected]"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||||
|
"Instance Admins",
|
||||||
|
"server-admins"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OrgSidebar = () => {
|
||||||
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openSupport, setOpenSupport] = useState(false);
|
||||||
|
const [openUser, setOpenUser] = useState(false);
|
||||||
|
const [openOrg, setOpenOrg] = useState(false);
|
||||||
|
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: orgs } = useGetOrganizations();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||||
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const router = useRouter();
|
||||||
|
const location = useLocation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const isMoreSelected = (
|
||||||
|
[
|
||||||
|
linkOptions({ to: "/organization/access-management" }).to,
|
||||||
|
linkOptions({ to: "/organization/app-connections" }).to,
|
||||||
|
linkOptions({ to: "/organization/billing" }).to,
|
||||||
|
linkOptions({ to: "/organization/sso" }).to,
|
||||||
|
linkOptions({ to: "/organization/gateways" }).to,
|
||||||
|
linkOptions({ to: "/organization/settings" }).to,
|
||||||
|
linkOptions({ to: "/organization/audit-logs" }).to
|
||||||
|
] as string[]
|
||||||
|
).includes(location.pathname);
|
||||||
|
|
||||||
|
const handleOrgChange = async (orgId: string) => {
|
||||||
|
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
|
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||||
|
|
||||||
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMfaEnabled) {
|
||||||
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
|
toggleShowMfa.on();
|
||||||
|
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.invalidate();
|
||||||
|
await navigateUserToOrg(navigate, orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = useLogoutUser();
|
||||||
|
const logOutUser = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Logging out...");
|
||||||
|
await logout.mutateAsync();
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldShowMfa) {
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
|
<Mfa
|
||||||
|
email={user.email as string}
|
||||||
|
method={requiredMfaMethod}
|
||||||
|
successCallback={mfaSuccessCallback}
|
||||||
|
closeMfa={() => toggleShowMfa.off()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<aside className="dark z-10 w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150">
|
||||||
|
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
|
<Menu>
|
||||||
|
<MenuGroup title="Overview">
|
||||||
|
<Link to="/organization/secret-manager/overview">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="notification-bell">
|
||||||
|
Projects
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/secret-sharing">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||||
|
Share Secret
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/access-management">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faUsers} className="mr-4" />
|
||||||
|
Access Control
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/app-connections">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faPlug} className="mr-4" />
|
||||||
|
App Connections
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/gateways">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faDoorClosed} className="mr-4" />
|
||||||
|
Gateways
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/billing">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
|
||||||
|
Usage & Billing
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/audit-logs">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faBook} className="mr-4" />
|
||||||
|
Audit Logs
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/sso">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
|
||||||
|
SSO Settings
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/organization/settings">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faCog} className="mr-4" />
|
||||||
|
Organization Settings
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</MenuGroup>
|
||||||
|
<MenuGroup title="Admin Panels">
|
||||||
|
{user?.superAdmin && (
|
||||||
|
<Link to="/admin">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faUserCog} className="mr-4" />
|
||||||
|
Server Admin Console
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link to="/organization/admin">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1">
|
||||||
|
<FontAwesomeIcon icon={faCog} className="mr-4" />
|
||||||
|
Org Admin Console
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</MenuGroup>
|
||||||
|
</Menu>
|
||||||
|
<div
|
||||||
|
className={`relative mt-10 ${
|
||||||
|
subscription && subscription.slug === "starter" && !subscription.has_used_trial
|
||||||
|
? "mb-2"
|
||||||
|
: "mb-4"
|
||||||
|
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
||||||
|
>
|
||||||
|
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||||
|
<Tooltip content="Start Free Pro Trial" side="right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
|
// direct user to start pro trial
|
||||||
|
const url = await mutateAsync({
|
||||||
|
orgId: currentOrg.id,
|
||||||
|
success_url: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}}
|
||||||
|
className="mt-1.5 w-full"
|
||||||
|
>
|
||||||
|
<div className="justify-left mb-1.5 mt-1.5 flex w-full flex-col items-center rounded-md p-1 text-xs text-mineshaft-300 transition-all duration-150 hover:bg-mineshaft-500 hover:text-primary-400">
|
||||||
|
<FontAwesomeIcon icon={faInfinity} className="py-2 text-lg text-primary" />
|
||||||
|
Pro Trial
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||||
|
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||||
|
<div className="mb-2">
|
||||||
|
<ServerAdminsPanel />
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<CreateOrgModal
|
||||||
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgSidebar } from "./OrgSidebar";
|
||||||
Reference in New Issue
Block a user