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 && (
(
- <>
-
-
-
+
+
+ {
+ 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}
/>
-
+
+
(
-
+
{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 && (
+
+
+
+ Select All
+
+
+ )}
+
+ {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}
+ >
+
+ {option.icon && (
+ {option.icon}
+ )}
+
+ {option.label}
+ {option.description && (
+
+ {option.description}
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {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 = ({
/>
-
+
(
- {
- const selectedKey = Array.from(selected).pop();
- field.onChange(selectedKey || "");
- }}
- >
- {roles.map((role: { id: string; name: string }) => (
- {role.name}
- ))}
+
+
+
+
+
+ {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