Files
prowler/ui/components/layout/sidebar/menu.tsx
T

220 lines
7.7 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LighthouseV2SidebarChat } from "@/app/(prowler)/lighthouse/_components/navigation";
import { InfoIcon, ProwlerShort } from "@/components/icons";
import { CollapsibleMenu } from "@/components/layout/sidebar/collapsible-menu";
import { MenuItem } from "@/components/layout/sidebar/menu-item";
import { Separator } from "@/components/shadcn";
import { Button } from "@/components/shadcn/button/button";
import { ScrollArea } from "@/components/shadcn/scroll-area/scroll-area";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { SidebarNavigationModeToggle } from "@/components/sidebar/navigation-mode-toggle";
import { useAuth } from "@/hooks";
import { useRuntimeConfig } from "@/hooks/use-runtime-config";
import { SIDEBAR_NAVIGATION_MODE, useSidebar } from "@/hooks/use-sidebar";
import { getMenuList } from "@/lib/menu-list";
import { LAUNCH_SCAN_HREF } from "@/lib/scans-navigation";
import { cn } from "@/lib/utils";
import { useScansStore } from "@/store";
import { GroupProps } from "@/types";
import { RolePermissionAttributes } from "@/types/users";
interface MenuHideRule {
label: string;
condition: (permissions: RolePermissionAttributes) => boolean;
}
const MENU_HIDE_RULES: MenuHideRule[] = [
{
label: "Billing",
condition: (permissions) => permissions?.manage_billing === false,
},
{
label: "Integrations",
condition: (permissions) => permissions?.manage_integrations === false,
},
];
const filterMenus = (menuGroups: GroupProps[], labelsToHide: string[]) => {
return menuGroups
.map((group) => ({
...group,
menus: group.menus
.filter((menu) => !labelsToHide.includes(menu.label))
.map((menu) => ({
...menu,
submenus: menu.submenus?.filter(
(submenu) => !labelsToHide.includes(submenu.label),
),
})),
}))
.filter((group) => group.menus.length > 0);
};
export const Menu = ({ isOpen }: { isOpen: boolean }) => {
const pathname = usePathname();
const { permissions } = useAuth();
const openLaunchScanModal = useScansStore(
(state) => state.openLaunchScanModal,
);
const isScansPage = pathname.startsWith("/scans");
const { apiDocsUrl } = useRuntimeConfig();
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const navigationMode = useSidebar((state) => state.navigationMode);
const setNavigationMode = useSidebar((state) => state.setNavigationMode);
const menuList = getMenuList({
pathname,
apiDocsUrl,
});
const labelsToHide = MENU_HIDE_RULES.filter((rule) =>
rule.condition(permissions),
).map((rule) => rule.label);
const filteredMenuList = filterMenus(menuList, labelsToHide);
return (
<div className="flex h-full flex-col overflow-hidden">
{/* Launch Scan Button — mt-1 aligns its top with the main content panel
(navbar 72px + py-4 = 88px), matching the fixed-height logo block. */}
<div className="mt-1 flex shrink-0 justify-center px-2">
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
{isScansPage ? (
<Button
type="button"
aria-label="Launch Scan"
className={cn(isOpen ? "h-14 w-full p-1" : "w-14")}
variant="default"
size="default"
onClick={openLaunchScanModal}
>
<LaunchScanButtonContent isOpen={isOpen} />
</Button>
) : (
<Button
asChild
className={cn(isOpen ? "h-14 w-full p-1" : "w-14")}
variant="default"
size="default"
>
<Link href={LAUNCH_SCAN_HREF} aria-label="Launch Scan">
<LaunchScanButtonContent isOpen={isOpen} />
</Link>
</Button>
)}
</TooltipTrigger>
{!isOpen && <TooltipContent side="right">Launch Scan</TooltipContent>}
</Tooltip>
</div>
<SidebarNavigationModeToggle
isOpen={isOpen}
value={navigationMode}
onChange={setNavigationMode}
chatEnabled={isCloudEnv}
/>
{/* Menu Items */}
<div className="flex-1 overflow-hidden">
{isCloudEnv && navigationMode === SIDEBAR_NAVIGATION_MODE.CHAT ? (
<LighthouseV2SidebarChat isOpen={isOpen} />
) : (
<ScrollArea className="h-full [&>div>div[style]]:block!">
<nav className="mt-2 w-full lg:mt-6">
<ul className="mx-2 flex flex-col items-start gap-1 pb-4">
{filteredMenuList.map((group, groupIndex) => (
<li key={groupIndex} className="w-full">
{group.menus.map((menu, menuIndex) => (
<div key={menuIndex} className="w-full">
{menu.submenus && menu.submenus.length > 0 ? (
<CollapsibleMenu
icon={menu.icon}
label={menu.label}
submenus={menu.submenus}
defaultOpen={menu.defaultOpen}
isOpen={isOpen}
/>
) : (
<MenuItem
href={menu.href}
label={menu.label}
icon={menu.icon}
active={menu.active}
target={menu.target}
tooltip={menu.tooltip}
isOpen={isOpen}
highlight={menu.highlight}
/>
)}
</div>
))}
</li>
))}
</ul>
</nav>
</ScrollArea>
)}
</div>
{/* Footer */}
<div className="text-muted-foreground border-border-neutral-secondary flex shrink-0 items-center justify-center gap-2 border-t pt-4 pb-2 text-center text-xs">
{isOpen ? (
<>
<span>{process.env.NEXT_PUBLIC_PROWLER_RELEASE_VERSION}</span>
{process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
<>
<Separator orientation="vertical" />
<Link
href="https://status.prowler.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1"
>
<InfoIcon size={16} />
<span className="text-muted-foreground font-normal opacity-80 transition-opacity hover:font-bold hover:opacity-100">
Service Status
</span>
</Link>
</>
)}
</>
) : (
process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
<Tooltip>
<TooltipTrigger asChild>
<Link
href="https://status.prowler.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center"
>
<InfoIcon size={16} />
</Link>
</TooltipTrigger>
<TooltipContent side="right">Service Status</TooltipContent>
</Tooltip>
)
)}
</div>
</div>
);
};
function LaunchScanButtonContent({ isOpen }: { isOpen: boolean }) {
return (
<span className={cn("flex items-center", isOpen && "gap-2.5")}>
<ProwlerShort aria-hidden="true" className="size-5 text-current" />
{isOpen && <span className="text-xl leading-8">Scan</span>}
</span>
);
}