From 8ce56b5ed64f1e6ce7da62378556efe42d024086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 23 Dec 2025 12:09:55 +0100 Subject: [PATCH] feat(ui): add search bar when adding a provider (#9634) Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 10 +- .../providers/radio-group-provider.tsx | 205 +++++++++++------- ui/components/shadcn/index.ts | 2 + ui/components/shadcn/input/input.tsx | 51 +++++ .../shadcn/search-input/search-input.tsx | 125 +++++++++++ 5 files changed, 316 insertions(+), 77 deletions(-) create mode 100644 ui/components/shadcn/input/input.tsx create mode 100644 ui/components/shadcn/search-input/search-input.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index c521270638..6c38f06b0d 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,7 +2,15 @@ All notable changes to the **Prowler UI** are documented in this file. -## [1.16.1] (Prowler v5.17.1) +## [1.17.0] (Prowler v5.17.0 Unreleased) + +### 🚀 Added + +- Add search bar when adding a provider [(#9634)](https://github.com/prowler-cloud/prowler/pull/9634) + +--- + +## [1.16.1] (Prowler v5.16.1) ### 🔄 Changed diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index 6bd4adacff..de4f8596b6 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -1,10 +1,11 @@ "use client"; -import { RadioGroup } from "@heroui/radio"; -import { FC } from "react"; +import { FC, useState } from "react"; import { Control, Controller } from "react-hook-form"; import { z } from "zod"; +import { SearchInput } from "@/components/shadcn"; +import { cn } from "@/lib/utils"; import { addProviderFormSchema } from "@/types"; import { @@ -19,9 +20,61 @@ import { MongoDBAtlasProviderBadge, OracleCloudProviderBadge, } from "../icons/providers-badge"; -import { CustomRadio } from "../ui/custom"; import { FormMessage } from "../ui/form"; +const PROVIDERS = [ + { + value: "aws", + label: "Amazon Web Services", + badge: AWSProviderBadge, + }, + { + value: "gcp", + label: "Google Cloud Platform", + badge: GCPProviderBadge, + }, + { + value: "azure", + label: "Microsoft Azure", + badge: AzureProviderBadge, + }, + { + value: "m365", + label: "Microsoft 365", + badge: M365ProviderBadge, + }, + { + value: "mongodbatlas", + label: "MongoDB Atlas", + badge: MongoDBAtlasProviderBadge, + }, + { + value: "kubernetes", + label: "Kubernetes", + badge: KS8ProviderBadge, + }, + { + value: "github", + label: "GitHub", + badge: GitHubProviderBadge, + }, + { + value: "iac", + label: "Infrastructure as Code", + badge: IacProviderBadge, + }, + { + value: "oraclecloud", + label: "Oracle Cloud Infrastructure", + badge: OracleCloudProviderBadge, + }, + { + value: "alibabacloud", + label: "Alibaba Cloud", + badge: AlibabaCloudProviderBadge, + }, +] as const; + interface RadioGroupProviderProps { control: Control>; isInvalid: boolean; @@ -33,90 +86,90 @@ export const RadioGroupProvider: FC = ({ isInvalid, errorMessage, }) => { + const [searchTerm, setSearchTerm] = useState(""); + + const lowerSearch = searchTerm.trim().toLowerCase(); + const filteredProviders = lowerSearch + ? PROVIDERS.filter( + (provider) => + provider.label.toLowerCase().includes(lowerSearch) || + provider.value.toLowerCase().includes(lowerSearch), + ) + : PROVIDERS; + return ( ( - <> - -
- -
- - Amazon Web Services -
-
- -
- - Google Cloud Platform -
-
- -
- - Microsoft Azure -
-
- -
- - Microsoft 365 -
-
- -
- - MongoDB Atlas -
-
- -
- - Kubernetes -
-
- -
- - GitHub -
-
- -
- - Infrastructure as Code -
-
- -
- - Oracle Cloud Infrastructure -
-
- -
- - Alibaba Cloud -
-
+
+
+ setSearchTerm(e.target.value)} + onClear={() => setSearchTerm("")} + /> +
+ +
+
+ {filteredProviders.length > 0 ? ( + filteredProviders.map((provider) => { + const BadgeComponent = provider.badge; + const isSelected = field.value === provider.value; + + return ( + + ); + }) + ) : ( +

+ No providers found matching "{searchTerm}" +

+ )}
- +
+ {errorMessage && ( {errorMessage} )} - +
)} /> ); diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts index 39a72730d1..88ff8d80ac 100644 --- a/ui/components/shadcn/index.ts +++ b/ui/components/shadcn/index.ts @@ -8,6 +8,8 @@ export * from "./card/resource-stats-card/resource-stats-card-header"; export * from "./checkbox/checkbox"; export * from "./combobox"; export * from "./dropdown/dropdown"; +export * from "./input/input"; +export * from "./search-input/search-input"; export * from "./select/multiselect"; export * from "./select/select"; export * from "./separator/separator"; diff --git a/ui/components/shadcn/input/input.tsx b/ui/components/shadcn/input/input.tsx new file mode 100644 index 0000000000..298a4b21d1 --- /dev/null +++ b/ui/components/shadcn/input/input.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import { ComponentProps, forwardRef } from "react"; + +import { cn } from "@/lib/utils"; + +const inputVariants = cva( + "flex w-full rounded-lg border text-sm transition-all outline-none file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + variant: { + default: + "border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1 placeholder:text-text-neutral-tertiary", + ghost: + "border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary placeholder:text-text-neutral-tertiary", + }, + inputSize: { + default: "h-10 px-4 py-3", + sm: "h-8 px-3 py-2 text-xs", + lg: "h-12 px-5 py-4", + }, + }, + defaultVariants: { + variant: "default", + inputSize: "default", + }, + }, +); + +export interface InputProps + extends Omit, "size">, + VariantProps {} + +const Input = forwardRef( + ({ className, variant, inputSize, type = "text", ...props }, ref) => { + return ( + + ); + }, +); + +Input.displayName = "Input"; + +export { Input, inputVariants }; diff --git a/ui/components/shadcn/search-input/search-input.tsx b/ui/components/shadcn/search-input/search-input.tsx new file mode 100644 index 0000000000..16397460b8 --- /dev/null +++ b/ui/components/shadcn/search-input/search-input.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import { SearchIcon, XCircle } from "lucide-react"; +import { ComponentProps, forwardRef } from "react"; + +import { cn } from "@/lib/utils"; + +const searchInputWrapperVariants = cva("relative flex items-center w-full", { + variants: { + size: { + default: "", + sm: "", + lg: "", + }, + }, + defaultVariants: { + size: "default", + }, +}); + +const searchInputVariants = cva( + "flex w-full rounded-lg border text-sm transition-all outline-none placeholder:text-text-neutral-tertiary disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + variant: { + default: + "border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1", + ghost: + "border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary", + }, + size: { + default: "h-10 pl-10 pr-10 py-3", + sm: "h-8 pl-8 pr-8 py-2 text-xs", + lg: "h-12 pl-12 pr-12 py-4", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +const iconSizeMap = { + default: 16, + sm: 14, + lg: 20, +} as const; + +const iconPositionMap = { + default: "left-3", + sm: "left-2.5", + lg: "left-4", +} as const; + +const clearButtonPositionMap = { + default: "right-3", + sm: "right-2.5", + lg: "right-4", +} as const; + +export interface SearchInputProps + extends Omit, "size">, + VariantProps { + onClear?: () => void; +} + +const SearchInput = forwardRef( + ( + { + className, + variant, + size = "default", + value, + onClear, + placeholder = "Search...", + ...props + }, + ref, + ) => { + const iconSize = iconSizeMap[size || "default"]; + const iconPosition = iconPositionMap[size || "default"]; + const clearButtonPosition = clearButtonPositionMap[size || "default"]; + const hasValue = value && String(value).length > 0; + + return ( +
+ + + {hasValue && onClear && ( + + )} +
+ ); + }, +); + +SearchInput.displayName = "SearchInput"; + +export { SearchInput, searchInputVariants };