Compare commits

...

8 Commits

Author SHA1 Message Date
Prowler Bot 2fdc480beb fix(ui): fix role cancel and select dropdown scroll (#11128)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-05-12 13:21:18 +02:00
Prowler Bot 8bfc1d85f5 chore(changelog): prepare changelog for v5.26.1 (#11130)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-05-12 13:18:44 +02:00
Prowler Bot 57501e1864 fix(api): defer scan broker publish until transaction commits (#11123)
Co-authored-by: Adrián Peña <adrianjpr@gmail.com>
2026-05-12 12:12:40 +02:00
Prowler Bot 02a83adfd4 fix(m365): exclude disabled guest users from entra_users_mfa_capable (#11119)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-05-12 08:50:44 +01:00
Prowler Bot 98a1bca403 chore(api): Bump version to v1.27.1 (#11114)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-11 15:36:28 +02:00
Prowler Bot 8aade7f024 chore(sdk): Bump version to v5.26.1 (#11111)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-11 15:36:17 +02:00
Prowler Bot 43b50c4d6f chore(ui): Bump version to v5.26.1 (#11113)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-11 15:35:40 +02:00
Prowler Bot 578c354a69 chore(api): Update prowler dependency to v5.26 for release 5.26.0 (#11106)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-11 13:23:19 +02:00
19 changed files with 371 additions and 43 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.26.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.26.1
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+8
View File
@@ -2,6 +2,14 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.27.1] (Prowler v5.26.1)
### 🐞 Fixed
- `POST /api/v1/scans` was intermittently failing with `Scan matching query does not exist` in the `scan-perform` worker; the Celery task is now published via `transaction.on_commit` so the worker cannot read the Scan before the dispatch-wide transaction commits [(#11122)](https://github.com/prowler-cloud/prowler/pull/11122)
---
## [1.27.0] (Prowler v5.26.0)
### 🚀 Added
+3 -3
View File
@@ -6754,8 +6754,8 @@ uuid6 = "2024.7.10"
[package.source]
type = "git"
url = "https://github.com/prowler-cloud/prowler.git"
reference = "master"
resolved_reference = "16798e293da365965120961e6539e3a9756564f9"
reference = "v5.26"
resolved_reference = "02cdcb29dbcd8eb5ed442c1cd03830000324fb0f"
[[package]]
name = "psutil"
@@ -9424,4 +9424,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
content-hash = "a3ab982d11a87d951ff15694d2ca7fd51f1f51a451abb0baa067ccf6966367a8"
content-hash = "24f7a92f6c72a8207ab15f75c813a5a244c018afb0a582a5abf8c96e2c7faf12"
+2 -2
View File
@@ -25,7 +25,7 @@ dependencies = [
"defusedxml==0.7.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.26",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -50,7 +50,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.27.0"
version = "1.27.1"
[project.scripts]
celery = "src.backend.config.settings.celery"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.27.0
version: 1.27.1
description: |-
Prowler API specification.
+38 -19
View File
@@ -4,6 +4,7 @@ import json
import logging
import os
import time
import uuid
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@@ -16,7 +17,7 @@ from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
from celery import chain
from celery import chain, states
from celery.result import AsyncResult
from config.custom_logging import BackendLogger
from config.env import env
@@ -60,6 +61,7 @@ from django.utils.dateparse import parse_date
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django_celery_beat.models import PeriodicTask
from django_celery_results.models import TaskResult
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
@@ -422,7 +424,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.27.0"
spectacular_settings.VERSION = "1.27.1"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -2534,28 +2536,45 @@ class ScanViewSet(BaseRLSViewSet):
def create(self, request, *args, **kwargs):
input_serializer = self.get_serializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
# Broker publish is deferred to on_commit so the worker cannot read
# Scan before BaseRLSViewSet's dispatch-wide atomic commits.
pre_task_id = str(uuid.uuid4())
with transaction.atomic():
scan = input_serializer.save()
with transaction.atomic():
task = perform_scan_task.apply_async(
kwargs={
"tenant_id": self.request.tenant_id,
"scan_id": str(scan.id),
"provider_id": str(scan.provider_id),
# Disabled for now
# checks_to_execute=scan.scanner_args.get("checks_to_execute")
},
scan.task_id = pre_task_id
scan.save(update_fields=["task_id"])
attack_paths_db_utils.create_attack_paths_scan(
tenant_id=self.request.tenant_id,
scan_id=str(scan.id),
provider_id=str(scan.provider_id),
)
attack_paths_db_utils.create_attack_paths_scan(
tenant_id=self.request.tenant_id,
scan_id=str(scan.id),
provider_id=str(scan.provider_id),
)
task_result, _ = TaskResult.objects.get_or_create(
task_id=pre_task_id,
defaults={"status": states.PENDING, "task_name": "scan-perform"},
)
prowler_task, _ = Task.objects.update_or_create(
id=pre_task_id,
tenant_id=self.request.tenant_id,
defaults={"task_runner_task": task_result},
)
prowler_task = Task.objects.get(id=task.id)
scan.task_id = task.id
scan.save(update_fields=["task_id"])
scan_kwargs = {
"tenant_id": self.request.tenant_id,
"scan_id": str(scan.id),
"provider_id": str(scan.provider_id),
# Disabled for now
# checks_to_execute=scan.scanner_args.get("checks_to_execute")
}
transaction.on_commit(
lambda: perform_scan_task.apply_async(
kwargs=scan_kwargs, task_id=pre_task_id
)
)
self.response_serializer_class = TaskSerializer
output_serializer = self.get_serializer(prowler_task)
+8
View File
@@ -2,6 +2,14 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.26.1] (Prowler v5.26.1)
### 🐞 Fixed
- `entra_users_mfa_capable` no longer flags disabled guest users by requesting `accountEnabled` and `userType` from Microsoft Graph via `$select` and using Graph as the source of truth for `account_enabled` (EXO `Get-User` does not return guest users) [(#11002)](https://github.com/prowler-cloud/prowler/pull/11002)
---
## [5.26.0] (Prowler v5.26.0)
### 🚀 Added
+1 -1
View File
@@ -48,7 +48,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.26.0"
prowler_version = "5.26.1"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
@@ -5,10 +5,12 @@ from enum import Enum
from typing import Dict, List, Optional
from uuid import UUID
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import (
RunHuntingQueryPostRequestBody,
)
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
from pydantic.v1 import BaseModel, validator
from prowler.lib.logger import logger
@@ -806,7 +808,29 @@ class Entra(M365Service):
logger.info("Entra - Getting users...")
users = {}
try:
users_response = await self.client.users.get()
# Microsoft Graph's /users endpoint omits accountEnabled, userType and
# onPremisesSyncEnabled from the default property set, so we must request
# them explicitly via $select. Without this, disabled guest users surface
# as account_enabled=True (Pydantic default) and user_type=None, which
# bypasses the guest/disabled filters in checks like
# entra_users_mfa_capable (CIS 5.2.3.4). See issue #10921.
query_parameters = (
UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=[
"id",
"displayName",
"userType",
"accountEnabled",
"onPremisesSyncEnabled",
],
)
)
request_configuration = RequestConfiguration(
query_parameters=query_parameters,
)
users_response = await self.client.users.get(
request_configuration=request_configuration,
)
directory_roles = await self.client.directory_roles.get()
async def fetch_role_members(directory_role):
@@ -830,6 +854,19 @@ class Entra(M365Service):
while users_response:
for user in getattr(users_response, "value", []) or []:
reg_info = registration_details.get(user.id, {})
# Prefer Microsoft Graph as the source of truth for
# accountEnabled: it covers every directory user including
# guests, whereas EXO's Get-User only returns mail-enabled
# accounts and silently drops disabled guests. Fall back to
# the EXO PowerShell value only when Graph does not return a
# value (e.g. older tenants or permission-restricted reads).
graph_account_enabled = getattr(user, "account_enabled", None)
if graph_account_enabled is None:
account_enabled = not self.user_accounts_status.get(
user.id, {}
).get("AccountDisabled", False)
else:
account_enabled = bool(graph_account_enabled)
users[user.id] = User(
id=user.id,
name=user.display_name,
@@ -838,9 +875,7 @@ class Entra(M365Service):
),
directory_roles_ids=user_roles_map.get(user.id, []),
is_mfa_capable=reg_info.get("is_mfa_capable", False),
account_enabled=not self.user_accounts_status.get(
user.id, {}
).get("AccountDisabled", False),
account_enabled=account_enabled,
authentication_methods=reg_info.get(
"authentication_methods", []
),
+1 -1
View File
@@ -95,7 +95,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">=3.10,<3.13"
version = "5.26.0"
version = "5.26.1"
[project.scripts]
prowler = "prowler.__main__:prowler"
@@ -521,10 +521,26 @@ class Test_Entra_Service:
assert len(users) == 6
assert users_builder.get.await_count == 1
assert users_builder.get.await_args.kwargs == {}
# The Graph users.get() call must request accountEnabled, userType and
# onPremisesSyncEnabled via $select. They are not part of the default
# property set, and omitting them causes disabled guest users to leak
# into checks like entra_users_mfa_capable (issue #10921).
request_configuration = users_builder.get.await_args.kwargs[
"request_configuration"
]
assert set(request_configuration.query_parameters.select) == {
"id",
"displayName",
"userType",
"accountEnabled",
"onPremisesSyncEnabled",
}
with_url_mock.assert_called_once_with("next-link")
assert users["user-1"].directory_roles_ids == ["role-template-1"]
assert users["user-6"].directory_roles_ids == ["role-template-1"]
# When Graph does not return accountEnabled (legacy SimpleNamespace
# fixtures) we still honour the EXO PowerShell fallback for backwards
# compatibility.
assert users["user-6"].account_enabled is False
assert users["user-1"].is_mfa_capable is True
assert users["user-2"].is_mfa_capable is False
@@ -532,6 +548,81 @@ class Test_Entra_Service:
assert users["user-6"].authentication_methods == ["mobilePhone"]
assert users["user-2"].authentication_methods == []
def test__get_users_uses_graph_account_enabled_for_disabled_guests(self):
"""Regression test for https://github.com/prowler-cloud/prowler/issues/10921.
Disabled guest users do not appear in EXO's ``Get-User`` output, so the
previous code resolved their ``account_enabled`` from the EXO map,
defaulted it to ``True`` and surfaced them as failing findings in
``entra_users_mfa_capable``. The Graph ``accountEnabled`` value must be
used as the source of truth so disabled guests are excluded.
"""
entra_service = Entra.__new__(Entra)
# Empty EXO map mirrors the production scenario where the disabled guest
# is absent from Get-User results.
entra_service.user_accounts_status = {}
graph_users = [
SimpleNamespace(
id="member-1",
display_name="Member User",
on_premises_sync_enabled=False,
account_enabled=True,
user_type="Member",
),
SimpleNamespace(
id="guest-1",
display_name="Disabled Guest",
on_premises_sync_enabled=False,
account_enabled=False,
user_type="Guest",
),
SimpleNamespace(
id="guest-2",
display_name="Enabled Guest",
on_premises_sync_enabled=False,
account_enabled=True,
user_type="Guest",
),
]
users_response = SimpleNamespace(
value=graph_users,
odata_next_link=None,
)
users_builder = SimpleNamespace(
get=AsyncMock(return_value=users_response),
with_url=MagicMock(),
)
directory_roles_builder = SimpleNamespace(
get=AsyncMock(return_value=SimpleNamespace(value=[])),
by_directory_role_id=MagicMock(),
)
registration_details_builder = SimpleNamespace(
get=AsyncMock(return_value=SimpleNamespace(value=[], odata_next_link=None)),
with_url=MagicMock(),
)
reports_builder = SimpleNamespace(
authentication_methods=SimpleNamespace(
user_registration_details=registration_details_builder
)
)
entra_service.client = SimpleNamespace(
users=users_builder,
directory_roles=directory_roles_builder,
reports=reports_builder,
)
users = asyncio.run(entra_service._get_users())
assert len(users) == 3
assert users["member-1"].account_enabled is True
assert users["member-1"].user_type == "Member"
assert users["guest-1"].account_enabled is False
assert users["guest-1"].user_type == "Guest"
assert users["guest-2"].account_enabled is True
assert users["guest-2"].user_type == "Guest"
def test__get_user_registration_details_handles_pagination(self):
entra_service = Entra.__new__(Entra)
+9
View File
@@ -2,6 +2,15 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.26.1] (Prowler 5.26.1)
### 🐞 Fixed
- Role form Cancel buttons now return to Roles [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125)
- Shared select dropdowns stay constrained and scrollable inside modals [(#11125)](https://github.com/prowler-cloud/prowler/pull/11125)
---
## [1.26.0] (Prowler v5.26.0)
### 🚀 Added
@@ -1,10 +1,15 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AddRoleForm } from "./add-role-form";
const routerMocks = vi.hoisted(() => ({
push: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
useRouter: () => routerMocks,
}));
vi.mock("@/actions/roles/roles", () => ({
@@ -73,6 +78,7 @@ vi.mock("@/components/ui", () => ({
describe("AddRoleForm", () => {
afterEach(() => {
routerMocks.push.mockClear();
vi.unstubAllEnvs();
});
@@ -99,4 +105,16 @@ describe("AddRoleForm", () => {
expect(screen.queryByText("Manage Alerts")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Billing")).not.toBeInTheDocument();
});
it("navigates back to roles when cancel is clicked", async () => {
// Given
const user = userEvent.setup();
render(<AddRoleForm groups={[]} />);
// When
await user.click(screen.getByRole("button", { name: /cancel/i }));
// Then
expect(routerMocks.push).toHaveBeenCalledWith("/roles");
});
});
@@ -252,7 +252,11 @@ export const AddRoleForm = ({
)}
</div>
)}
<FormButtons submitText="Add Role" isDisabled={isLoading} />
<FormButtons
submitText="Add Role"
isDisabled={isLoading}
onCancel={() => router.push("/roles")}
/>
</form>
</Form>
);
@@ -271,7 +271,11 @@ export const EditRoleForm = ({
)}
</div>
)}
<FormButtons submitText="Update Role" isDisabled={isLoading} />
<FormButtons
submitText="Update Role"
isDisabled={isLoading}
onCancel={() => router.push("/roles")}
/>
</form>
</Form>
);
@@ -239,6 +239,46 @@ describe("MultiSelect", () => {
expect(screen.getByRole("dialog")).toHaveClass("max-w-[24rem]");
});
it("keeps long option lists scrollable inside the dropdown", async () => {
// Given
const user = userEvent.setup();
render(
<MultiSelect values={[]} onValuesChange={() => {}}>
<MultiSelectTrigger>
<MultiSelectValue placeholder="Select accounts" />
</MultiSelectTrigger>
<MultiSelectContent search={false}>
{Array.from({ length: 20 }, (_, index) => (
<MultiSelectItem key={index} value={`account-${index}`}>
Account {index}
</MultiSelectItem>
))}
</MultiSelectContent>
</MultiSelect>,
);
// When
await user.click(screen.getByRole("combobox"));
// Then
const list = screen
.getByRole("dialog")
.querySelector('[data-slot="command-list"]');
expect(screen.getByRole("dialog")).toHaveStyle({
maxHeight:
"min(360px, var(--radix-popover-content-available-height, 360px))",
});
expect(list).toHaveClass("minimal-scrollbar");
expect(list).toHaveStyle({
maxHeight:
"min(300px, var(--radix-popover-content-available-height, 300px))",
});
expect(list).toHaveClass("overflow-y-auto");
expect(list).toHaveClass("overscroll-contain");
});
it("keeps the legacy clear-all behavior by default", async () => {
const user = userEvent.setup();
const onValuesChange = vi.fn();
+17 -3
View File
@@ -10,6 +10,7 @@ import {
useEffect,
useRef,
useState,
type WheelEvent,
} from "react";
import { Badge } from "@/components/shadcn/badge/badge";
@@ -49,6 +50,10 @@ type MultiSelectContextType = {
};
const MultiSelectContext = createContext<MultiSelectContextType | null>(null);
const stopWheelPropagation = (event: WheelEvent<HTMLElement>) => {
event.stopPropagation();
};
export function MultiSelect({
children,
values,
@@ -335,12 +340,16 @@ export function MultiSelectContent({
<PopoverContent
align="start"
data-slot="multiselect-content"
style={{
maxHeight:
"min(360px, var(--radix-popover-content-available-height, 360px))",
}}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 rounded-lg border p-0",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 overflow-hidden rounded-lg border p-0",
widthClasses,
)}
>
<Command {...props} className="rounded-lg">
<Command {...props} className="max-h-[inherit] rounded-lg">
{canSearch ? (
<CommandInput
placeholder={
@@ -354,7 +363,12 @@ export function MultiSelectContent({
)}
<CommandList
ref={listRef}
className="minimal-scrollbar max-h-[300px] overflow-x-hidden overflow-y-auto p-3"
onWheelCapture={stopWheelPropagation}
style={{
maxHeight:
"min(300px, var(--radix-popover-content-available-height, 300px))",
}}
className="minimal-scrollbar overflow-x-hidden overflow-y-auto overscroll-contain p-3"
>
{canSearch && (
<CommandEmpty className="text-bg-button-secondary py-6 text-center text-sm">
@@ -0,0 +1,63 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./select";
Object.defineProperty(HTMLElement.prototype, "hasPointerCapture", {
writable: true,
configurable: true,
value: () => false,
});
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
writable: true,
configurable: true,
value: () => {},
});
describe("Select", () => {
it("keeps long option lists scrollable inside the dropdown", async () => {
// Given
const user = userEvent.setup();
render(
<Select defaultValue="option-1">
<SelectTrigger aria-label="Options">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 20 }, (_, index) => (
<SelectItem key={index} value={`option-${index}`}>
Option {index}
</SelectItem>
))}
</SelectContent>
</Select>,
);
// When
await user.click(screen.getByRole("combobox", { name: /options/i }));
// Then
const viewport = screen
.getByRole("listbox")
.querySelector('[data-slot="select-viewport"]');
expect(screen.getByRole("listbox")).toHaveStyle({
maxHeight: "var(--radix-select-content-available-height)",
});
expect(viewport).toHaveClass("minimal-scrollbar");
expect(viewport).toHaveStyle({
maxHeight:
"min(300px, var(--radix-select-content-available-height, 300px))",
});
expect(viewport).toHaveClass("overflow-y-auto");
expect(viewport).toHaveClass("overscroll-contain");
});
});
+19 -4
View File
@@ -2,10 +2,14 @@
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { ComponentProps } from "react";
import { ComponentProps, type WheelEvent } from "react";
import { cn } from "@/lib/utils";
const stopWheelPropagation = (event: WheelEvent<HTMLElement>) => {
event.stopPropagation();
};
function Select({
allowDeselect = false,
...props
@@ -83,6 +87,7 @@ function SelectContent({
position = "popper",
align = "start",
width = "default",
style,
...props
}: ComponentProps<typeof SelectPrimitive.Content> & {
width?: "default" | "wide";
@@ -97,22 +102,32 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-border-input-primary bg-bg-input-primary relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-lg border",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
widthClasses,
className,
)}
style={{
maxHeight: "var(--radix-select-content-available-height)",
...style,
}}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-slot="select-viewport"
onWheelCapture={stopWheelPropagation}
style={{
maxHeight:
"min(300px, var(--radix-select-content-available-height, 300px))",
}}
className={cn(
"flex flex-col gap-1 p-3",
"minimal-scrollbar flex flex-col gap-1 overflow-x-hidden overflow-y-auto overscroll-contain p-3",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
"w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}