mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(ui): add search bar when adding a provider (#9634)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<z.infer<typeof addProviderFormSchema>>;
|
||||
isInvalid: boolean;
|
||||
@@ -33,90 +86,90 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
|
||||
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 (
|
||||
<Controller
|
||||
name="providerType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<RadioGroup
|
||||
className="flex flex-wrap"
|
||||
isInvalid={isInvalid}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CustomRadio description="Amazon Web Services" value="aws">
|
||||
<div className="flex items-center">
|
||||
<AWSProviderBadge size={26} />
|
||||
<span className="ml-2">Amazon Web Services</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Google Cloud Platform" value="gcp">
|
||||
<div className="flex items-center">
|
||||
<GCPProviderBadge size={26} />
|
||||
<span className="ml-2">Google Cloud Platform</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Microsoft Azure" value="azure">
|
||||
<div className="flex items-center">
|
||||
<AzureProviderBadge size={26} />
|
||||
<span className="ml-2">Microsoft Azure</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Microsoft 365" value="m365">
|
||||
<div className="flex items-center">
|
||||
<M365ProviderBadge size={26} />
|
||||
<span className="ml-2">Microsoft 365</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="MongoDB Atlas" value="mongodbatlas">
|
||||
<div className="flex items-center">
|
||||
<MongoDBAtlasProviderBadge size={26} />
|
||||
<span className="ml-2">MongoDB Atlas</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Kubernetes" value="kubernetes">
|
||||
<div className="flex items-center">
|
||||
<KS8ProviderBadge size={26} />
|
||||
<span className="ml-2">Kubernetes</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="GitHub" value="github">
|
||||
<div className="flex items-center">
|
||||
<GitHubProviderBadge size={26} />
|
||||
<span className="ml-2">GitHub</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Infrastructure as Code" value="iac">
|
||||
<div className="flex items-center">
|
||||
<IacProviderBadge size={26} />
|
||||
<span className="ml-2">Infrastructure as Code</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio
|
||||
description="Oracle Cloud Infrastructure"
|
||||
value="oraclecloud"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<OracleCloudProviderBadge size={26} />
|
||||
<span className="ml-2">Oracle Cloud Infrastructure</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Alibaba Cloud" value="alibabacloud">
|
||||
<div className="flex items-center">
|
||||
<AlibabaCloudProviderBadge size={26} />
|
||||
<span className="ml-2">Alibaba Cloud</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<div className="flex h-[calc(100vh-200px)] flex-col px-4">
|
||||
<div className="relative z-10 shrink-0 pb-4">
|
||||
<SearchInput
|
||||
aria-label="Search providers"
|
||||
placeholder="Search providers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onClear={() => setSearchTerm("")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="minimal-scrollbar relative flex-1 overflow-y-auto pr-3">
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label="Select a provider"
|
||||
className="flex flex-col gap-3"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
paddingTop: "24px",
|
||||
marginTop: "-24px",
|
||||
}}
|
||||
>
|
||||
{filteredProviders.length > 0 ? (
|
||||
filteredProviders.map((provider) => {
|
||||
const BadgeComponent = provider.badge;
|
||||
const isSelected = field.value === provider.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={provider.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => field.onChange(provider.value)}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-3 rounded-lg border p-4 text-left transition-all",
|
||||
"hover:border-button-primary",
|
||||
"focus-visible:border-button-primary focus-visible:ring-button-primary focus:outline-none focus-visible:ring-1",
|
||||
isSelected
|
||||
? "border-button-primary bg-bg-neutral-tertiary"
|
||||
: "border-border-neutral-secondary bg-bg-neutral-secondary",
|
||||
isInvalid && "border-bg-fail",
|
||||
)}
|
||||
>
|
||||
<BadgeComponent size={26} />
|
||||
<span className="text-text-neutral-primary text-sm font-medium">
|
||||
{provider.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-text-neutral-tertiary py-4 text-sm">
|
||||
No providers found matching "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-text-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
51
ui/components/shadcn/input/input.tsx
Normal file
51
ui/components/shadcn/input/input.tsx
Normal file
@@ -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<ComponentProps<"input">, "size">,
|
||||
VariantProps<typeof inputVariants> {}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, variant, inputSize, type = "text", ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(inputVariants({ variant, inputSize, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input, inputVariants };
|
||||
125
ui/components/shadcn/search-input/search-input.tsx
Normal file
125
ui/components/shadcn/search-input/search-input.tsx
Normal file
@@ -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<ComponentProps<"input">, "size">,
|
||||
VariantProps<typeof searchInputVariants> {
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
(
|
||||
{
|
||||
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 (
|
||||
<div className={cn(searchInputWrapperVariants({ size }))}>
|
||||
<SearchIcon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"text-text-neutral-tertiary pointer-events-none absolute",
|
||||
iconPosition,
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
data-slot="search-input"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
className={cn(searchInputVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
{hasValue && onClear && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear search"
|
||||
onClick={onClear}
|
||||
className={cn(
|
||||
"text-text-neutral-tertiary hover:text-text-neutral-primary absolute transition-colors focus:outline-none",
|
||||
clearButtonPosition,
|
||||
)}
|
||||
>
|
||||
<XCircle size={iconSize} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SearchInput.displayName = "SearchInput";
|
||||
|
||||
export { SearchInput, searchInputVariants };
|
||||
Reference in New Issue
Block a user