diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 3f9937ac04..ae3d1bfbba 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixed +- Dropdown selects in the "Send to Jira" modal and other dialogs not responding to clicks [(#10097)](https://github.com/prowler-cloud/prowler/pull/10097) - Update credentials for the Alibaba Cloud provider [(#10098)](https://github.com/prowler-cloud/prowler/pull/10098) --- diff --git a/ui/components/filters/custom-account-selection.tsx b/ui/components/filters/custom-account-selection.tsx deleted file mode 100644 index 04c95c03b7..0000000000 --- a/ui/components/filters/custom-account-selection.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; -import { Select, SelectItem } from "@heroui/select"; - -const accounts = [ - { key: "audit-test-1", label: "740350143844" }, - { key: "audit-test-2", label: "890837126756" }, - { key: "audit-test-3", label: "563829104923" }, - { key: "audit-test-4", label: "678943217543" }, - { key: "audit-test-5", label: "932187465320" }, - { key: "audit-test-6", label: "492837106587" }, - { key: "audit-test-7", label: "812736459201" }, - { key: "audit-test-8", label: "374829106524" }, - { key: "audit-test-9", label: "926481053298" }, - { key: "audit-test-10", label: "748192364579" }, - { key: "audit-test-11", label: "501374829106" }, -]; -export const CustomAccountSelection = () => { - return ( - - ); -}; diff --git a/ui/components/filters/custom-region-selection.tsx b/ui/components/filters/custom-region-selection.tsx deleted file mode 100644 index a242e8f664..0000000000 --- a/ui/components/filters/custom-region-selection.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { Select, SelectItem } from "@heroui/select"; -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useCallback, useMemo } from "react"; - -export const CustomRegionSelection: React.FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const region = "none"; - // Memoize selected keys based on the URL - const selectedKeys = useMemo(() => { - const params = searchParams.get("filter[regions]"); - return params ? params.split(",") : []; - }, [searchParams]); - - const applyRegionFilter = useCallback( - (values: string[]) => { - const params = new URLSearchParams(searchParams.toString()); - if (values.length > 0) { - params.set("filter[regions]", values.join(",")); - } else { - params.delete("filter[regions]"); - } - router.push(`?${params.toString()}`, { scroll: false }); - }, - [router, searchParams], - ); - - return ( - - ); -}; diff --git a/ui/components/filters/custom-select-provider.tsx b/ui/components/filters/custom-select-provider.tsx deleted file mode 100644 index bab26272cb..0000000000 --- a/ui/components/filters/custom-select-provider.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client"; - -import { Select, SelectItem } from "@heroui/select"; -import { useRouter, useSearchParams } from "next/navigation"; -import { ReactElement } from "react"; - -import { PROVIDER_TYPES, ProviderType } from "@/types/providers"; - -import { - CustomProviderInputAlibabaCloud, - CustomProviderInputAWS, - CustomProviderInputAzure, - CustomProviderInputGCP, - CustomProviderInputGitHub, - CustomProviderInputIac, - CustomProviderInputKubernetes, - CustomProviderInputM365, - CustomProviderInputMongoDBAtlas, - CustomProviderInputOracleCloud, -} from "./custom-provider-inputs"; - -const providerDisplayData: Record< - ProviderType, - { label: string; component: ReactElement } -> = { - aws: { - label: "Amazon Web Services", - component: , - }, - azure: { - label: "Microsoft Azure", - component: , - }, - gcp: { - label: "Google Cloud Platform", - component: , - }, - github: { - label: "GitHub", - component: , - }, - iac: { - label: "Infrastructure as Code", - component: , - }, - kubernetes: { - label: "Kubernetes", - component: , - }, - m365: { - label: "Microsoft 365", - component: , - }, - mongodbatlas: { - label: "MongoDB Atlas", - component: , - }, - oraclecloud: { - label: "Oracle Cloud Infrastructure", - component: , - }, - alibabacloud: { - label: "Alibaba Cloud", - component: , - }, -}; - -const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({ - key: providerType, - label: providerDisplayData[providerType].label, - value: providerDisplayData[providerType].component, -})); - -export const CustomSelectProvider = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const applyProviderFilter = (value: string) => { - const params = new URLSearchParams(searchParams.toString()); - if (value) { - params.set("filter[provider_type]", value); - } else { - params.delete("filter[provider_type]"); - } - router.push(`?${params.toString()}`, { scroll: false }); - }; - - const currentProvider = searchParams.get("filter[provider_type]") || ""; - - const selectedKeys = dataInputsProvider.some( - (provider) => provider.key === currentProvider, - ) - ? [currentProvider] - : []; - - return ( - - ); -}; diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index e8df5c30be..2ee5d24bfa 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -3,30 +3,15 @@ import { FilterOption } from "@/types"; import { DataTableFilterCustom } from "../ui/table"; -import { CustomAccountSelection } from "./custom-account-selection"; -import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings"; -import { CustomDatePicker } from "./custom-date-picker"; -import { CustomRegionSelection } from "./custom-region-selection"; import { CustomSearchInput } from "./custom-search-input"; -import { CustomSelectProvider } from "./custom-select-provider"; export interface FilterControlsProps { search?: boolean; - providers?: boolean; - date?: boolean; - regions?: boolean; - accounts?: boolean; - mutedFindings?: boolean; customFilters?: FilterOption[]; } export const FilterControls = ({ search = false, - providers = false, - date = false, - regions = false, - accounts = false, - mutedFindings = false, customFilters, }: FilterControlsProps) => { return ( @@ -34,11 +19,6 @@ export const FilterControls = ({
{search && } - {providers && } - {date && } - {regions && } - {accounts && } - {mutedFindings && }
{customFilters && customFilters.length > 0 && ( diff --git a/ui/components/filters/index.ts b/ui/components/filters/index.ts index 5d9f577590..7e2de31cd7 100644 --- a/ui/components/filters/index.ts +++ b/ui/components/filters/index.ts @@ -1,9 +1,6 @@ export * from "./clear-filters-button"; -export * from "./custom-account-selection"; export * from "./custom-checkbox-muted-findings"; export * from "./custom-date-picker"; export * from "./custom-provider-inputs"; -export * from "./custom-region-selection"; -export * from "./custom-select-provider"; export * from "./data-filters"; export * from "./filter-controls"; diff --git a/ui/components/findings/send-to-jira-modal.tsx b/ui/components/findings/send-to-jira-modal.tsx index a692fe20b7..d5ee2d36fc 100644 --- a/ui/components/findings/send-to-jira-modal.tsx +++ b/ui/components/findings/send-to-jira-modal.tsx @@ -1,10 +1,7 @@ "use client"; -import { Input } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; -import type { Selection } from "@react-types/shared"; -import { Search, Send } from "lucide-react"; +import { Send } from "lucide-react"; import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -14,16 +11,11 @@ import { pollJiraDispatchTask, sendFindingToJira, } from "@/actions/integrations/jira-dispatch"; -import { JiraIcon } from "@/components/icons/services/IconServices"; import { Modal } from "@/components/shadcn/modal"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomBanner } from "@/components/ui/custom/custom-banner"; -import { - Form, - FormControl, - FormField, - FormMessage, -} from "@/components/ui/form"; +import { Form, FormField, FormMessage } from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { IntegrationProps } from "@/types/integrations"; @@ -42,15 +34,6 @@ const sendToJiraSchema = z.object({ type SendToJiraFormData = z.infer; -const selectorClassNames = { - trigger: "min-h-12", - popoverContent: "bg-bg-neutral-secondary", - listboxWrapper: "max-h-[300px] bg-bg-neutral-secondary", - listbox: "gap-0", - label: "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!", - value: "text-text-neutral-secondary text-small", -}; - // The commented code is related to issue types, which are not required for the first implementation, but will be used in the future export const SendToJiraModal = ({ isOpen, @@ -61,8 +44,6 @@ export const SendToJiraModal = ({ const { toast } = useToast(); const [integrations, setIntegrations] = useState([]); const [isFetchingIntegrations, setIsFetchingIntegrations] = useState(false); - const [searchProjectValue, setSearchProjectValue] = useState(""); - // const [searchIssueTypeValue, setSearchIssueTypeValue] = useState(""); const form = useForm({ resolver: zodResolver(sendToJiraSchema), @@ -75,18 +56,11 @@ export const SendToJiraModal = ({ }); const selectedIntegration = form.watch("integration"); - // const selectedProject = form.watch("project"); const hasConnectedIntegration = integrations.some( (i) => i.attributes.connected === true, ); - const getSelectedValue = (keys: Selection): string => { - if (keys === "all") return ""; - const first = Array.from(keys)[0]; - return first !== null ? String(first) : ""; - }; - const setOpenForFormButtons: Dispatch> = (value) => { const next = typeof value === "function" ? value(isOpen) : value; onOpenChange(next); @@ -129,8 +103,6 @@ export const SendToJiraModal = ({ } else { // Reset form when modal closes form.reset(); - setSearchProjectValue(""); - // setSearchIssueTypeValue(""); } }, [isOpen, form, toast]); @@ -187,32 +159,16 @@ export const SendToJiraModal = ({ ({} as Record); const projectEntries = Object.entries(projects); - const shouldShowProjectSearch = projectEntries.length > 5; - // const issueTypes: string[] = - // selectedIntegrationData?.attributes.configuration.issue_types || - // ([] as string[]); - // Filter projects based on search - const filteredProjects = (() => { - if (!searchProjectValue) return projectEntries; + const integrationOptions = integrations.map((integration) => ({ + value: integration.id, + label: integration.attributes.configuration.domain || integration.id, + })); - const lowerSearch = searchProjectValue.toLowerCase(); - return projectEntries.filter( - ([key, name]) => - key.toLowerCase().includes(lowerSearch) || - name.toLowerCase().includes(lowerSearch), - ); - })(); - - // Filter issue types based on search - // const filteredIssueTypes = useMemo(() => { - // if (!searchIssueTypeValue) return issueTypes; - - // const lowerSearch = searchIssueTypeValue.toLowerCase(); - // return issueTypes.filter((type) => - // type.toLowerCase().includes(lowerSearch), - // ); - // }, [issueTypes, searchIssueTypeValue]); + const projectOptions = projectEntries.map(([key, name]) => ({ + value: key, + label: `${key} - ${name}`, + })); return ( ( - <> - - - +
+ + { + const selectedValue = values.at(-1) ?? ""; + field.onChange(selectedValue); + // Reset dependent fields + form.setValue("project", ""); + form.setValue("issueType", "Task"); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Select a Jira integration" + searchable={true} + emptyIndicator="No integrations found." + disabled={isFetchingIntegrations} + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> - +
)} /> )} - {/* Project Selection - Enhanced Style */} - {selectedIntegration && Object.keys(projects).length > 0 && ( + {/* Project Selection */} + {selectedIntegration && projectEntries.length > 0 && ( ( - <> - - } - value={searchProjectValue} - onValueChange={setSearchProjectValue} - onClear={() => setSearchProjectValue("")} - classNames={{ - inputWrapper: - "border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary", - input: "text-small", - clearButton: "text-default-400", - }} - /> - - ) : null, - }} - > - {filteredProjects.map(([key, name]) => ( - -
-
-
-
- - {key} - - - - - - - {name} - -
-
-
-
-
- ))} - -
+
+ + { + const selectedValue = values.at(-1) ?? ""; + field.onChange(selectedValue); + // Keep issue type defaulting to Task when project changes + form.setValue("issueType", "Task"); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Select a Jira project" + searchable={true} + emptyIndicator="No projects found." + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> - +
)} /> )} diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx index c99d833cbc..a04341b4d2 100644 --- a/ui/components/findings/table/provider-icon-cell.tsx +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -12,7 +12,7 @@ import { } from "@/components/icons/providers-badge"; import { ProviderType } from "@/types"; -const PROVIDER_ICONS = { +export const PROVIDER_ICONS = { aws: AWSProviderBadge, azure: AzureProviderBadge, gcp: GCPProviderBadge, diff --git a/ui/components/integrations/index.ts b/ui/components/integrations/index.ts index d46942d57c..223887ad53 100644 --- a/ui/components/integrations/index.ts +++ b/ui/components/integrations/index.ts @@ -1,4 +1,3 @@ -export * from "../providers/enhanced-provider-selector"; export * from "./api-key/api-key-link-card"; export * from "./jira/jira-integration-card"; export * from "./jira/jira-integration-form"; diff --git a/ui/components/integrations/s3/s3-integration-form.tsx b/ui/components/integrations/s3/s3-integration-form.tsx index 2570441b29..7b3af4b018 100644 --- a/ui/components/integrations/s3/s3-integration-form.tsx +++ b/ui/components/integrations/s3/s3-integration-form.tsx @@ -8,12 +8,18 @@ import { useState } from "react"; import { Control, useForm } from "react-hook-form"; import { createIntegration, updateIntegration } from "@/actions/integrations"; -import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; +import { PROVIDER_ICONS } from "@/components/findings/table/provider-icon-cell"; import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { Form } from "@/components/ui/form"; +import { + Form, + FormControl, + FormField, + FormMessage, +} from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { getAWSCredentialsTemplateLinks } from "@/lib"; import { AWSCredentialsRole } from "@/types"; @@ -272,18 +278,40 @@ export const S3IntegrationForm = ({ // Show configuration step (step 0 or editing configuration) if (isEditingConfig || currentStep === 0) { + const providerOptions = providers.map((provider) => { + const Icon = PROVIDER_ICONS[provider.attributes.provider]; + return { + value: provider.id, + label: provider.attributes.alias || provider.attributes.uid, + icon: Icon ? : undefined, + description: provider.attributes.connection.connected + ? "Connected" + : "Disconnected", + }; + }); + return ( <> {/* Provider Selection */}
- ( + <> + + + + + + )} />
diff --git a/ui/components/integrations/security-hub/security-hub-integration-form.tsx b/ui/components/integrations/security-hub/security-hub-integration-form.tsx index 1acfa31809..8a62f62457 100644 --- a/ui/components/integrations/security-hub/security-hub-integration-form.tsx +++ b/ui/components/integrations/security-hub/security-hub-integration-form.tsx @@ -6,15 +6,21 @@ import { Radio, RadioGroup } from "@heroui/radio"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { useSession } from "next-auth/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Control, useForm } from "react-hook-form"; import { createIntegration, updateIntegration } from "@/actions/integrations"; -import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; +import { PROVIDER_ICONS } from "@/components/findings/table/provider-icon-cell"; import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { Form, FormControl, FormField } from "@/components/ui/form"; +import { + Form, + FormControl, + FormField, + FormMessage, +} from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { getAWSCredentialsTemplateLinks } from "@/lib"; import { AWSCredentialsRole } from "@/types"; @@ -52,7 +58,7 @@ export const SecurityHubIntegrationForm = ({ const isEditingConfig = editMode === "configuration"; const isEditingCredentials = editMode === "credentials"; - const disabledProviderIds = useMemo(() => { + const disabledProviderIds = (() => { // When editing, no providers should be disabled since we're not changing it if (isEditing) { return []; @@ -69,7 +75,7 @@ export const SecurityHubIntegrationForm = ({ }); return usedProviderIds; - }, [isEditing, existingIntegrations]); + })(); const form = useForm({ resolver: zodResolver( @@ -107,6 +113,26 @@ export const SecurityHubIntegrationForm = ({ const providerIdValue = form.watch("provider_id"); const hasErrors = !!form.formState.errors.provider_id || !providerIdValue; + const providerOptions = providers + .filter((provider) => provider.attributes.provider === "aws") + .map((provider) => { + const isDisabled = disabledProviderIds.includes(provider.id); + const connectionLabel = provider.attributes.connection.connected + ? "Connected" + : "Disconnected"; + + const Icon = PROVIDER_ICONS[provider.attributes.provider]; + return { + value: provider.id, + label: provider.attributes.alias || provider.attributes.uid, + icon: Icon ? : undefined, + description: isDisabled + ? `${connectionLabel} (Already in use)` + : connectionLabel, + disabled: isDisabled, + }; + }); + useEffect(() => { if (!useCustomCredentials && isCreating) { setCurrentStep(0); @@ -325,17 +351,29 @@ export const SecurityHubIntegrationForm = ({ {!isEditingConfig && ( <>
- ( + <> + + { + field.onChange(values.at(-1) ?? ""); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Search and select an AWS provider" + searchable={true} + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> + + + + )} />
diff --git a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx index b3b4d965ad..a1bd977b1b 100644 --- a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx +++ b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx @@ -1,6 +1,5 @@ "use client"; -import { Chip } from "@heroui/chip"; import { format } from "date-fns"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; @@ -16,7 +15,7 @@ import { IntegrationCardHeader, IntegrationSkeleton, } from "@/components/integrations/shared"; -import { Button } from "@/components/shadcn"; +import { Badge, Button } from "@/components/shadcn"; import { Modal } from "@/components/shadcn/modal"; import { useToast } from "@/components/ui"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; @@ -385,14 +384,13 @@ export const SecurityHubIntegrationsManager = ({ {enabledRegions.length > 0 && (
{enabledRegions.map((region) => ( - {region} - + ))}
)} diff --git a/ui/components/integrations/shared/integration-card-header.tsx b/ui/components/integrations/shared/integration-card-header.tsx index 73f9173e61..ab0a252585 100644 --- a/ui/components/integrations/shared/integration-card-header.tsx +++ b/ui/components/integrations/shared/integration-card-header.tsx @@ -1,23 +1,18 @@ "use client"; -import { Chip } from "@heroui/chip"; import { ExternalLink } from "lucide-react"; import { ReactNode } from "react"; +import { Badge } from "@/components/shadcn"; +import { cn } from "@/lib/utils"; + interface IntegrationCardHeaderProps { icon: ReactNode; title: string; subtitle?: string; chips?: Array<{ label: string; - color?: - | "default" - | "primary" - | "secondary" - | "success" - | "warning" - | "danger"; - variant?: "solid" | "bordered" | "light" | "flat" | "faded" | "shadow"; + className?: string; }>; connectionStatus?: { connected: boolean; @@ -63,25 +58,30 @@ export const IntegrationCardHeader = ({ {(chips.length > 0 || connectionStatus) && (
{chips.map((chip, index) => ( - {chip.label} - + ))} {connectionStatus && ( - {connectionStatus.label || (connectionStatus.connected ? "Connected" : "Disconnected")} - + )}
)} diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx index d087f34729..57f0717313 100644 --- a/ui/components/invitations/forms/edit-form.tsx +++ b/ui/components/invitations/forms/edit-form.tsx @@ -1,4 +1,3 @@ -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { MailIcon, ShieldIcon } from "lucide-react"; import { Dispatch, SetStateAction } from "react"; @@ -6,6 +5,13 @@ import { Controller, useForm } from "react-hook-form"; import * as z from "zod"; import { updateInvite } from "@/actions/invitations/invitation"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; @@ -126,27 +132,25 @@ export const EditForm = ({ isRequired={false} /> -
+
+ ( - + + + + + {roles.map((role) => ( + + {role.name} + + ))} + )} /> diff --git a/ui/components/invitations/workflow/forms/send-invitation-form.tsx b/ui/components/invitations/workflow/forms/send-invitation-form.tsx index ea1cb19935..843885d772 100644 --- a/ui/components/invitations/workflow/forms/send-invitation-form.tsx +++ b/ui/components/invitations/workflow/forms/send-invitation-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { SaveIcon } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -9,6 +8,13 @@ import * as z from "zod"; import { sendInvite } from "@/actions/invitations/invitation"; import { Button } from "@/components/shadcn"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; @@ -111,35 +117,33 @@ export const SendInvitationForm = ({ name="roleId" control={form.control} render={({ field }) => ( - <> +
{form.formState.errors.roleId && (

{form.formState.errors.roleId.message}

)} - +
)} /> diff --git a/ui/components/manage-groups/forms/add-group-form.tsx b/ui/components/manage-groups/forms/add-group-form.tsx index fa03f1f4eb..394d3ae55d 100644 --- a/ui/components/manage-groups/forms/add-group-form.tsx +++ b/ui/components/manage-groups/forms/add-group-form.tsx @@ -7,8 +7,9 @@ import * as z from "zod"; import { createProviderGroup } from "@/actions/manage-groups"; import { Button } from "@/components/shadcn"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { ApiError } from "@/types"; @@ -39,6 +40,14 @@ export const AddGroupForm = ({ }); const isLoading = form.formState.isSubmitting; + const providerOptions = providers.map((provider) => ({ + label: provider.name, + value: provider.id, + })); + const roleOptions = roles.map((role) => ({ + label: role.name, + value: role.id, + })); const onSubmitClient = async (values: FormValues) => { try { @@ -128,15 +137,19 @@ export const AddGroupForm = ({ name="providers" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ +
)} /> {form.formState.errors.providers && ( @@ -155,15 +168,19 @@ export const AddGroupForm = ({ name="roles" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ +
)} /> {form.formState.errors.roles && ( diff --git a/ui/components/manage-groups/forms/edit-group-form.tsx b/ui/components/manage-groups/forms/edit-group-form.tsx index 781a502e5b..a29a054e8b 100644 --- a/ui/components/manage-groups/forms/edit-group-form.tsx +++ b/ui/components/manage-groups/forms/edit-group-form.tsx @@ -9,8 +9,9 @@ import * as z from "zod"; import { updateProviderGroup } from "@/actions/manage-groups/manage-groups"; import { Button } from "@/components/shadcn"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { ApiError } from "@/types"; @@ -176,18 +177,29 @@ export const EditGroupForm = ({ ]; return ( - p.id) || []} - onChange={(name, selectedValues) => { - const selectedProviders = combinedProviders.filter( - (provider) => selectedValues.includes(provider.id), - ); - field.onChange(selectedProviders); - }} - /> +
+ ({ + label: provider.name, + value: provider.id, + }))} + onValueChange={(selectedValues) => { + const selectedProviders = combinedProviders.filter( + (provider) => selectedValues.includes(provider.id), + ); + field.onChange(selectedProviders); + }} + defaultValue={ + field.value?.map((provider) => provider.id) || [] + } + placeholder="Select providers" + aria-label="Select providers" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
); }} /> @@ -216,18 +228,27 @@ export const EditGroupForm = ({ ]; return ( - r.id) || []} - onChange={(name, selectedValues) => { - const selectedRoles = combinedRoles.filter((role) => - selectedValues.includes(role.id), - ); - field.onChange(selectedRoles); - }} - /> +
+ ({ + label: role.name, + value: role.id, + }))} + onValueChange={(selectedValues) => { + const selectedRoles = combinedRoles.filter((role) => + selectedValues.includes(role.id), + ); + field.onChange(selectedRoles); + }} + defaultValue={field.value?.map((role) => role.id) || []} + placeholder="Select roles" + aria-label="Select roles" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
); }} /> diff --git a/ui/components/providers/enhanced-provider-selector.tsx b/ui/components/providers/enhanced-provider-selector.tsx deleted file mode 100644 index 94f543b2d0..0000000000 --- a/ui/components/providers/enhanced-provider-selector.tsx +++ /dev/null @@ -1,307 +0,0 @@ -"use client"; - -import { Input } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; -import { SharedSelection } from "@heroui/system"; -import { CheckSquare, Search, Square } from "lucide-react"; -import { useState } from "react"; -import { Control, FieldValues, Path } from "react-hook-form"; - -import { Button } from "@/components/shadcn"; -import { FormControl, FormField, FormMessage } from "@/components/ui/form"; -import { ProviderProps, ProviderType } from "@/types/providers"; - -const providerTypeLabels: Record = { - aws: "Amazon Web Services", - gcp: "Google Cloud Platform", - azure: "Microsoft Azure", - m365: "Microsoft 365", - kubernetes: "Kubernetes", - github: "GitHub", - iac: "Infrastructure as Code", - oraclecloud: "Oracle Cloud Infrastructure", - mongodbatlas: "MongoDB Atlas", - alibabacloud: "Alibaba Cloud", -}; - -interface EnhancedProviderSelectorProps { - control: Control; - name: Path; - providers: ProviderProps[]; - label?: string; - placeholder?: string; - isInvalid?: boolean; - showFormMessage?: boolean; - selectionMode?: "single" | "multiple"; - providerType?: ProviderType; - enableSearch?: boolean; - disabledProviderIds?: string[]; -} - -export const EnhancedProviderSelector = ({ - control, - name, - providers, - label = "Provider", - placeholder = "Select provider", - isInvalid = false, - showFormMessage = true, - selectionMode = "single", - providerType, - enableSearch = false, - disabledProviderIds = [], -}: EnhancedProviderSelectorProps) => { - const [searchValue, setSearchValue] = useState(""); - - const filteredProviders = (() => { - let filtered = providers; - - // Filter by provider type if specified - if (providerType) { - filtered = filtered.filter((p) => p.attributes.provider === providerType); - } - - // Filter by search value - if (searchValue && enableSearch) { - const lowerSearch = searchValue.toLowerCase(); - filtered = filtered.filter((p) => { - const displayName = p.attributes.alias || p.attributes.uid; - const typeLabel = providerTypeLabels[p.attributes.provider]; - return ( - displayName.toLowerCase().includes(lowerSearch) || - typeLabel.toLowerCase().includes(lowerSearch) - ); - }); - } - - // Sort providers - return filtered.sort((a, b) => { - const typeComparison = a.attributes.provider.localeCompare( - b.attributes.provider, - ); - if (typeComparison !== 0) return typeComparison; - - const nameA = a.attributes.alias || a.attributes.uid; - const nameB = b.attributes.alias || b.attributes.uid; - return nameA.localeCompare(nameB); - }); - })(); - - return ( - { - const isMultiple = selectionMode === "multiple"; - const selectedIds: string[] = isMultiple - ? (value as string[] | undefined) || [] - : value - ? [value as string] - : []; - const allProviderIds = filteredProviders - .filter((p) => !disabledProviderIds.includes(p.id)) - .map((p) => p.id); - const isAllSelected = - isMultiple && - allProviderIds.length > 0 && - allProviderIds.every((id) => selectedIds.includes(id)); - - const handleSelectAll = () => { - if (isAllSelected) { - onChange([]); - } else { - onChange(allProviderIds); - } - }; - - const handleSelectionChange = (keys: SharedSelection) => { - if (keys === "all") { - onChange(allProviderIds); - return; - } - if (isMultiple) { - const selectedArray = Array.from(keys).map(String); - onChange(selectedArray); - } else { - const selectedValue = Array.from(keys)[0]; - onChange(selectedValue ? String(selectedValue) : ""); - } - }; - - return ( - <> - -
- {isMultiple && filteredProviders.length > 1 && ( -
- - {label} - - -
- )} - } - value={searchValue} - onValueChange={setSearchValue} - onClear={() => setSearchValue("")} - classNames={{ - inputWrapper: - "border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary", - input: "text-small", - clearButton: "text-default-400", - }} - /> -
- ) : null, - }} - > - {filteredProviders.map((provider) => { - const providerType = provider.attributes.provider; - const displayName = - provider.attributes.alias || provider.attributes.uid; - const typeLabel = providerTypeLabels[providerType]; - const isDisabled = disabledProviderIds.includes( - provider.id, - ); - - return ( - -
-
-
-
- {displayName} -
-
- {typeLabel} - {isDisabled && ( - - (Already used) - - )} -
-
-
-
-
-
-
- - ); - })} - -
-
- {showFormMessage && ( - - )} - - ); - }} - /> - ); -}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx index ad38a761c0..1347b1d47d 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx @@ -1,11 +1,17 @@ import { Chip } from "@heroui/chip"; import { Divider } from "@heroui/divider"; -import { Select, SelectItem } from "@heroui/select"; import { Switch } from "@heroui/switch"; import { useEffect, useState } from "react"; import { Control, UseFormSetValue, useWatch } from "react-hook-form"; import { CredentialsRoleHelper } from "@/components/providers/workflow"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { CustomInput } from "@/components/ui/custom"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { AWSCredentialsRole } from "@/types"; @@ -77,47 +83,47 @@ export const AWSRoleCredentialsForm = ({ Specify which AWS credentials to use - { + setValue( + ProviderCredentialFields.CREDENTIALS_TYPE, + value as "aws-sdk-default" | "access-secret-key", + ); + }} > -
- - {isCloudEnv - ? "Prowler Cloud will assume your IAM role" - : "AWS SDK Default"} - - {isCloudEnv && ( - - Recommended - - )} -
- - -
- Access & Secret Key -
-
- + + + + + +
+ + {isCloudEnv + ? "Prowler Cloud will assume your IAM role" + : "AWS SDK Default"} + + {isCloudEnv && ( + + Recommended + + )} +
+
+ +
+ Access & Secret Key +
+
+
+ +
{credentialsType === "access-secret-key" && ( <> diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx index 56c61308fd..9ff4772540 100644 --- a/ui/components/roles/workflow/forms/add-role-form.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.tsx @@ -12,8 +12,9 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { addRole } from "@/actions/roles/roles"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; import { getErrorMessage, permissionFormFields } from "@/lib"; import { addRoleFormSchema, ApiError } from "@/types"; @@ -232,15 +233,21 @@ export const AddRoleForm = ({ name="groups" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ ({ + label: group.name, + value: group.id, + }))} + onValueChange={field.onChange} + defaultValue={field.value || []} + placeholder="Select groups" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
)} /> {form.formState.errors.groups && ( diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx index ab8fc5b567..76afe290a0 100644 --- a/ui/components/roles/workflow/forms/edit-role-form.tsx +++ b/ui/components/roles/workflow/forms/edit-role-form.tsx @@ -12,8 +12,9 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { updateRole } from "@/actions/roles/roles"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; import { getErrorMessage, permissionFormFields } from "@/lib"; import { ApiError, editRoleFormSchema } from "@/types"; @@ -250,15 +251,21 @@ export const EditRoleForm = ({ name="groups" control={form.control} render={({ field }) => ( - { - field.onChange(selectedValues); - }} - /> +
+ ({ + label: group.name, + value: group.id, + }))} + onValueChange={field.onChange} + defaultValue={field.value || []} + placeholder="Select groups" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
)} /> diff --git a/ui/components/shadcn/badge/badge.tsx b/ui/components/shadcn/badge/badge.tsx index 59b863a541..f0b8306f4a 100644 --- a/ui/components/shadcn/badge/badge.tsx +++ b/ui/components/shadcn/badge/badge.tsx @@ -17,6 +17,7 @@ const badgeVariants = cva( "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + tag: "bg-bg-tag border-border-tag text-text-neutral-primary", }, }, defaultVariants: { diff --git a/ui/components/shadcn/command.tsx b/ui/components/shadcn/command.tsx index d848db5c27..4a3f672dc4 100644 --- a/ui/components/shadcn/command.tsx +++ b/ui/components/shadcn/command.tsx @@ -66,7 +66,7 @@ function CommandInput({ return (
) { - return ; + return ; } function DrawerTrigger({ diff --git a/ui/components/shadcn/dropdown/action-dropdown.tsx b/ui/components/shadcn/dropdown/action-dropdown.tsx index fe44da2757..3eeb7dc385 100644 --- a/ui/components/shadcn/dropdown/action-dropdown.tsx +++ b/ui/components/shadcn/dropdown/action-dropdown.tsx @@ -81,8 +81,9 @@ export function ActionDropdownItem({ return ( ) { +}: React.ComponentProps & { + container?: HTMLElement | null; +}) { return ( - + void; + defaultValue?: string[]; + placeholder?: string; + searchable?: boolean; + hideSelectAll?: boolean; + maxCount?: number; + closeOnSelect?: boolean; + resetOnDefaultValueChange?: boolean; + emptyIndicator?: ReactNode; + disabled?: boolean; + className?: string; + id?: string; + "aria-label"?: string; +} + +function arraysEqual(a: string[], b: string[]): boolean { + if (a.length !== b.length) return false; + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); + return sortedA.every((val, index) => val === sortedB[index]); +} + +export function EnhancedMultiSelect({ + options, + onValueChange, + defaultValue = [], + placeholder = "Select options", + searchable = true, + hideSelectAll = false, + maxCount = 3, + closeOnSelect = false, + resetOnDefaultValueChange = true, + emptyIndicator, + disabled = false, + className, + id, + "aria-label": ariaLabel, +}: EnhancedMultiSelectProps) { + const [selectedValues, setSelectedValues] = useState(defaultValue); + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const [portalContainer, setPortalContainer] = useState( + null, + ); + + const buttonRef = useRef(null); + const prevDefaultValueRef = useRef(defaultValue); + const selectedAtOpenRef = useRef(selectedValues); + const multiSelectId = useId(); + const listboxId = `${multiSelectId}-listbox`; + + // Detect dialog container for portal stacking (critical for Jira modal) + useEffect(() => { + if (!buttonRef.current) return; + const closestDialogContainer = buttonRef.current.closest( + "[data-slot='dialog-content'], [data-slot='modal-content'], [role='dialog']", + ); + setPortalContainer( + closestDialogContainer instanceof HTMLElement + ? closestDialogContainer + : null, + ); + }, []); + + // Reset when defaultValue changes externally (e.g. React Hook Form reset) + useEffect(() => { + if (!resetOnDefaultValueChange) return; + const prev = prevDefaultValueRef.current; + if (!arraysEqual(prev, defaultValue)) { + if (!arraysEqual(selectedValues, defaultValue)) { + setSelectedValues(defaultValue); + } + prevDefaultValueRef.current = [...defaultValue]; + } + }, [defaultValue, selectedValues, resetOnDefaultValueChange]); + + function handleOpenChange(nextOpen: boolean) { + if (nextOpen) { + selectedAtOpenRef.current = [...selectedValues]; + } else { + setSearch(""); + } + setOpen(nextOpen); + } + + const enabledOptions = options.filter((o) => !o.disabled); + + const filteredOptions = ( + searchable && search + ? options.filter( + (o) => + o.label.toLowerCase().includes(search.toLowerCase()) || + o.value.toLowerCase().includes(search.toLowerCase()), + ) + : options + ).toSorted((a, b) => { + const snapshot = selectedAtOpenRef.current; + const aSelected = snapshot.includes(a.value) ? 0 : 1; + const bSelected = snapshot.includes(b.value) ? 0 : 1; + return aSelected - bSelected; + }); + + function getOptionByValue(value: string) { + return options.find((o) => o.value === value); + } + + function toggleOption(value: string) { + if (disabled) return; + const option = getOptionByValue(value); + if (option?.disabled) return; + const next = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + setSelectedValues(next); + onValueChange(next); + if (closeOnSelect) setOpen(false); + } + + function toggleAll() { + if (disabled) return; + if (selectedValues.length === enabledOptions.length) { + handleClear(); + } else { + const all = enabledOptions.map((o) => o.value); + setSelectedValues(all); + onValueChange(all); + } + if (closeOnSelect) setOpen(false); + } + + function handleClear() { + if (disabled) return; + setSelectedValues([]); + onValueChange([]); + } + + return ( + + + + + setOpen(false)} + > + + {searchable && ( + + )} + + {emptyIndicator || "No results found."} + {!hideSelectAll && !search && ( + + + + + )} + + {filteredOptions.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + toggleOption(option.value)} + role="option" + aria-selected={isSelected} + aria-disabled={option.disabled} + className={cn( + "cursor-pointer", + option.disabled && "cursor-not-allowed opacity-50", + )} + disabled={option.disabled} + > + + ); + })} + + + +
+ {selectedValues.length > 0 && ( + <> + + + + )} + +
+
+
+
+ ); +} + +EnhancedMultiSelect.displayName = "EnhancedMultiSelect"; +export type { EnhancedMultiSelectProps, MultiSelectOption }; diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx index 8d1b6512ba..58b9b86035 100644 --- a/ui/components/shadcn/select/multiselect.tsx +++ b/ui/components/shadcn/select/multiselect.tsx @@ -224,9 +224,9 @@ export function MultiSelectValue({ .filter((value) => items.has(value)) .map((value) => ( {items.get(value)} {clickToRemove && ( - + )} ))} @@ -247,9 +247,9 @@ export function MultiSelectValue({ style={{ display: overflowAmount > 0 && !shouldWrap ? "block" : "none", }} - variant="outline" + variant="tag" ref={overflowRef} - className="text-bg-button-secondary border-slate-300 bg-slate-100 px-2 py-1 text-xs font-medium dark:border-slate-600 dark:bg-slate-800" + className="px-2 py-1 text-xs font-medium" > +{overflowAmount} diff --git a/ui/components/ui/custom/custom-dropdown-selection.tsx b/ui/components/ui/custom/custom-dropdown-selection.tsx deleted file mode 100644 index 4f7e3d6bf0..0000000000 --- a/ui/components/ui/custom/custom-dropdown-selection.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import React, { useCallback } from "react"; - -import { - MultiSelect, - MultiSelectContent, - MultiSelectItem, - MultiSelectTrigger, - MultiSelectValue, -} from "@/components/shadcn/select/multiselect"; - -interface CustomDropdownSelectionProps { - label: string; - name: string; - values: { id: string; name: string }[]; - onChange: (name: string, selectedValues: string[]) => void; - selectedKeys?: string[]; -} - -export const CustomDropdownSelection: React.FC< - CustomDropdownSelectionProps -> = ({ label, name, values, onChange, selectedKeys = [] }) => { - const handleValuesChange = useCallback( - (newValues: string[]) => { - onChange(name, newValues); - }, - [name, onChange], - ); - - return ( -
-

{label}

- - - - - - {values.map((item) => ( - - {item.name} - - ))} - - -
- ); -}; diff --git a/ui/components/ui/custom/index.ts b/ui/components/ui/custom/index.ts index 0007e08580..6314862ea7 100644 --- a/ui/components/ui/custom/index.ts +++ b/ui/components/ui/custom/index.ts @@ -1,5 +1,4 @@ export * from "./custom-banner"; -export * from "./custom-dropdown-selection"; export * from "./custom-input"; export * from "./custom-link"; export * from "./custom-modal-buttons"; diff --git a/ui/components/users/forms/edit-form.tsx b/ui/components/users/forms/edit-form.tsx index 19cef75624..26b94f518d 100644 --- a/ui/components/users/forms/edit-form.tsx +++ b/ui/components/users/forms/edit-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { ShieldIcon, UserIcon } from "lucide-react"; import { Dispatch, SetStateAction } from "react"; @@ -9,6 +8,13 @@ import * as z from "zod"; import { updateUser, updateUserRole } from "@/actions/users/users"; import { Card } from "@/components/shadcn"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; @@ -168,29 +174,22 @@ export const EditForm = ({ />
-
+
( - + + + + + {roles.map((role: { id: string; name: string }) => ( + + {role.name} + + ))} + )} /> diff --git a/ui/styles/globals.css b/ui/styles/globals.css index e44046cc48..daad4a6985 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -396,3 +396,10 @@ @apply bg-background text-foreground; } } + +/* Override vaul's injected user-select: none to allow text selection in drawers */ +@media (hover: hover) and (pointer: fine) { + [data-vaul-drawer][data-vaul-drawer] { + user-select: text; + } +} diff --git a/ui/tests/invitations/invitations-page.ts b/ui/tests/invitations/invitations-page.ts index 2a5b2f6826..72d6d4fdc9 100644 --- a/ui/tests/invitations/invitations-page.ts +++ b/ui/tests/invitations/invitations-page.ts @@ -34,7 +34,7 @@ export class InvitationsPage extends BasePage { this.emailInput = page.getByRole("textbox", { name: "Email" }); // Form select - this.roleSelect = page.getByRole("button", { name: /Role|Select a role/i }); + this.roleSelect = page.getByRole("combobox", { name: /Role|Select a role/i }); // Form details this.reviewInvitationDetailsButton = page.getByRole('button', { name: /Review Invitation Details/i }); diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 696db0db19..b96caa8362 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -1149,19 +1149,16 @@ export class ProvidersPage extends BasePage { } async selectAuthenticationMethod(method: AWSCredentialType): Promise { - // Select the authentication method + // Select the authentication method (shadcn Select renders as combobox + listbox) - const button = this.page.locator("button").filter({ + const trigger = this.page.locator('[role="combobox"]').filter({ hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i, }); - await button.click(); + await trigger.click(); - const modal = this.page - .locator('[role="dialog"], .modal, [data-testid*="modal"]') - .first(); - - await expect(modal).toBeVisible({ timeout: 10000 }); + const listbox = this.page.getByRole("listbox"); + await expect(listbox).toBeVisible({ timeout: 10000 }); if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) { await this.page