mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
feat: completed the product sidebar
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { ReactNode, ComponentType } from "react";
|
||||||
|
|
||||||
|
type ShouldWrapProps<T extends Record<string, any>> = {
|
||||||
|
children: ReactNode;
|
||||||
|
wrapper: ComponentType<T & { children: ReactNode }>;
|
||||||
|
isWrapped?: boolean;
|
||||||
|
} & T;
|
||||||
|
|
||||||
|
export const ShouldWrap = <T extends Record<string, any>>({
|
||||||
|
children,
|
||||||
|
wrapper: Wrapper,
|
||||||
|
isWrapped = false,
|
||||||
|
...wrapperProps
|
||||||
|
}: ShouldWrapProps<T>) => {
|
||||||
|
if (isWrapped) {
|
||||||
|
return <Wrapper {...(wrapperProps as any)}>{children}</Wrapper>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ interface IProps {
|
|||||||
export const Divider = ({ className }: IProps): JSX.Element => {
|
export const Divider = ({ className }: IProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
||||||
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
|
<div aria-hidden="true" className="h-1 w-full grow border-t border-mineshaft-300" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, useRef } from "react";
|
import { forwardRef, ReactNode, useRef } from "react";
|
||||||
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
||||||
|
|
||||||
export type LottieProps = {
|
export type LottieProps = {
|
||||||
@@ -9,24 +9,27 @@ export type LottieProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Lottie = ({ children, icon, iconMode, ...props }: LottieProps): JSX.Element => {
|
export const Lottie = forwardRef<HTMLDivElement, LottieProps>(
|
||||||
const iconRef = useRef<DotLottie | null>(null);
|
({ children, icon, iconMode, ...props }, ref): JSX.Element => {
|
||||||
return (
|
const iconRef = useRef<DotLottie | null>(null);
|
||||||
<div
|
return (
|
||||||
onMouseEnter={() => iconRef.current?.play()}
|
<div
|
||||||
onMouseLeave={() => iconRef.current?.stop()}
|
onMouseEnter={() => iconRef.current?.play()}
|
||||||
{...props}
|
onMouseLeave={() => iconRef.current?.stop()}
|
||||||
>
|
{...props}
|
||||||
<DotLottieReact
|
ref={ref}
|
||||||
dotLottieRefCallback={(el) => {
|
>
|
||||||
iconRef.current = el;
|
<DotLottieReact
|
||||||
}}
|
dotLottieRefCallback={(el) => {
|
||||||
mode={iconMode}
|
iconRef.current = el;
|
||||||
src={`/lotties/${icon}.json`}
|
}}
|
||||||
loop
|
mode={iconMode}
|
||||||
className="h-full w-full"
|
src={`/lotties/${icon}.json`}
|
||||||
/>
|
loop
|
||||||
{children}
|
className="h-full w-full"
|
||||||
</div>
|
/>
|
||||||
);
|
{children}
|
||||||
};
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
|
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from "react";
|
||||||
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export type MenuProps = {
|
export type MenuProps = {
|
||||||
@@ -14,9 +13,8 @@ export const Menu = ({ children, className }: MenuProps): JSX.Element => {
|
|||||||
export type MenuItemProps<T extends ElementType> = {
|
export type MenuItemProps<T extends ElementType> = {
|
||||||
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
|
||||||
as?: T;
|
as?: T;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
icon?: string;
|
leftIcon?: ReactNode;
|
||||||
iconMode?: Mode;
|
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
@@ -26,7 +24,7 @@ export type MenuItemProps<T extends ElementType> = {
|
|||||||
|
|
||||||
export const MenuItem = <T extends ElementType = "button">({
|
export const MenuItem = <T extends ElementType = "button">({
|
||||||
children,
|
children,
|
||||||
icon,
|
leftIcon,
|
||||||
iconMode,
|
iconMode,
|
||||||
className,
|
className,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -37,41 +35,21 @@ export const MenuItem = <T extends ElementType = "button">({
|
|||||||
inputRef,
|
inputRef,
|
||||||
...props
|
...props
|
||||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||||
const iconRef = useRef<DotLottie | null>(null);
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
|
"duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-2 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
|
||||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onMouseEnter={() => iconRef.current?.play()}
|
|
||||||
onMouseLeave={() => iconRef.current?.stop()}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
{leftIcon}
|
||||||
className={`${
|
{children && <span className="flex-grow whitespace-nowrap text-left">{children}</span>}
|
||||||
isSelected ? "visisble" : "invisible"
|
|
||||||
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
|
|
||||||
/>
|
|
||||||
{icon && (
|
|
||||||
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
|
|
||||||
<DotLottieReact
|
|
||||||
dotLottieRefCallback={(el) => {
|
|
||||||
iconRef.current = el;
|
|
||||||
}}
|
|
||||||
mode={iconMode}
|
|
||||||
src={`/lotties/${icon}.json`}
|
|
||||||
loop
|
|
||||||
className="h-full w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="flex-grow text-left">{children}</span>
|
|
||||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||||
</Item>
|
</Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from "./Card";
|
|||||||
export * from "./Checkbox";
|
export * from "./Checkbox";
|
||||||
export * from "./ConfirmActionModal";
|
export * from "./ConfirmActionModal";
|
||||||
export * from "./ContentLoader";
|
export * from "./ContentLoader";
|
||||||
|
export * from "./Divider";
|
||||||
export * from "./DatePicker";
|
export * from "./DatePicker";
|
||||||
export * from "./DeleteActionModal";
|
export * from "./DeleteActionModal";
|
||||||
export * from "./Drawer";
|
export * from "./Drawer";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { useDebounce } from "./useDebounce";
|
export { useDebounce } from "./useDebounce";
|
||||||
export * from "./useGetProjectTypeFromRoute";
|
export * from "./useGetProjectTypeFromRoute";
|
||||||
|
export { useLocalStorageState } from "./useLocalStorageState";
|
||||||
export { usePagination } from "./usePagination";
|
export { usePagination } from "./usePagination";
|
||||||
export { usePersistentState } from "./usePersistentState";
|
export { usePersistentState } from "./usePersistentState";
|
||||||
export { usePopUp } from "./usePopUp";
|
export { usePopUp } from "./usePopUp";
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
type SetStateAction<T> = T | ((prevState: T) => T);
|
||||||
|
|
||||||
|
const dispatchStorageEvent = (key: string, newValue: string | null): void => {
|
||||||
|
window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocalStorageItem = (key: string, value: unknown): void => {
|
||||||
|
const stringifiedValue = JSON.stringify(value);
|
||||||
|
window.localStorage.setItem(key, stringifiedValue);
|
||||||
|
dispatchStorageEvent(key, stringifiedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLocalStorageItem = (key: string): void => {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
dispatchStorageEvent(key, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalStorageItem = (key: string): string | null => {
|
||||||
|
return window.localStorage.getItem(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLocalStorageSubscribe = (callback: (e: StorageEvent) => void) => {
|
||||||
|
window.addEventListener("storage", callback);
|
||||||
|
return () => window.removeEventListener("storage", callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalStorageServerSnapshot = (): never => {
|
||||||
|
throw Error("useLocalStorage is a client-only hook");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocalStorageState = <T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T
|
||||||
|
): [T, (value: SetStateAction<T>) => void] => {
|
||||||
|
const getSnapshot = () => getLocalStorageItem(key);
|
||||||
|
|
||||||
|
const store = useSyncExternalStore(
|
||||||
|
useLocalStorageSubscribe,
|
||||||
|
getSnapshot,
|
||||||
|
getLocalStorageServerSnapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = useCallback(
|
||||||
|
(v: SetStateAction<T>): void => {
|
||||||
|
try {
|
||||||
|
const nextState =
|
||||||
|
typeof v === "function" ? (v as (prevState: T) => T)(JSON.parse(store || "null")) : v;
|
||||||
|
|
||||||
|
if (nextState === undefined || nextState === null) {
|
||||||
|
removeLocalStorageItem(key);
|
||||||
|
} else {
|
||||||
|
setLocalStorageItem(key, nextState);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[key, store]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
|
||||||
|
setLocalStorageItem(key, initialValue);
|
||||||
|
}
|
||||||
|
}, [key, initialValue]);
|
||||||
|
|
||||||
|
return [store ? JSON.parse(store) : initialValue, setState];
|
||||||
|
};
|
||||||
@@ -133,6 +133,15 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</MenuGroup>
|
</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">
|
<MenuGroup title="Admin Panels">
|
||||||
{user?.superAdmin && (
|
{user?.superAdmin && (
|
||||||
<Link to="/admin">
|
<Link to="/admin">
|
||||||
@@ -157,15 +166,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
<MenuGroup title="Others">
|
|
||||||
<Link to="/organization/secret-sharing">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
|
||||||
Share Secret
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</MenuGroup>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
<div
|
<div
|
||||||
className={`relative mt-10 ${
|
className={`relative mt-10 ${
|
||||||
|
|||||||
@@ -1,396 +1,314 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link, Outlet } from "@tanstack/react-router";
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Divider,
|
||||||
BreadcrumbContainer,
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Lottie,
|
||||||
Menu,
|
Menu,
|
||||||
MenuGroup,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TBreadcrumbFormat
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import { useProjectPermission, useWorkspace } from "@app/context";
|
||||||
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 { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
|
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.
|
// This is a generic layout shared by all types of projects.
|
||||||
// If the product layout differs significantly, create a new layout as needed.
|
// If the product layout differs significantly, create a new layout as needed.
|
||||||
export const ProjectLayout = () => {
|
export const ProjectLayout = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const [sidebarStyle, setSidebarStyle] = useLocalStorageState(
|
||||||
const { permission } = useProjectPermission();
|
"project-sidebar-style",
|
||||||
|
SidebarStyle.ExpandOnHover
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
|
||||||
const { subscription } = useSubscription();
|
|
||||||
|
|
||||||
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
const minSidebarWidth =
|
||||||
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
|
sidebarStyle === SidebarStyle.Expanded ? MAX_SIDEBAR_SIZE : MIN_SIDEBAR_SIZE;
|
||||||
const isCmek = currentWorkspace?.type === ProjectType.KMS;
|
const maxSidebarWidth =
|
||||||
const isSSH = currentWorkspace?.type === ProjectType.SSH;
|
sidebarStyle === SidebarStyle.Collapsed ? MIN_SIDEBAR_SIZE : MAX_SIDEBAR_SIZE;
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
<div
|
||||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
className="dark hidden w-full flex-col overflow-x-hidden md:flex"
|
||||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
style={{ height: "calc(100vh - 3rem)" }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-grow flex-col overflow-y-auto overflow-x-hidden md:flex-row">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: -150 }}
|
exit={{ x: -150 }}
|
||||||
transition={{ duration: 0.2 }}
|
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]">
|
<nav className="items-between flex h-full flex-col justify-between">
|
||||||
<div>
|
<Menu>
|
||||||
<ProjectSelect />
|
<ShouldWrap
|
||||||
<div className="px-1">
|
wrapper={Tooltip}
|
||||||
<Menu>
|
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||||
<MenuGroup title="Main Menu">
|
content="Secret Manager"
|
||||||
{isSecretManager && (
|
position="right"
|
||||||
<Link
|
>
|
||||||
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
<Link
|
||||||
params={{
|
to="/projects/$projectId/secret-manager/overview"
|
||||||
projectId: currentWorkspace.id
|
params={{ projectId: currentWorkspace.id }}
|
||||||
}}
|
>
|
||||||
>
|
{({ isActive }) => (
|
||||||
{({ isActive }) => (
|
<MenuItem
|
||||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
className="relative flex items-center gap-2 overflow-hidden"
|
||||||
{t("nav.menu.secrets")}
|
isSelected={isActive}
|
||||||
</MenuItem>
|
leftIcon={
|
||||||
)}
|
<Lottie
|
||||||
</Link>
|
className="inline-block h-6 w-6 shrink-0"
|
||||||
)}
|
icon="sliding-carousel"
|
||||||
{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
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{isActive && (
|
||||||
<MenuItem isSelected={isActive} icon="groups">
|
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||||
Access Control
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
</Link>
|
Secret Manager
|
||||||
<Link
|
</MenuItem>
|
||||||
to={`/${currentWorkspace.type}/$projectId/settings` as const}
|
)}
|
||||||
params={{
|
</Link>
|
||||||
projectId: currentWorkspace.id
|
</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 }) => (
|
{isActive && (
|
||||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||||
{t("nav.menu.project-settings")}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
</Link>
|
PKI Manager
|
||||||
</MenuGroup>
|
</MenuItem>
|
||||||
</Menu>
|
)}
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
</motion.div>
|
</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 />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</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";
|
||||||
@@ -153,7 +153,7 @@ export const MyProjectView = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace),
|
to: getProjectHomePage(workspace.defaultType),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
|
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||||
import { zodValidator } from '@tanstack/zod-adapter'
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
import { z } from 'zod'
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OverviewPage } from './OverviewPage'
|
import { OverviewPage } from "./OverviewPage";
|
||||||
|
|
||||||
const SecretOverviewPageQuerySchema = z.object({
|
const SecretOverviewPageQuerySchema = z.object({
|
||||||
search: z.string().catch(''),
|
search: z.string().catch(""),
|
||||||
secretPath: z.string().catch('/'),
|
secretPath: z.string().catch("/")
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview',
|
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout/overview"
|
||||||
)({
|
)({
|
||||||
component: OverviewPage,
|
component: OverviewPage,
|
||||||
validateSearch: zodValidator(SecretOverviewPageQuerySchema),
|
validateSearch: zodValidator(SecretOverviewPageQuerySchema),
|
||||||
search: {
|
search: {
|
||||||
middlewares: [stripSearchParams({ secretPath: '/', search: '' })],
|
middlewares: [stripSearchParams({ secretPath: "/", search: "" })]
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { createFileRoute, linkOptions, Outlet } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
import { ProjectLayout } from "@app/layouts/ProjectLayout";
|
import { SecretManagerLayout } from "@app/layouts/SecretManagerLayout";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout"
|
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/secret-manager/_secret-manager-layout"
|
||||||
)({
|
)({
|
||||||
component: () => <Outlet />
|
component: SecretManagerLayout
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user