Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
58 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
726c0b2
refactor(ui): extract resource findings columns to separate file
alejandrobailo Jan 26, 2026
bf52a76
fix(ui): correct debounce behavior in controlled search mode
alejandrobailo Jan 26, 2026
b46b168
refactor(ui): make DataTableRowActions generic for type safety
alejandrobailo Jan 26, 2026
6eac8bd
Merge branch 'master' into feat/PROWLER-740-resource-view-restyling
alejandrobailo Jan 27, 2026
bc1f12f
feat(ui): add group column and simplify resource table labels
alejandrobailo Jan 27, 2026
7787214
Merge branch 'master' into feat/PROWLER-740-resource-view-restyling
alejandrobailo Jan 27, 2026
7f95f59
Merge branch 'master' into feat/PROWLER-740-resource-view-restyling
alejandrobailo Jan 27, 2026
3112abb
fix(api): restore Added section header in changelog
alejandrobailo Jan 28, 2026
99e9675
fix(ui): prevent provider logo from shrinking on resize
alejandrobailo Jan 28, 2026
934ae6d
refactor(ui): scale CodeSnippet with hideCode, icon, and formatter props
alejandrobailo Jan 28, 2026
f35ddde
refactor(ui): migrate resources table from HeroUI to shadcn
alejandrobailo Jan 28, 2026
7d57125
refactor(ui): migrate EntityInfo from HeroUI to shadcn
alejandrobailo Jan 28, 2026
f906d9b
refactor(ui): replace SnippetChip with CodeSnippet in findings
alejandrobailo Jan 28, 2026
f0f72b5
style(ui): format JSX expressions for readability
alejandrobailo Jan 28, 2026
2443698
style(ui): adjust entityId container padding
alejandrobailo Jan 28, 2026
5f6a604
refactor(ui): redesign resources table skeleton
alejandrobailo Jan 28, 2026
25756d0
fix(ui): prevent horizontal scroll on long resource UIDs
alejandrobailo Jan 28, 2026
ceff264
fix(api): remove unused variable assignments in tests
alejandrobailo Jan 28, 2026
5e15a34
Merge remote-tracking branch 'origin/master' into feat/PROWLER-740-re…
alejandrobailo Jan 28, 2026
ab506a6
fix(auth): preserve callbackUrl during authentication redirect
alejandrobailo Jan 28, 2026
2dbcdc3
test(auth): use goto instead of gotoFresh for callbackUrl test
alejandrobailo Jan 28, 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/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
104 changes: 104 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,110 @@
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."""
import uuid

non_existent_id = str(uuid.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
39 changes: 21 additions & 18 deletions ui/app/(prowler)/findings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
SkeletonTableFindings,
} from "@/components/findings/table";
import { ContentLayout } from "@/components/ui";
import { FilterTransitionWrapper } from "@/contexts";
import {
createDict,
createScanDetailsMapping,
Expand All @@ -36,7 +37,7 @@
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const { searchParamsKey, encodedSort } =

Check failure on line 40 in ui/app/(prowler)/findings/page.tsx

View workflow job for this annotation

GitHub Actions / ui-tests

'searchParamsKey' is assigned a value but never used
extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);

Expand Down Expand Up @@ -148,24 +149,26 @@

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
82 changes: 57 additions & 25 deletions ui/app/(prowler)/resources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
getLatestMetadataInfo,
getLatestResources,
getMetadataInfo,
getResourceById,
getResources,
} from "@/actions/resources";
import { FilterControls } from "@/components/filters";
import { ResourceDetailsSheet } from "@/components/resources/resource-details-sheet";
import { ResourcesFilters } from "@/components/resources/resources-filters";
import { SkeletonTableResources } from "@/components/resources/skeleton/skeleton-table-resources";
import { ColumnResources } from "@/components/resources/table/column-resources";
import { ResourcesTableWithSelection } from "@/components/resources/table";
import { ContentLayout } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { FilterTransitionWrapper } from "@/contexts";
import {
createDict,
extractFiltersAndQuery,
Expand All @@ -32,7 +33,7 @@
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const { searchParamsKey, encodedSort } =

Check failure on line 36 in ui/app/(prowler)/resources/page.tsx

View workflow job for this annotation

GitHub Actions / ui-tests

'searchParamsKey' is assigned a value but never used
extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
const outputFilters = replaceFieldKey(filters, "inserted_at", "updated_at");
Expand All @@ -40,14 +41,42 @@
// Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams);

const [metadataInfoData, providersData] = await Promise.all([
(hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({
query,
filters: outputFilters,
sort: encodedSort,
}),
getProviders({ pageSize: 50 }),
]);
// Check if there's a specific resource ID to fetch
const resourceId = resolvedSearchParams.resourceId?.toString();

const [metadataInfoData, providersData, resourceByIdData] = await Promise.all(
[
(hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({
query,
filters: outputFilters,
sort: encodedSort,
}),
getProviders({ pageSize: 50 }),
resourceId
? getResourceById(resourceId, { include: ["provider"] })
: Promise.resolve(null),
],
);

// Process the resource data to match the expected structure
const processedResource = resourceByIdData?.data
? (() => {
const resource = resourceByIdData.data;
const providerDict = createDict("providers", resourceByIdData);

const provider = {
data: providerDict[resource.relationships?.provider?.data?.id],
};

return {
...resource,
relationships: {
...resource.relationships,
provider,
},
} as ResourceProps;
})()
: null;

// Extract unique regions, services, groups from the metadata endpoint
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
Expand All @@ -62,19 +91,24 @@

return (
<ContentLayout title="Resources" icon="lucide:warehouse">
<FilterControls search date />
<div className="flex flex-col gap-6">
<ResourcesFilters
providerIds={providerIds}
providerDetails={providerDetails}
uniqueRegions={uniqueRegions}
uniqueServices={uniqueServices}
uniqueGroups={uniqueGroups}
/>
<Suspense key={searchParamsKey} fallback={<SkeletonTableResources />}>
<FilterTransitionWrapper>
<div className="mb-6">
<ResourcesFilters
providers={providersData?.data || []}
providerIds={providerIds}
providerDetails={providerDetails}
uniqueRegions={uniqueRegions}
uniqueServices={uniqueServices}
uniqueGroups={uniqueGroups}
/>
</div>
<Suspense fallback={<SkeletonTableResources />}>
<SSRDataTable searchParams={resolvedSearchParams} />
</Suspense>
</div>
</FilterTransitionWrapper>
{processedResource && (
<ResourceDetailsSheet resource={processedResource} />
)}
</ContentLayout>
);
}
Expand Down Expand Up @@ -150,9 +184,7 @@
<p>{resourcesData.errors[0].detail}</p>
</div>
)}
<DataTable
key={`resources-${Date.now()}`}
columns={ColumnResources}
<ResourcesTableWithSelection
data={expandedResources || []}
metadata={resourcesData?.meta}
/>
Expand Down
48 changes: 48 additions & 0 deletions ui/components/resources/resource-details-sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { usePathname, useRouter, useSearchParams } from "next/navigation";

import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { ResourceProps } from "@/types";

import { ResourceDetail } from "./table/resource-detail";

interface ResourceDetailsSheetProps {
resource: ResourceProps;
}

export const ResourceDetailsSheet = ({
resource,
}: ResourceDetailsSheetProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const handleOpenChange = (open: boolean) => {
if (!open) {
const params = new URLSearchParams(searchParams.toString());
params.delete("resourceId");
router.push(`${pathname}?${params.toString()}`, { scroll: false });
}
};

return (
<Sheet open={true} onOpenChange={handleOpenChange}>
<SheetContent className="my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] overflow-y-auto pt-10 md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
<SheetHeader>
<SheetTitle className="sr-only">Resource Details</SheetTitle>
<SheetDescription className="sr-only">
View the resource details
</SheetDescription>
</SheetHeader>
<ResourceDetail resourceDetails={resource} />
</SheetContent>
</Sheet>
);
};
Loading
Loading