feat: completed the product sidebar

This commit is contained in:
=
2025-07-02 01:16:48 +05:30
parent 09cdd5ec91
commit c66510f473
17 changed files with 749 additions and 435 deletions
@@ -133,6 +133,15 @@ export const OrgSidebar = ({ isHidden }: Props) => {
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Share Secret
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Admin Panels">
{user?.superAdmin && (
<Link to="/admin">
@@ -157,15 +166,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Share Secret
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div
className={`relative mt-10 ${
@@ -1,396 +1,314 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { faDotCircle, faHome, faMobile, faWindowMaximize } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
BreadcrumbContainer,
Divider,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Lottie,
Menu,
MenuGroup,
MenuItem,
TBreadcrumbFormat
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { useProjectPermission, useWorkspace } from "@app/context";
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
import { ProjectSelect } from "./components/ProjectSelect";
import { useLocalStorageState } from "@app/hooks";
import { ShouldWrap } from "@app/components/utilities/ShouldWrapComponent";
enum SidebarStyle {
Expanded = "expanded",
Collapsed = "collapsed",
ExpandOnHover = "expand-on-hover"
}
const MIN_SIDEBAR_SIZE = "55px";
const MAX_SIDEBAR_SIZE = "220px";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const [sidebarStyle, setSidebarStyle] = useLocalStorageState(
"project-sidebar-style",
SidebarStyle.ExpandOnHover
);
const { t } = useTranslation();
const { assumedPrivilegeDetails } = useProjectPermission();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { subscription } = useSubscription();
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
const isCmek = currentWorkspace?.type === ProjectType.KMS;
const isSSH = currentWorkspace?.type === ProjectType.SSH;
const isSecretScanning = currentWorkspace?.type === ProjectType.SecretScanning;
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId,
options: { enabled: isSecretManager }
});
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
projectSlug,
options: { enabled: isSecretManager }
});
// we only show the secret rotations v1 tab if they have existing rotations
const { data: secretRotations } = useGetSecretRotations({
workspaceId,
options: {
enabled: isSecretManager && Boolean(subscription.secretRotation),
refetchOnMount: false
}
});
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
enabled:
isSecretScanning &&
subscription.secretScanning &&
permission.can(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
),
refetchInterval: 30000
});
const minSidebarWidth =
sidebarStyle === SidebarStyle.Expanded ? MAX_SIDEBAR_SIZE : MIN_SIDEBAR_SIZE;
const maxSidebarWidth =
sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE;
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<div
className="dark hidden w-full flex-col overflow-x-hidden md:flex"
style={{ height: "calc(100vh - 3rem)" }}
>
<div className="flex flex-grow flex-col overflow-y-auto overflow-x-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
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"
style={{ width: minSidebarWidth }}
whileHover={{
width: maxSidebarWidth
}}
className="dark group w-full overflow-hidden 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>
<ProjectSelect />
<div className="px-1">
<Menu>
<MenuGroup title="Main Menu">
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
{t("nav.menu.secrets")}
</MenuItem>
)}
</Link>
)}
{isCertManager && (
<>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/subscribers` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="pki-subscriber">
Subscribers
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificate-templates` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
iconMode="reverse"
isSelected={isActive}
icon="pki-template"
>
Certificate Templates
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificates` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate">
Certificates
</MenuItem>
)}
</Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate-authority">
Certificate Authorities
</MenuItem>
)}
</Link>
<Link
to={`/${ProjectType.CertificateManager}/$projectId/alerting` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="notification-bell">
Alerting
</MenuItem>
)}
</Link>
</>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="key-user" iconMode="reverse">
KMIP
</MenuItem>
)}
</Link>
)}
{isSSH && (
<>
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="server">
Hosts
</MenuItem>
)}
</Link>
{/* <Link
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
Certificates
</MenuItem>
)}
</Link> */}
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to={`/${ProjectType.SSH}/$projectId/cas` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="certificate-authority"
iconMode="reverse"
>
Certificate Authorities
</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
</>
)}
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/data-sources` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="blocks">
Data Sources
</MenuItem>
)}
</Link>
)}
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/findings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="search">
<div className="flex w-full items-center justify-between">
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && Boolean(secretRotations?.length) && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</MenuItem>
)}
</Link>
)}
</MenuGroup>
<MenuGroup title="Other">
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
<nav className="items-between flex h-full flex-col justify-between">
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Secret Manager"
position="right"
>
<Link
to="/projects/$projectId/secret-manager/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie
className="inline-block h-6 w-6 shrink-0"
icon="sliding-carousel"
/>
}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
Secret Manager
</MenuItem>
)}
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="PKI Manager"
position="right"
>
<Link
to="/projects/$projectId/cert-manager/subscribers"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="note" />}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
</Link>
</MenuGroup>
</Menu>
</div>
</div>
PKI Manager
</MenuItem>
)}
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="KMS"
position="right"
>
<Link
to="/projects/$projectId/kms/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="unlock" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
KMS
</MenuItem>
)}
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="SSH"
position="right"
>
<Link
to="/projects/$projectId/ssh/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="verified" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
SSH
</MenuItem>
)}
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Secret Scanning"
position="right"
>
<Link
to="/projects/$projectId/secret-scanning/findings"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="secret-scan" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Secret Scanning
</MenuItem>
)}
</Link>
</ShouldWrap>
</Menu>
<Divider />
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Project Settings"
position="right"
>
<Link
to="/projects/$projectId/secret-manager/overview"
params={{ projectId: currentWorkspace.id }}
>
{({ isActive }) => (
<MenuItem
className="relative flex items-center gap-2 overflow-hidden"
isSelected={isActive}
leftIcon={
<Lottie className="inline-block h-6 w-6 shrink-0" icon="settings-cog" />
}
>
{isActive && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Project Settings
</MenuItem>
)}
</Link>
</ShouldWrap>
</Menu>
<div className="flex-grow" />
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Organization Home"
position="right"
>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
}
>
Organization
</MenuItem>
</Link>
</ShouldWrap>
<DropdownMenu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Sidebar Control"
position="right"
>
<DropdownMenuTrigger className="w-full">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faWindowMaximize}
flip="vertical"
/>
}
>
Sidebar Control
</MenuItem>
</DropdownMenuTrigger>
</ShouldWrap>
<DropdownMenuContent>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.Expanded && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.Expanded)}
>
Expanded
</DropdownMenuItem>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.Collapsed && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.Collapsed)}
>
Collapsed
</DropdownMenuItem>
<DropdownMenuItem
iconPos="right"
icon={
sidebarStyle === SidebarStyle.ExpandOnHover && (
<FontAwesomeIcon icon={faDotCircle} size="sm" />
)
}
onClick={() => setSidebarStyle(SidebarStyle.ExpandOnHover)}
>
Expand on hover
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Menu>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 py-4 dark:[color-scheme:dark]">
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
@@ -0,0 +1,121 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div className="px-1">
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-8g0 hover:bg-mineshaft-500",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${isSelected ? "opacity-100" : "opacity-0"} absolute left-0 h-full w-0.5 rounded-bl rounded-tl bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
</div>
);
};
// export const MenuIconButton = <T extends ElementType = "button">({
// children,
// icon,
// className,
// isDisabled,
// isSelected,
// as: Item = "div",
// description,
// // wrapping in forward ref with generic component causes the loss of ts definitions on props
// inputRef,
// lottieIconMode = "forward",
// ...props
// }: MenuItemProps<T> &
// ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
// const iconRef = useRef<DotLottie | null>(null);
// return (
// <div className="px-1">
// <Item
// type="button"
// role="menuitem"
// className={twMerge(
// "group relative my-1 flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
// isSelected && "bg-mineshaft-600 hover:bg-mineshaft-700",
// isDisabled && "cursor-not-allowed hover:bg-transparent",
// className
// )}
// onMouseEnter={() => iconRef.current?.play()}
// onMouseLeave={() => iconRef.current?.stop()}
// ref={inputRef}
// {...props}
// >
// <div
// className={`${"opacity-0"} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
// />
// {icon && (
// <div className="my-auto mb-2 h-6 w-6">
// <DotLottieReact
// dotLottieRefCallback={(el) => {
// iconRef.current = el;
// }}
// src={`/lotties/${icon}.json`}
// loop
// className="h-full w-full"
// mode={lottieIconMode}
// />
// </div>
// )}
// <div
// className="flex-grow justify-center break-words text-center"
// style={{ fontSize: "10px" }}
// >
// {children}
// </div>
// </Item>
// </div>
// );
// };
@@ -0,0 +1,65 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div>
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "rounded-none bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute left-0 h-full w-0.5 bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
</div>
);
};
@@ -0,0 +1 @@
export { MenuIconButton } from "./MenuIconButton";
@@ -0,0 +1,137 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
// This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed.
export const SecretManagerLayout = () => {
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId
});
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({
projectSlug
});
// we only show the secret rotations v1 tab if they have existing rotations
const { data: secretRotations } = useGetSecretRotations({
workspaceId,
options: {
refetchOnMount: false
}
});
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
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 overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 pb-2 pt-3 text-lg text-white">
Secret Manager
</div>
<div className="mt-2 flex-grow">
<Menu>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.secrets")}</MenuItem>
)}
</Link>
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.integrations")}</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Secret Rotation</MenuItem>}
</Link>
)}
<Link
to={`/projects/$projectId/${ProjectType.SecretManager}/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</MenuItem>
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>{t("nav.menu.project-settings")}</MenuItem>
)}
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 dark:[color-scheme:dark]">
<Outlet />
</div>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
</p>
</div>
</>
);
};
@@ -0,0 +1 @@
export { SecretManagerLayout } from "./SecretManagerLayout";