Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4f5fc36
feat(api): add provider_id filters to ResourceFilter
alejandrobailo Jan 21, 2026
282506b
feat(ui): add client-side pagination and search to DataTable
alejandrobailo Jan 21, 2026
d596a6f
feat(ui): restyle resources view with improved table and filters
alejandrobailo Jan 21, 2026
8065f69
feat(ui): add filter transition for smoother table updates
alejandrobailo Jan 22, 2026
d4e1818
refactor(ui): increase search debounce to 500ms
alejandrobailo Jan 22, 2026
b06604a
fix(ui): improve sidebar menu item active state detection
alejandrobailo Jan 22, 2026
5133102
fix(ui): enable nested modals inside resource detail drawer
alejandrobailo Jan 22, 2026
c4305cd
feat(api): add provider_id filters to LatestResourceFilter
alejandrobailo Jan 22, 2026
c478de5
test(api): add tests for LatestResourceFilter provider_id filters
alejandrobailo Jan 22, 2026
4266a22
refactor(ui): migrate CodeSnippet from HeroUI to shadcn pattern
alejandrobailo Jan 22, 2026
934c42e
refactor(ui): migrate resource-detail from HeroUI to shadcn
alejandrobailo Jan 22, 2026
d4afbc2
fix(ui): change default pagination page size to 10
alejandrobailo Jan 22, 2026
9fb3f12
refactor(ui): add paramPrefix support to DataTable components
alejandrobailo Jan 22, 2026
f52b4c0
refactor(ui): migrate resource-detail findings to server-side pagination
alejandrobailo Jan 22, 2026
9a8a4e4
style(ui): format menu-item and use-url-filters
alejandrobailo Jan 22, 2026
a2b2124
style(ui): remove gap from time range selector buttons
alejandrobailo Jan 22, 2026
9a9fef6
chore: CHANGELOG updated
alejandrobailo Jan 22, 2026
2b6768b
fix(ui): resolve linting and formatting issues
alejandrobailo Jan 22, 2026
7d3d528
chore: update CHANGELOG entries
alejandrobailo Jan 22, 2026
9bdf800
docs(api): add provider_id filters to resources endpoints
alejandrobailo Jan 22, 2026
48aa960
Merge branch 'master' into feat/PROWLER-740-resource-view-restyling
alejandrobailo Jan 22, 2026
31eb9a5
chore(api): mark v1.19.0 as unreleased
alejandrobailo Jan 22, 2026
da4a725
chore(ui): bump CHANGELOG to v1.18.0
alejandrobailo Jan 22, 2026
451278b
fix(api): restore OpenAPI tags section
alejandrobailo Jan 23, 2026
db7abd1
fix(api): use existing uuid4 import in test
alejandrobailo Jan 23, 2026
078e0ae
refactor(ui): remove unused provider props from resources
alejandrobailo Jan 23, 2026
d8498bf
feat(ui): add controlled mode to DataTable components
alejandrobailo Jan 23, 2026
3fa93e4
fix(ui): improve resource detail drawer behavior
alejandrobailo Jan 23, 2026
d44fa07
feat(ui): add initial loading spinner to findings tab in resource drawer
alejandrobailo Jan 23, 2026
713eef3
refactor(ui): migrate dropdown and modal to shadcn components
alejandrobailo Jan 23, 2026
75973d4
feat(ui): add shadcn Modal and ActionDropdown components
alejandrobailo Jan 23, 2026
7affdb5
refactor(ui): migrate CustomAlertModal to shadcn Modal
alejandrobailo Jan 23, 2026
8b75bb9
refactor(ui): migrate findings row actions to ActionDropdown
alejandrobailo Jan 23, 2026
238e831
feat(ui): add delta/muted column to resource findings table
alejandrobailo Jan 23, 2026
7154de1
Merge branch 'master' into feat/PROWLER-740-resource-view-restyling
alejandrobailo Jan 23, 2026
8792a60
fix: build conflicts
alejandrobailo Jan 23, 2026
e991a9f
fix: useActionState remplaced by controlled client state to render th…
alejandrobailo Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to the **Prowler API** are documented in this file.

## [1.19.0] (Prowler UNRELEASED)

### Added

- `provider_id` and `provider_id__in` filters for resources endpoints (`GET /resources` and `GET /resources/metadata/latest`) [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864)

### 🔄 Changed

- Lazy-load providers and compliance data to reduce API/worker startup memory and time [(#9857)](https://github.com/prowler-cloud/prowler/pull/9857)
Expand Down
4 changes: 4 additions & 0 deletions api/src/backend/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ class Meta:


class ResourceFilter(ProviderRelationshipFilterSet):
provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
tag_key = CharFilter(method="filter_tag_key")
tag_value = CharFilter(method="filter_tag_value")
tag = CharFilter(method="filter_tag")
Expand Down Expand Up @@ -540,6 +542,8 @@ def filter_tag(self, queryset, name, value):


class LatestResourceFilter(ProviderRelationshipFilterSet):
provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
tag_key = CharFilter(method="filter_tag_key")
tag_value = CharFilter(method="filter_tag_value")
tag = CharFilter(method="filter_tag")
Expand Down
60 changes: 60 additions & 0 deletions api/src/backend/api/specs/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8186,6 +8186,21 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
- in: query
name: filter[provider_id__in]
schema:
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
Expand Down Expand Up @@ -8588,6 +8603,21 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
- in: query
name: filter[provider_id__in]
schema:
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
Expand Down Expand Up @@ -8884,6 +8914,21 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
- in: query
name: filter[provider_id__in]
schema:
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
Expand Down Expand Up @@ -9186,6 +9231,21 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
- in: query
name: filter[provider_id__in]
schema:
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
Expand Down
102 changes: 102 additions & 0 deletions api/src/backend/api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4353,6 +4353,108 @@ def test_resources_metadata_latest(
assert attributes["types"] == [latest_scan_resource.type]
assert "groups" in attributes

def test_resources_latest_filter_by_provider_id(
self, authenticated_client, latest_scan_resource
):
"""Test that provider_id filter works on latest resources endpoint."""
provider = latest_scan_resource.provider
response = authenticated_client.get(
reverse("resource-latest"),
{"filter[provider_id]": str(provider.id)},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 1
assert (
response.json()["data"][0]["attributes"]["uid"] == latest_scan_resource.uid
)

def test_resources_latest_filter_by_provider_id_in(
self, authenticated_client, latest_scan_resource
):
"""Test that provider_id__in filter works on latest resources endpoint."""
provider = latest_scan_resource.provider
response = authenticated_client.get(
reverse("resource-latest"),
{"filter[provider_id__in]": str(provider.id)},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 1
assert (
response.json()["data"][0]["attributes"]["uid"] == latest_scan_resource.uid
)

def test_resources_latest_filter_by_provider_id_in_multiple(
self, authenticated_client, providers_fixture
):
"""Test that provider_id__in filter works with multiple provider IDs."""
provider1, provider2 = providers_fixture[0], providers_fixture[1]
tenant_id = str(provider1.tenant_id)

# Create completed scans for both providers
scan1 = Scan.objects.create(
name="scan for provider 1",
provider=provider1,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant_id,
)
scan2 = Scan.objects.create(
name="scan for provider 2",
provider=provider2,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant_id,
)

# Create resources for each provider
resource1 = Resource.objects.create(
tenant_id=tenant_id,
provider=provider1,
uid="resource_provider_1",
name="Resource Provider 1",
region="us-east-1",
service="ec2",
type="instance",
)
resource2 = Resource.objects.create(
tenant_id=tenant_id,
provider=provider2,
uid="resource_provider_2",
name="Resource Provider 2",
region="us-west-2",
service="s3",
type="bucket",
)

# Test filtering by both providers
response = authenticated_client.get(
reverse("resource-latest"),
{"filter[provider_id__in]": f"{provider1.id},{provider2.id}"},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2

# Test filtering by single provider returns only that provider's resource
response = authenticated_client.get(
reverse("resource-latest"),
{"filter[provider_id__in]": str(provider1.id)},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 1
assert response.json()["data"][0]["attributes"]["uid"] == resource1.uid

def test_resources_latest_filter_by_provider_id_no_match(
self, authenticated_client, latest_scan_resource
):
"""Test that provider_id filter returns empty when no match."""
non_existent_id = str(uuid4())
response = authenticated_client.get(
reverse("resource-latest"),
{"filter[provider_id]": non_existent_id},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0


@pytest.mark.django_db
class TestFindingViewSet:
Expand Down
8 changes: 8 additions & 0 deletions ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to the **Prowler UI** are documented in this file.

## [1.18.0] (Prowler v5.18.0 UNRELEASED)

### 🔄 Changed

- Restyle resources view with improved resource detail drawer [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864)

---

## [1.17.0] (Prowler v5.17.0)

### 🚀 Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const TimeRangeSelector = ({
isLoading = false,
}: TimeRangeSelectorProps) => {
return (
<div className="border-border-neutral-tertiary bg-bg-neutral-tertiary inline-flex items-center gap-2 rounded-full border">
<div className="border-border-neutral-tertiary bg-bg-neutral-tertiary inline-flex items-center rounded-full border">
{Object.entries(TIME_RANGE_OPTIONS).map(([key, range]) => (
<button
key={key}
Expand Down
43 changes: 22 additions & 21 deletions ui/app/(prowler)/findings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SkeletonTableFindings,
} from "@/components/findings/table";
import { ContentLayout } from "@/components/ui";
import { FilterTransitionWrapper } from "@/contexts";
import {
createDict,
createScanDetailsMapping,
Expand All @@ -36,8 +37,7 @@ export default async function Findings({
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const { searchParamsKey, encodedSort } =
extractSortAndKey(resolvedSearchParams);
const { encodedSort } = extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);

// Check if the searchParams contain any date or scan filter
Expand Down Expand Up @@ -100,7 +100,6 @@ export default async function Findings({
const resourceId = finding.relationships?.resources?.data?.[0]?.id;
const scan = scanId ? scanDict[scanId] : undefined;
const providerId = scan?.relationships?.provider?.data?.id;

const resource = resourceId ? resourceDict[resourceId] : undefined;
const provider = providerId ? providerDict[providerId] : undefined;

Expand Down Expand Up @@ -148,24 +147,26 @@ export default async function Findings({

return (
<ContentLayout title="Findings" icon="lucide:tag">
<div className="mb-6">
<FindingsFilters
providers={providersData?.data || []}
providerIds={providerIds}
providerDetails={providerDetails}
completedScans={completedScans || []}
completedScanIds={completedScanIds}
scanDetails={scanDetails}
uniqueRegions={uniqueRegions}
uniqueServices={uniqueServices}
uniqueResourceTypes={uniqueResourceTypes}
uniqueCategories={uniqueCategories}
uniqueGroups={uniqueGroups}
/>
</div>
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={resolvedSearchParams} />
</Suspense>
<FilterTransitionWrapper>
<div className="mb-6">
<FindingsFilters
providers={providersData?.data || []}
providerIds={providerIds}
providerDetails={providerDetails}
completedScans={completedScans || []}
completedScanIds={completedScanIds}
scanDetails={scanDetails}
uniqueRegions={uniqueRegions}
uniqueServices={uniqueServices}
uniqueResourceTypes={uniqueResourceTypes}
uniqueCategories={uniqueCategories}
uniqueGroups={uniqueGroups}
/>
</div>
<Suspense fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={resolvedSearchParams} />
</Suspense>
</FilterTransitionWrapper>
{processedFinding && <FindingDetailsSheet finding={processedFinding} />}
</ContentLayout>
);
Expand Down
8 changes: 4 additions & 4 deletions ui/app/(prowler)/lighthouse/config/(connect-llm)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
import { DeleteLLMProviderForm } from "@/components/lighthouse/forms/delete-llm-provider-form";
import { WorkflowConnectLLM } from "@/components/lighthouse/workflow";
import { Button } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { NavigationHeader } from "@/components/ui";
import { CustomAlertModal } from "@/components/ui/custom";
import type { LighthouseProvider } from "@/types/lighthouse";

interface ConnectLLMLayoutProps {
Expand Down Expand Up @@ -63,8 +63,8 @@ export default function ConnectLLMLayout({ children }: ConnectLLMLayoutProps) {

return (
<>
<CustomAlertModal
isOpen={isDeleteOpen}
<Modal
open={isDeleteOpen}
onOpenChange={setIsDeleteOpen}
title="Are you absolutely sure?"
description="This action cannot be undone. This will permanently delete your LLM provider configuration and remove your data from the server."
Expand All @@ -73,7 +73,7 @@ export default function ConnectLLMLayout({ children }: ConnectLLMLayoutProps) {
providerType={provider}
setIsOpen={setIsDeleteOpen}
/>
</CustomAlertModal>
</Modal>

<NavigationHeader
title={isEditMode ? "Configure LLM Provider" : "Connect LLM Provider"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
updateMutedFindingsConfig,
} from "@/actions/processors";
import { Button, Card, Skeleton } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { useToast } from "@/components/ui";
import { CustomAlertModal } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { fontMono } from "@/config/fonts";
import {
Expand Down Expand Up @@ -152,8 +152,8 @@ export function AdvancedMutelistForm() {
return (
<>
{/* Delete Confirmation Modal */}
<CustomAlertModal
isOpen={showDeleteConfirmation}
<Modal
open={showDeleteConfirmation}
onOpenChange={setShowDeleteConfirmation}
title="Delete Mutelist Configuration"
size="md"
Expand Down Expand Up @@ -185,7 +185,7 @@ export function AdvancedMutelistForm() {
</Button>
</div>
</div>
</CustomAlertModal>
</Modal>

<Card variant="base" className="p-6">
<form action={formAction} className="flex flex-col gap-4">
Expand Down
Loading
Loading