Compare commits

...

5 Commits

Author SHA1 Message Date
sumit_chaturvedi
98047e690d Merge branch 'master' into PRWLR-7512-create-custom-link-component 2025-07-08 15:16:46 +05:30
sumit_chaturvedi
fa11e98a55 chore(ui): addressed PR comments 2025-07-08 14:33:56 +05:30
sumit_chaturvedi
ff691f1d37 Merge branch 'master' into PRWLR-7512-create-custom-link-component
# Conflicts:
#	ui/CHANGELOG.md
2025-07-08 08:22:04 +05:30
sumit_chaturvedi
819c9306ee docs: changelog update 2025-07-07 10:56:30 +05:30
sumit_chaturvedi
bfc72170c5 feat(ui): create CustomLink component decoupled from CustomButton 2025-07-07 10:19:50 +05:30
21 changed files with 235 additions and 92 deletions

View File

@@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🚀 Added
- SAML login integration [(#8203)](https://github.com/prowler-cloud/prowler/pull/8203)
- Introduced new `CustomLink` component for handling all navigation and link-related behavior [(#8195)] (https://github.com/prowler-cloud/prowler/pull/8195)
### 🔄 Changed

View File

@@ -2,8 +2,9 @@
import React from "react";
import { CustomLink } from "@/components/ui/custom";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoScansAvailable = () => {
return (
@@ -23,8 +24,8 @@ export const NoScansAvailable = () => {
</p>
</div>
</div>
<CustomButton
asLink="/scans"
<CustomLink
href="/scans"
className="flex-shrink-0"
ariaLabel="Go to Scans page"
variant="solid"
@@ -32,7 +33,7 @@ export const NoScansAvailable = () => {
size="sm"
>
Go to Scans
</CustomButton>
</CustomLink>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { Tooltip } from "@nextui-org/react";
import { CustomButton } from "@/components/ui/custom/custom-button";
import { CustomLink } from "@/components/ui/custom";
import { cn } from "@/lib/utils";
interface DeltaIndicatorProps {
@@ -18,16 +18,16 @@ export const DeltaIndicator = ({ delta }: DeltaIndicatorProps) => {
? "New finding."
: "Status changed since the previous scan."}
</span>
<CustomButton
<CustomLink
ariaLabel="Learn more about findings"
color="transparent"
size="sm"
className="h-auto min-w-0 p-0 text-primary"
asLink="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-8-analyze-the-findings"
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-8-analyze-the-findings"
target="_blank"
>
Learn more
</CustomButton>
</CustomLink>
</div>
}
>

View File

@@ -2,8 +2,9 @@
import { Card, CardBody, Divider, Snippet } from "@nextui-org/react";
import { CustomLink } from "@/components/ui/custom";
import { AddIcon } from "../icons";
import { CustomButton } from "../ui/custom";
import { DateWithTime } from "../ui/entities";
interface InvitationDetailsProps {
@@ -108,8 +109,8 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
</CardBody>
</Card>
<div className="flex w-full items-center justify-end">
<CustomButton
asLink="/invitations/"
<CustomLink
href={"/invitations/"}
ariaLabel="Send Invitation"
variant="solid"
color="action"
@@ -117,7 +118,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
endContent={<AddIcon size={20} />}
>
Back to Invitations
</CustomButton>
</CustomLink>
</div>
</div>
);

View File

@@ -1,13 +1,14 @@
"use client";
import { CustomLink } from "@/components/ui/custom";
import { AddIcon } from "../icons";
import { CustomButton } from "../ui/custom";
export const SendInvitationButton = () => {
return (
<div className="flex w-full items-center justify-end">
<CustomButton
asLink="/invitations/new"
<CustomLink
href={"/invitations/new"}
ariaLabel="Send Invitation"
variant="solid"
color="action"
@@ -15,7 +16,7 @@ export const SendInvitationButton = () => {
endContent={<AddIcon size={20} />}
>
Send Invitation
</CustomButton>
</CustomLink>
</div>
);
};

View File

@@ -2,19 +2,19 @@
import { SettingsIcon } from "lucide-react";
import { CustomButton } from "../ui/custom";
import { CustomLink } from "@/components/ui/custom";
export const ManageGroupsButton = () => {
return (
<CustomButton
asLink="/manage-groups"
<CustomLink
href={"/manage-groups"}
ariaLabel="Manage Groups"
variant="dashed"
color="warning"
color="secondary"
size="md"
startContent={<SettingsIcon size={20} />}
>
Manage Groups
</CustomButton>
</CustomLink>
);
};

View File

@@ -1,19 +1,19 @@
"use client";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom";
export const LinkToFindings = () => {
return (
<div className="mt-4 flex w-full items-center justify-end">
<CustomButton
asLink="/findings?sort=severity,-inserted_at&filter[status__in]=FAIL&filter[delta__in]=new"
<CustomLink
href="/findings?sort=severity,-inserted_at&filter[status__in]=FAIL&filter[delta__in]=new"
ariaLabel="Go to Findings page"
variant="solid"
color="action"
size="sm"
>
Check out on Findings
</CustomButton>
</CustomLink>
</div>
);
};

View File

@@ -10,7 +10,7 @@ import {
KS8ProviderBadge,
M365ProviderBadge,
} from "@/components/icons/providers-badge";
import { CustomButton } from "@/components/ui/custom/custom-button";
import { CustomLink } from "@/components/ui/custom";
import { ProviderOverviewProps } from "@/types";
export const ProvidersOverview = ({
@@ -178,8 +178,8 @@ export const ProvidersOverview = ({
</div>
</div>
<div className="mt-4 flex w-full items-center justify-end">
<CustomButton
asLink="/providers"
<CustomLink
href="/providers"
ariaLabel="Go to Providers page"
variant="solid"
color="action"
@@ -187,7 +187,7 @@ export const ProvidersOverview = ({
endContent={<AddIcon size={20} />}
>
Add Provider
</CustomButton>
</CustomLink>
</div>
</CardBody>
</Card>

View File

@@ -1,19 +1,20 @@
"use client";
import { CustomLink } from "@/components/ui/custom";
import { AddIcon } from "../icons";
import { CustomButton } from "../ui/custom";
export const AddProviderButton = () => {
return (
<CustomButton
asLink="/providers/connect-account"
<CustomLink
href="/providers/connect-account"
ariaLabel="Add Cloud Provider"
variant="solid"
color="action"
size="md"
color="action"
endContent={<AddIcon size={20} />}
>
Add Cloud Provider
</CustomButton>
</CustomLink>
);
};

View File

@@ -1,6 +1,6 @@
"use client";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom";
interface LinkToScansProps {
providerUid?: string;
@@ -8,14 +8,14 @@ interface LinkToScansProps {
export const LinkToScans = ({ providerUid }: LinkToScansProps) => {
return (
<CustomButton
asLink={`/scans?filter[provider_uid]=${providerUid}`}
<CustomLink
href={`/scans?filter[provider_uid]=${providerUid}`}
ariaLabel="Go to Scans page"
variant="solid"
color="action"
size="sm"
>
View Scan Jobs
</CustomButton>
</CustomLink>
);
};

View File

@@ -3,7 +3,7 @@
import { Snippet } from "@nextui-org/react";
import { useSession } from "next-auth/react";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom";
import { getAWSCredentialsTemplateLinks } from "@/lib";
export const CredentialsRoleHelper = () => {
@@ -16,15 +16,15 @@ export const CredentialsRoleHelper = () => {
A <strong>new read-only IAM role</strong> must be manually created.
</p>
<CustomButton
<CustomLink
ariaLabel="Use the following AWS CloudFormation Quick Link to deploy the IAM Role"
color="transparent"
className="h-auto w-fit min-w-0 p-0 text-blue-500"
asLink={`${getAWSCredentialsTemplateLinks().cloudformationQuickLink}${session?.tenantId}`}
variant="textLink"
href={`${getAWSCredentialsTemplateLinks().cloudformationQuickLink}${session?.tenantId}`}
target="_blank"
>
Use the following AWS CloudFormation Quick Link to deploy the IAM Role
</CustomButton>
</CustomLink>
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-200 dark:bg-gray-700" />
@@ -38,24 +38,24 @@ export const CredentialsRoleHelper = () => {
</p>
<div className="flex w-fit flex-col gap-2">
<CustomButton
<CustomLink
ariaLabel="CloudFormation Template"
color="transparent"
className="h-auto w-fit min-w-0 p-0 text-blue-500"
asLink={getAWSCredentialsTemplateLinks().cloudformation}
variant="textLink"
href={getAWSCredentialsTemplateLinks().cloudformation}
target="_blank"
>
CloudFormation Template
</CustomButton>
<CustomButton
</CustomLink>
<CustomLink
ariaLabel="Terraform Code"
color="transparent"
className="h-auto w-fit min-w-0 p-0 text-blue-500"
asLink={getAWSCredentialsTemplateLinks().terraform}
variant="textLink"
href={getAWSCredentialsTemplateLinks().terraform}
target="_blank"
>
Terraform Code
</CustomButton>
</CustomLink>
</div>
<p className="text-xs font-bold text-gray-600 dark:text-gray-400">

View File

@@ -1,13 +1,14 @@
"use client";
import { CustomLink } from "@/components/ui/custom";
import { AddIcon } from "../icons";
import { CustomButton } from "../ui/custom";
export const AddRoleButton = () => {
return (
<div className="flex w-full items-center justify-end">
<CustomButton
asLink="/roles/new"
<CustomLink
href="/roles/new"
ariaLabel="Add Role"
variant="solid"
color="action"
@@ -15,7 +16,7 @@ export const AddRoleButton = () => {
endContent={<AddIcon size={20} />}
>
Add Role
</CustomButton>
</CustomLink>
</div>
);
};

View File

@@ -1,6 +1,6 @@
"use client";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom";
interface LinkToFindingsProps {
scanId?: string;
@@ -12,15 +12,15 @@ export const LinkToFindingsFromScan = ({
isDisabled,
}: LinkToFindingsProps) => {
return (
<CustomButton
asLink={`/findings?filter[scan__in]=${scanId}&filter[status__in]=FAIL`}
<CustomLink
href={`/findings?filter[scan__in]=${scanId}&filter[status__in]=FAIL`}
ariaLabel="Go to Findings page"
color="muted"
variant="ghost"
className="text-xs font-medium text-default-500 hover:text-primary disabled:opacity-30"
size="sm"
isDisabled={isDisabled}
>
See Findings
</CustomButton>
</CustomLink>
);
};

View File

@@ -3,8 +3,9 @@
import { Card, CardBody } from "@nextui-org/react";
import React from "react";
import { CustomLink } from "@/components/ui/custom";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoProvidersAdded = () => {
return (
@@ -25,16 +26,16 @@ export const NoProvidersAdded = () => {
</p>
</div>
<CustomButton
asLink="/providers/connect-account"
<CustomLink
href="/providers/connect-account"
ariaLabel="Go to Add Cloud Provider page"
className="w-full max-w-xs justify-center"
variant="solid"
color="action"
size="lg"
size="md"
>
Get Started
</CustomButton>
</CustomLink>
</CardBody>
</Card>
</div>

View File

@@ -2,8 +2,9 @@
import React from "react";
import { CustomLink } from "@/components/ui/custom";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoProvidersConnected = () => {
return (
@@ -26,8 +27,8 @@ export const NoProvidersConnected = () => {
</p>
</div>
<div className="w-full md:w-auto md:flex-shrink-0">
<CustomButton
asLink="/providers"
<CustomLink
href="/providers"
className="w-full justify-center md:w-fit"
ariaLabel="Go to Cloud providers page"
variant="solid"
@@ -35,7 +36,7 @@ export const NoProvidersConnected = () => {
size="md"
>
Review Cloud Providers
</CustomButton>
</CustomLink>
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { Button, CircularProgress } from "@nextui-org/react";
import type { PressEvent } from "@react-types/shared";
import clsx from "clsx";
import Link from "next/link";
import React from "react";
import { NextUIColors, NextUIVariants } from "@/types";
@@ -53,7 +52,6 @@ interface CustomButtonProps {
isLoading?: boolean;
isIconOnly?: boolean;
ref?: React.RefObject<HTMLButtonElement>;
asLink?: string;
}
export const CustomButton = React.forwardRef<
@@ -78,14 +76,11 @@ export const CustomButton = React.forwardRef<
isDisabled = false,
isLoading = false,
isIconOnly,
asLink,
...props
},
ref,
) => (
<Button
as={asLink ? Link : undefined}
href={asLink}
target={target}
type={type}
aria-label={ariaLabel}

View File

@@ -0,0 +1,140 @@
import Link from "next/link";
import React from "react";
import { cn } from "@/lib";
interface CustomLinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
target?: "_self" | "_blank";
rel?: string;
className?: string;
children?: React.ReactNode;
variant?:
| "default"
| "dashed"
| "ghost"
| "block"
| "solid"
| "unstyled"
| "iconButton"
| "textLink";
color?:
| "primary"
| "secondary"
| "action"
| "transparent"
| "danger"
| "success"
| "muted";
size?: "md" | "sm" | "lg";
startContent?: React.ReactNode;
endContent?: React.ReactNode;
isIconOnly?: boolean;
ariaLabel?: string;
isDisabled?: boolean;
}
const linkClasses = {
base: "inline-flex items-center gap-1 text-sm font-medium transition-colors duration-200",
iconOnly: "p-2 rounded-full justify-center",
disabled: "opacity-30 pointer-events-none cursor-not-allowed",
};
const variantClasses = {
default: "",
dashed:
"border border-default border-dashed bg-transparent justify-center whitespace-nowrap shadow-sm hover:border-solid hover:bg-default-100 active:bg-default-200 active:border-solid",
iconButton:
"whitespace-nowrap rounded-[14px] border-2 border-gray-200 bg-prowler-grey-medium p-3 bg-transparent",
ghost:
"whitespace-nowrap border border-prowler-theme-green text-default-500 hover:bg-prowler-theme-green hover:!text-black disabled:opacity-30",
solid: "whitespace-nowrap min-w-20",
textLink: "h-auto w-fit min-w-0 p-0 text-blue-500",
block: "block w-full text-left",
unstyled: "",
};
const colorClasses = {
primary: "text-prowler-theme-green",
secondary: "text-default-800 dark:text-white",
action:
"bg-prowler-theme-green font-bold text-prowler-theme-midnight hover:opacity-80 transition-opacity duration-100",
transparent: "border-0 border-transparent bg-transparent",
danger: "text-red-600 dark:text-red-400",
success: "text-green-600 dark:text-green-400",
muted: "text-gray-500 dark:text-gray-400",
};
const sizeClasses = {
sm: "text-xs px-4 h-8 rounded-lg",
md: "text-sm px-4 py-2 h-10 rounded-lg",
lg: "text-lg px-5 py-3 h-12 rounded-xl",
};
export const CustomLink = React.forwardRef<HTMLAnchorElement, CustomLinkProps>(
(
{
href,
target = "_self",
rel,
className,
children,
variant = "default",
color = "primary",
size = "md",
startContent,
endContent,
isIconOnly = false,
ariaLabel,
isDisabled = false,
...rest
},
ref,
) => {
const isExternal = target === "_blank";
const computedRel = isExternal ? "noopener noreferrer" : rel;
const content = (
<>
{startContent && <span>{startContent}</span>}
{!isIconOnly && children}
{endContent && <span>{endContent}</span>}
</>
);
const combinedClasses = cn(
linkClasses.base,
colorClasses[color],
sizeClasses[size],
variantClasses[variant],
isIconOnly && linkClasses.iconOnly,
isDisabled && linkClasses.disabled,
className,
);
return isDisabled ? (
<span
className={combinedClasses}
aria-disabled="true"
aria-label={ariaLabel}
>
{content}
</span>
) : (
<Link
href={href}
target={target}
rel={computedRel}
ref={ref}
aria-label={ariaLabel}
className={combinedClasses}
{...rest}
>
{content}
</Link>
);
},
);
CustomLink.displayName = "CustomLink";

View File

@@ -4,6 +4,7 @@ export * from "./custom-button";
export * from "./custom-dropdown-filter";
export * from "./custom-dropdown-selection";
export * from "./custom-input";
export * from "./custom-link";
export * from "./custom-loader";
export * from "./custom-radio";
export * from "./custom-server-input";

View File

@@ -2,7 +2,7 @@ import { Icon } from "@iconify/react";
import { Divider } from "@nextui-org/react";
import React from "react";
import { CustomButton } from "@/components/ui/custom/custom-button";
import { CustomLink } from "@/components/ui/custom/custom-link";
interface NavigationHeaderProps {
title: string;
@@ -18,16 +18,14 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
return (
<>
<header className="flex items-center gap-3 border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<CustomButton
asLink={href || ""}
className="border-gray-200 bg-transparent p-0"
<CustomLink
href={href || ""}
variant="iconButton"
ariaLabel="Navigation button"
variant="bordered"
isIconOnly
radius="lg"
color="muted"
>
<Icon icon={icon} className="text-gray-600 dark:text-gray-400" />
</CustomButton>
</CustomLink>
<Divider orientation="vertical" className="h-6" />
<h1 className="text-xl font-light text-default-700">{title}</h1>
</header>

View File

@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import { logOut } from "@/actions/auth";
import { AddIcon, InfoIcon } from "@/components/icons";
import { CustomLink } from "@/components/ui/custom";
import { CollapseMenuButton } from "@/components/ui/sidebar/collapse-menu-button";
import {
Tooltip,
@@ -18,7 +19,6 @@ import { getMenuList } from "@/lib/menu-list";
import { cn } from "@/lib/utils";
import { Button } from "../button/button";
import { CustomButton } from "../custom/custom-button";
import { ScrollArea } from "../scroll-area/scroll-area";
export const Menu = ({ isOpen }: { isOpen: boolean }) => {
@@ -28,17 +28,17 @@ export const Menu = ({ isOpen }: { isOpen: boolean }) => {
return (
<>
<div className="px-2">
<CustomButton
asLink="/scans"
className={cn(isOpen ? "w-full" : "w-fit")}
ariaLabel="Launch Scan"
<CustomLink
href="/scans"
className={cn(isOpen ? "w-full" : "w-fit", "justify-center")}
variant="solid"
ariaLabel="Launch Scan"
color="action"
size="md"
endContent={isOpen ? <AddIcon size={20} /> : null}
>
{isOpen ? "Launch Scan" : <AddIcon size={20} />}
</CustomButton>
</CustomLink>
</div>
<ScrollArea className="[&>div>div[style]]:!block">
<nav className="mt-2 h-full w-full lg:mt-6">

View File

@@ -1,13 +1,14 @@
"use client";
import { CustomLink } from "@/components/ui/custom";
import { AddIcon } from "../icons";
import { CustomButton } from "../ui/custom";
export const AddUserButton = () => {
return (
<div className="flex w-full items-center justify-end">
<CustomButton
asLink="/invitations/new"
<CustomLink
href="/invitations/new"
ariaLabel="Invite User"
variant="solid"
color="action"
@@ -15,7 +16,7 @@ export const AddUserButton = () => {
endContent={<AddIcon size={20} />}
>
Invite User
</CustomButton>
</CustomLink>
</div>
);
};