mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 15:31:19 +00:00
feat: completed all nav restructing and breadcrumbs cleanup
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, Outlet, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbContainer,
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
@@ -30,15 +25,10 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
import { MenuIconButton } from "./components/MenuIconButton";
|
||||
import { SidebarFooter } from "./components/SidebarFooter";
|
||||
import { SidebarHeader } from "./components/SidebarHeader";
|
||||
|
||||
type Props = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const OrganizationLayout = ({ children }: Props) => {
|
||||
export const OrganizationLayout = () => {
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
@@ -53,9 +43,6 @@ export const OrganizationLayout = ({ children }: Props) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const isNestedLayout = Boolean(children);
|
||||
const [isCollapsed, setIsCollapsed] = useToggle(isNestedLayout);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleOrgChange = async (orgId: string) => {
|
||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
@@ -96,168 +83,108 @@ export const OrganizationLayout = ({ children }: Props) => {
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden transition-all md:flex">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside
|
||||
className={twMerge(
|
||||
"dark w-60 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150",
|
||||
isCollapsed && "w-16"
|
||||
)}
|
||||
>
|
||||
<aside className="dark 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]">
|
||||
<div>
|
||||
<SidebarHeader onChangeOrg={handleOrgChange} isCollapsed={isCollapsed} />
|
||||
<SidebarHeader onChangeOrg={handleOrgChange} />
|
||||
<div className="px-1">
|
||||
<AnimatePresence mode="wait">
|
||||
{isCollapsed ? (
|
||||
<motion.div
|
||||
key="menu-icons"
|
||||
className="space-y-1"
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -300, opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<motion.div
|
||||
key="menu-list-items"
|
||||
initial={{ x: 300, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -300, opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<Menu>
|
||||
<MenuGroup title="PRODUCTS">
|
||||
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(
|
||||
`/${ProjectType.SecretManager}`
|
||||
)}
|
||||
icon="sliding-carousel"
|
||||
>
|
||||
Secret Manager
|
||||
</MenuIconButton>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="sliding-carousel">
|
||||
Secret Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={`/organization/${ProjectType.CertificateManager}/overview` as const}
|
||||
>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(
|
||||
`/${ProjectType.CertificateManager}`
|
||||
)}
|
||||
icon="note"
|
||||
>
|
||||
Cert Manager
|
||||
</MenuIconButton>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="note">
|
||||
Cert Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to={`/organization/${ProjectType.KMS}/overview` as const}>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(`/${ProjectType.KMS}`)}
|
||||
icon="unlock"
|
||||
>
|
||||
KMS
|
||||
</MenuIconButton>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="unlock">
|
||||
Key Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(`/${ProjectType.SSH}`)}
|
||||
icon="verified"
|
||||
>
|
||||
SSH
|
||||
</MenuIconButton>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="verified">
|
||||
SSH
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="menu-list-items"
|
||||
initial={{ x: 300, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -300, opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<Menu>
|
||||
<MenuGroup title="PRODUCTS">
|
||||
<Link
|
||||
to={`/organization/${ProjectType.SecretManager}/overview` as const}
|
||||
<Link to="/organization/secret-scanning">
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
isSelected={isActive}
|
||||
icon="secret-scan"
|
||||
className="text-white"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="sliding-carousel">
|
||||
Secret Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
`/organization/${ProjectType.CertificateManager}/overview` as const
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="note">
|
||||
Cert Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to={`/organization/${ProjectType.KMS}/overview` as const}>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="unlock">
|
||||
Key Management
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="verified">
|
||||
SSH
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/secret-scanning">
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
isSelected={isActive}
|
||||
icon="secret-scan"
|
||||
className="text-white"
|
||||
>
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="groups">
|
||||
Access Control
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/secret-sharing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||
Secret Sharing
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="spinning-coin">
|
||||
Usage & Billing
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="groups">
|
||||
Access Control
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/secret-sharing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||
Secret Sharing
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="spinning-coin">
|
||||
Usage & Billing
|
||||
</MenuItem>
|
||||
)}
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="moving-block">
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Organization Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="moving-block">
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
Organization Settings
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarFooter isCollapsed={isCollapsed} onToggleSidebar={setIsCollapsed.toggle} />
|
||||
<SidebarFooter />
|
||||
</nav>
|
||||
</aside>
|
||||
<CreateOrgModal
|
||||
@@ -265,34 +192,10 @@ export const OrganizationLayout = ({ children }: Props) => {
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
|
||||
{breadcrumbs && (
|
||||
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{breadcrumbs.map((el, index) => {
|
||||
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
|
||||
return (
|
||||
<>
|
||||
{el.link && isNotLastCrumb && !("disabled" in el.link) ? (
|
||||
<Link {...el.link}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>{el.label}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Link>
|
||||
) : (
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{el.label}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{isNotLastCrumb && <BreadcrumbSeparator />}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
)}
|
||||
{children || <Outlet />}
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+30
-71
@@ -1,13 +1,10 @@
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faAnglesLeft,
|
||||
faAnglesRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faEnvelope,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faUser
|
||||
@@ -20,15 +17,12 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
MenuItem
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
|
||||
|
||||
import { MenuIconButton } from "../MenuIconButton";
|
||||
|
||||
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
@@ -52,12 +46,7 @@ export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
]
|
||||
];
|
||||
|
||||
type Props = {
|
||||
isCollapsed?: boolean;
|
||||
onToggleSidebar: () => void;
|
||||
};
|
||||
|
||||
export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
|
||||
export const SidebarFooter = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
@@ -82,38 +71,28 @@ export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
|
||||
subscription && subscription.slug === "starter" && !subscription.has_used_trial
|
||||
? "mb-2"
|
||||
: "mb-4"
|
||||
} flex w-full cursor-default flex-col items-center ${isCollapsed ? "px-1" : "px-2"} text-sm text-mineshaft-400`}
|
||||
} flex w-full cursor-default flex-col items-center px-2 text-sm text-mineshaft-400`}
|
||||
>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) &&
|
||||
!isCollapsed && <WishForm />}
|
||||
{!isCollapsed && (
|
||||
<Link
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
action: "invite"
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="mb-3 w-full pl-2 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||
Invite people
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
<Link
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
action: "invite"
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="mb-3 w-full pl-2 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||
Invite people
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
{isCollapsed ? (
|
||||
<MenuIconButton>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
|
||||
Support
|
||||
</MenuIconButton>
|
||||
) : (
|
||||
<div className="mb-4 flex w-full items-center pl-2 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex w-full items-center pl-2 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
@@ -161,39 +140,19 @@ export const SidebarFooter = ({ isCollapsed, onToggleSidebar }: Props) => {
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{isCollapsed ? (
|
||||
<MenuIconButton onClick={onToggleSidebar} className="p-4">
|
||||
<FontAwesomeIcon icon={faAnglesRight} className="text-lg" />
|
||||
</MenuIconButton>
|
||||
) : (
|
||||
<MenuItem onClick={onToggleSidebar} className="mb-2 w-full px-2 text-mineshaft-400">
|
||||
<FontAwesomeIcon icon={faAnglesLeft} className="mr-3" />
|
||||
Collapse
|
||||
</MenuItem>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
{isCollapsed ? (
|
||||
<DropdownMenuTrigger asChild className="w-full">
|
||||
<div className="flex w-full cursor-pointer items-center rounded-md border border-mineshaft-600 p-2 px-1">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="max-w-40 flex-grow truncate text-ellipsis text-left text-sm capitalize text-white">
|
||||
{user?.firstName} {user?.lastName}
|
||||
</div>
|
||||
<div>
|
||||
<MenuIconButton>
|
||||
<div className="my-1 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase text-black">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
</MenuIconButton>
|
||||
<FontAwesomeIcon icon={faUser} className="text-xs text-mineshaft-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full cursor-pointer items-center rounded-md border border-mineshaft-600 p-2 px-1">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="max-w-40 flex-grow truncate text-ellipsis text-left text-sm capitalize text-white">
|
||||
{user?.firstName} {user?.lastName}
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUser} className="text-xs text-mineshaft-400" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
|
||||
|
||||
+16
-164
@@ -1,4 +1,4 @@
|
||||
import { faCheck, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faSignOut, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Prop = {
|
||||
onChangeOrg: (orgId: string) => void;
|
||||
isCollapsed?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => {
|
||||
export const SidebarHeader = ({ onChangeOrg }: Prop) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const navigate = useNavigate();
|
||||
const { data: orgs } = useGetOrganizations();
|
||||
@@ -36,38 +34,20 @@ export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex cursor-pointer items-center p-2 pt-4",
|
||||
isCollapsed && "transition-all duration-150 hover:bg-mineshaft-700"
|
||||
)}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center p-2 pt-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all",
|
||||
isCollapsed ? "border-none" : "transition-all duration-150 hover:bg-mineshaft-700"
|
||||
)}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
|
||||
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
{currentOrg?.name}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
{currentOrg?.name}
|
||||
</div>
|
||||
<div className="text-xs text-mineshaft-400">Free Plan</div>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-mineshaft-400" />
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-mineshaft-400">Free Plan</div>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-mineshaft-400" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
@@ -113,139 +93,11 @@ export const SidebarHeader = ({ onChangeOrg, isCollapsed }: Prop) => {
|
||||
);
|
||||
})}
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<button type="button" onClick={logOutUser} className="w-full">
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
</button>
|
||||
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600">
|
||||
// <div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
||||
// <div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
|
||||
// {currentOrg?.name.charAt(0)}
|
||||
// </div>
|
||||
// <div
|
||||
// className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
|
||||
// style={{ maxWidth: "140px" }}
|
||||
// >
|
||||
// {currentOrg?.name}
|
||||
// </div>
|
||||
// <FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
|
||||
// </div>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent align="start" className="p-1">
|
||||
// <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</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;
|
||||
// }
|
||||
//
|
||||
// onChangeOrg(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" />
|
||||
// <button type="button" onClick={logOutUser} className="w-full">
|
||||
// <DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
// </button>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger
|
||||
// asChild
|
||||
// className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
|
||||
// >
|
||||
// <div
|
||||
// className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
|
||||
// style={{ fontSize: "11px", width: "26px", height: "26px" }}
|
||||
// >
|
||||
// {user?.firstName?.charAt(0)}
|
||||
// {user?.lastName && user?.lastName?.charAt(0)}
|
||||
// </div>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent align="start" className="p-1">
|
||||
// <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</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>
|
||||
// {user?.superAdmin && (
|
||||
// <Link to="/admin">
|
||||
// <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
// Server Admin Console
|
||||
// </DropdownMenuItem>
|
||||
// </Link>
|
||||
// )}
|
||||
// <Link to="/organization/admin">
|
||||
// <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
// Organization Admin Console
|
||||
// </DropdownMenuItem>
|
||||
// </Link>
|
||||
// <div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
// <button type="button" onClick={logOutUser} className="w-full">
|
||||
// <DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
// </button>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
|
||||
@@ -4,21 +4,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbContainer,
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
||||
import { ProjectSelect } from "./components/ProjectSelect";
|
||||
|
||||
// This is a generic layout shared by all types of projects.
|
||||
@@ -54,6 +51,7 @@ export const ProjectLayout = () => {
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<MinimizedOrgSidebar />
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
@@ -199,34 +197,10 @@ export const ProjectLayout = () => {
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
{breadcrumbs && (
|
||||
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{breadcrumbs.map((el, index) => {
|
||||
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
|
||||
return (
|
||||
<>
|
||||
{el.link && isNotLastCrumb && !("disabled" in el.link) ? (
|
||||
<Link {...el.link}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>{el.label}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Link>
|
||||
) : (
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{el.label}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{isNotLastCrumb && <BreadcrumbSeparator />}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
)}
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { ComponentPropsWithRef, ElementType, useRef } from "react";
|
||||
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { MenuItemProps } from "@app/components/v2";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const MenuIconButton = <T extends ElementType = "button">({
|
||||
children,
|
||||
@@ -10,7 +10,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "button",
|
||||
as: Item = "div",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
@@ -35,7 +35,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
||||
<div
|
||||
className={`${
|
||||
isSelected ? "visisble" : "invisible"
|
||||
} absolute -left-[0.28rem] h-full w-[0.07rem] rounded-md bg-primary`}
|
||||
} absolute -left-[0.28rem] h-full w-0.5 rounded-md bg-primary`}
|
||||
/>
|
||||
{icon && (
|
||||
<div className="my-auto mb-2 h-6 w-6">
|
||||
@@ -49,7 +49,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="flex-grow break-words text-center text-xxs">{children}</span>
|
||||
<div className="flex-grow justify-center break-words text-center text-xxs">{children}</div>
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faCheck,
|
||||
faCog,
|
||||
faEllipsis,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faMoneyBill,
|
||||
faReply,
|
||||
faShare,
|
||||
faSignOut,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
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
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
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 { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "@app/layouts/OrganizationLayout/components/SidebarFooter/SidebarFooter";
|
||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||
|
||||
import { MenuIconButton } from "../MenuIconButton";
|
||||
import { ProjectSwitcher } from "./ProjectSwitcher";
|
||||
|
||||
export const MinimizedOrgSidebar = () => {
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
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 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 (
|
||||
<>
|
||||
<aside className="dark w-16 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]">
|
||||
<div>
|
||||
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<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" />
|
||||
<Link to="/organization/secret-manager/overview">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faReply} />}>
|
||||
Home
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faSignOut} />}
|
||||
onClick={logOutUser}
|
||||
>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<motion.div
|
||||
key="menu-icons"
|
||||
className="space-y-1"
|
||||
initial={{ x: 300, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -300, opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<DropdownMenu modal>
|
||||
<DropdownMenuTrigger>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(
|
||||
`/${ProjectType.SecretManager}`
|
||||
)}
|
||||
icon="sliding-carousel"
|
||||
>
|
||||
Secret Manager
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
className="px-3 pb-2"
|
||||
style={{ minWidth: "320px" }}
|
||||
>
|
||||
<ProjectSwitcher type={ProjectType.SecretManager} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu modal>
|
||||
<DropdownMenuTrigger>
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(
|
||||
`/${ProjectType.CertificateManager}`
|
||||
)}
|
||||
icon="note"
|
||||
>
|
||||
Cert Manager
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
className="px-3 pb-2"
|
||||
style={{ minWidth: "320px" }}
|
||||
>
|
||||
<ProjectSwitcher type={ProjectType.CertificateManager} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu modal>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(`/${ProjectType.KMS}`)}
|
||||
icon="unlock"
|
||||
>
|
||||
KMS
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
className="px-3 pb-2"
|
||||
style={{ minWidth: "320px" }}
|
||||
>
|
||||
<ProjectSwitcher type={ProjectType.KMS} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu modal>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
<MenuIconButton
|
||||
isSelected={window.location.pathname.startsWith(`/${ProjectType.SSH}`)}
|
||||
icon="verified"
|
||||
>
|
||||
SSH
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
className="px-3 pb-2"
|
||||
style={{ minWidth: "320px" }}
|
||||
>
|
||||
<ProjectSwitcher type={ProjectType.SSH} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="w-full">
|
||||
<MenuIconButton>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<FontAwesomeIcon icon={faEllipsis} className="mb-3 text-lg" />
|
||||
<span>More</span>
|
||||
</div>
|
||||
</MenuIconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" side="right" className="p-1">
|
||||
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
|
||||
<Link to="/organization/access-management">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUsers} />}>
|
||||
Access Control
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/secret-sharing">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faShare} />}>
|
||||
Secret Sharing
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
<Link to="/organization/billing">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faMoneyBill} />}>
|
||||
Usage & Billing
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/organization/audit-logs">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faBook} />}>
|
||||
Audit Logs
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faCog} />}>
|
||||
Organization Settings
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<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`}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
<MenuIconButton>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
|
||||
Support
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<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>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<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 items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
|
||||
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
|
||||
Start Free Pro Trial
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full" asChild>
|
||||
<div>
|
||||
<MenuIconButton>
|
||||
<div className="my-1 flex h-6 w-6 items-center justify-center rounded-md bg-primary text-sm uppercase text-black">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
</MenuIconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</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>
|
||||
{user?.superAdmin && (
|
||||
<Link to="/admin">
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Server Admin Console
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/organization/admin">
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Organization Admin Console
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<CreateOrgModal
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
import { faExternalLink, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { DropdownMenuItem, EmptyState, Input } from "@app/components/v2";
|
||||
import { getProjectTitle } from "@app/helpers/project";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type Props = {
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
export const ProjectSwitcher = ({ type }: Props) => {
|
||||
const { data: workspaces, isPending } = useGetUserWorkspaces({ type });
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredWorkspaces = workspaces?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link to={`/organization/${type}/overview` as const}>
|
||||
<div className="py-2 text-xs capitalize text-bunker-300">
|
||||
{getProjectTitle(type)} projects
|
||||
<FontAwesomeIcon icon={faExternalLink} size="xs" className="ml-1" />
|
||||
</div>
|
||||
</Link>
|
||||
<div className="w-full pb-2">
|
||||
<Input
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
value={search}
|
||||
onChange={(evt) => setSearch(evt.target.value)}
|
||||
size="xs"
|
||||
placeholder="Search by name"
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
<div className="thin-scrollbar max-h-64 overflow-auto">
|
||||
{filteredWorkspaces?.map((el) => (
|
||||
<Link
|
||||
to={`/${type}/$projectId/overview` as const}
|
||||
params={{ projectId: el.id }}
|
||||
key={el.id}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<span className="capitalize">{el.name}</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
))}
|
||||
{!isPending && !filteredWorkspaces?.length && (
|
||||
<EmptyState title="No project found" iconSize="1x" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
|
||||
Reference in New Issue
Block a user