mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 07:14:20 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
253 lines
8.2 KiB
TypeScript
253 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { Icon } from "@iconify/react";
|
|
import Link from "next/link";
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
import { ReactNode } from "react";
|
|
|
|
import { LighthouseIcon } from "@/components/icons/Icons";
|
|
import { buildPerScanComplianceHref } from "@/lib/compliance/compliance-tab-url";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface CustomBreadcrumbItem {
|
|
name: string;
|
|
path?: string;
|
|
icon?: string | ReactNode;
|
|
isLast?: boolean;
|
|
isClickable?: boolean;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
interface BreadcrumbNavigationProps {
|
|
mode?: "auto" | "custom" | "hybrid";
|
|
title?: string;
|
|
icon?: string | ReactNode;
|
|
titleAction?: ReactNode;
|
|
customItems?: CustomBreadcrumbItem[];
|
|
className?: string;
|
|
paramToPreserve?: string;
|
|
showTitle?: boolean;
|
|
}
|
|
|
|
export function BreadcrumbNavigation({
|
|
mode = "auto",
|
|
title,
|
|
icon,
|
|
titleAction,
|
|
customItems = [],
|
|
className = "",
|
|
paramToPreserve = "scanId",
|
|
showTitle = true,
|
|
}: BreadcrumbNavigationProps) {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
|
|
const generateAutoBreadcrumbs = (): CustomBreadcrumbItem[] => {
|
|
const pathIconMapping: Record<string, string | ReactNode> = {
|
|
"/integrations": "lucide:puzzle",
|
|
"/alerts": "lucide:bell-ring",
|
|
"/providers": "lucide:cloud",
|
|
"/users": "lucide:users",
|
|
"/compliance": "lucide:shield-check",
|
|
"/findings": "lucide:search",
|
|
"/scans": "lucide:timer",
|
|
"/roles": "lucide:key",
|
|
"/resources": "lucide:database",
|
|
"/lighthouse": <LighthouseIcon />,
|
|
"/manage-groups": "lucide:users-2",
|
|
"/services": "lucide:server",
|
|
"/workloads": "lucide:layers",
|
|
"/attack-paths": "lucide:git-branch",
|
|
};
|
|
|
|
const pathSegments = pathname
|
|
.split("/")
|
|
.filter((segment) => segment !== "");
|
|
|
|
if (pathSegments.length === 0) {
|
|
return [{ name: "Home", path: "/", isLast: true }];
|
|
}
|
|
|
|
const breadcrumbs: CustomBreadcrumbItem[] = [];
|
|
let currentPath = "";
|
|
|
|
pathSegments.forEach((segment, index) => {
|
|
currentPath += `/${segment}`;
|
|
const isLast = index === pathSegments.length - 1;
|
|
let displayName = segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
|
|
if (segment.includes("-")) {
|
|
displayName = segment
|
|
.split("-")
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(" ");
|
|
}
|
|
if (segment === "lighthouse") {
|
|
displayName = "Lighthouse AI";
|
|
}
|
|
|
|
const segmentIcon = !isLast ? pathIconMapping[currentPath] : undefined;
|
|
|
|
breadcrumbs.push({
|
|
name: displayName,
|
|
path: currentPath,
|
|
icon: segmentIcon,
|
|
isLast,
|
|
isClickable: !isLast,
|
|
});
|
|
});
|
|
|
|
return breadcrumbs;
|
|
};
|
|
|
|
const buildNavigationUrl = (path: string) => {
|
|
const paramValue = searchParams.get(paramToPreserve);
|
|
// A preserved scan only makes sense on Single Scan, which is no longer
|
|
// the landing tab: the bare route would drop it for Multiple Scans.
|
|
if (path === "/compliance" && paramValue) {
|
|
return buildPerScanComplianceHref({ [paramToPreserve]: paramValue });
|
|
}
|
|
return path;
|
|
};
|
|
|
|
const renderTitleWithIcon = (
|
|
titleText: string,
|
|
isLink: boolean = false,
|
|
showIcon: boolean = true,
|
|
) => (
|
|
<div className="flex items-center gap-2">
|
|
{showIcon && typeof icon === "string" ? (
|
|
<Icon
|
|
className="text-text-neutral-primary"
|
|
height={24}
|
|
icon={icon}
|
|
width={24}
|
|
/>
|
|
) : showIcon && icon ? (
|
|
<div className="flex h-8 w-8 items-center justify-center *:h-full *:w-full">
|
|
{icon}
|
|
</div>
|
|
) : null}
|
|
<h1
|
|
className={`text-text-neutral-primary max-w-[200px] truncate text-sm font-bold sm:max-w-none ${isLink ? "hover:text-button-primary transition-colors" : ""}`}
|
|
>
|
|
{titleText}
|
|
</h1>
|
|
{titleAction}
|
|
</div>
|
|
);
|
|
|
|
let breadcrumbItems: CustomBreadcrumbItem[] = [];
|
|
|
|
switch (mode) {
|
|
case "auto":
|
|
breadcrumbItems = generateAutoBreadcrumbs();
|
|
break;
|
|
case "custom":
|
|
breadcrumbItems = customItems;
|
|
break;
|
|
case "hybrid":
|
|
breadcrumbItems = [...generateAutoBreadcrumbs(), ...customItems];
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<div className={cn(className, "w-fit md:w-full")}>
|
|
<nav aria-label="Breadcrumb">
|
|
<ol className="flex flex-wrap items-center">
|
|
{breadcrumbItems.map((breadcrumb, index) => (
|
|
<li
|
|
key={breadcrumb.path || index}
|
|
className="flex items-center"
|
|
aria-current={
|
|
index === breadcrumbItems.length - 1 ? "page" : undefined
|
|
}
|
|
>
|
|
{breadcrumb.isLast && showTitle && title ? (
|
|
renderTitleWithIcon(title, false, index === 0)
|
|
) : breadcrumb.isClickable && breadcrumb.path ? (
|
|
<Link
|
|
href={buildNavigationUrl(breadcrumb.path)}
|
|
className="flex cursor-pointer items-center gap-2"
|
|
>
|
|
{index === 0 &&
|
|
breadcrumb.icon &&
|
|
typeof breadcrumb.icon === "string" ? (
|
|
<Icon
|
|
aria-hidden="true"
|
|
className="text-text-neutral-primary"
|
|
height={24}
|
|
icon={breadcrumb.icon}
|
|
width={24}
|
|
/>
|
|
) : index === 0 && breadcrumb.icon ? (
|
|
<div className="flex h-6 w-6 items-center justify-center *:h-full *:w-full">
|
|
{breadcrumb.icon}
|
|
</div>
|
|
) : null}
|
|
<span className="text-text-neutral-primary hover:text-button-primary max-w-[150px] truncate text-sm font-bold transition-colors sm:max-w-none">
|
|
{breadcrumb.name}
|
|
</span>
|
|
</Link>
|
|
) : breadcrumb.isClickable && breadcrumb.onClick ? (
|
|
<button
|
|
onClick={breadcrumb.onClick}
|
|
className="text-text-neutral-primary hover:text-text-neutral-primary-hover flex cursor-pointer items-center gap-2 text-sm font-medium transition-colors"
|
|
>
|
|
{index === 0 &&
|
|
breadcrumb.icon &&
|
|
typeof breadcrumb.icon === "string" ? (
|
|
<Icon
|
|
aria-hidden="true"
|
|
className="text-text-neutral-primary"
|
|
height={24}
|
|
icon={breadcrumb.icon}
|
|
width={24}
|
|
/>
|
|
) : index === 0 && breadcrumb.icon ? (
|
|
<div className="flex h-6 w-6 items-center justify-center *:h-full *:w-full">
|
|
{breadcrumb.icon}
|
|
</div>
|
|
) : null}
|
|
<span className="max-w-[150px] truncate sm:max-w-none">
|
|
{breadcrumb.name}
|
|
</span>
|
|
</button>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
{index === 0 &&
|
|
breadcrumb.icon &&
|
|
typeof breadcrumb.icon === "string" ? (
|
|
<Icon
|
|
aria-hidden="true"
|
|
className="text-text-neutral-tertiary"
|
|
height={24}
|
|
icon={breadcrumb.icon}
|
|
width={24}
|
|
/>
|
|
) : index === 0 && breadcrumb.icon ? (
|
|
<div className="flex h-6 w-6 items-center justify-center *:h-full *:w-full">
|
|
{breadcrumb.icon}
|
|
</div>
|
|
) : null}
|
|
<span className="max-w-[150px] truncate text-sm font-medium text-gray-900 sm:max-w-none dark:text-gray-100">
|
|
{breadcrumb.name}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{index < breadcrumbItems.length - 1 && (
|
|
<span
|
|
aria-hidden="true"
|
|
className="text-text-neutral-tertiary px-1 text-sm"
|
|
>
|
|
/
|
|
</span>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|