1+ import { Info } from "lucide-react" ;
12import { Suspense } from "react" ;
23
34import {
@@ -7,12 +8,14 @@ import {
78import { getThreatScore } from "@/actions/overview" ;
89import { getScans } from "@/actions/scans" ;
910import {
10- ComplianceCard ,
1111 ComplianceSkeletonGrid ,
1212 NoScansAvailable ,
1313 ThreatScoreBadge ,
1414} from "@/components/compliance" ;
15- import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header" ;
15+ import { ComplianceFilters } from "@/components/compliance/compliance-header/compliance-filters" ;
16+ import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overview-grid" ;
17+ import { Alert , AlertDescription } from "@/components/shadcn/alert" ;
18+ import { Card , CardContent } from "@/components/shadcn/card/card" ;
1619import { ContentLayout } from "@/components/ui" ;
1720import {
1821 ExpandedScanData ,
@@ -30,12 +33,6 @@ export default async function Compliance({
3033 const resolvedSearchParams = await searchParams ;
3134 const searchParamsKey = JSON . stringify ( resolvedSearchParams || { } ) ;
3235
33- const filters = Object . fromEntries (
34- Object . entries ( resolvedSearchParams ) . filter ( ( [ key ] ) =>
35- key . startsWith ( "filter[" ) ,
36- ) ,
37- ) ;
38-
3936 const scansData = await getScans ( {
4037 filters : {
4138 "filter[state]" : "completed" ,
@@ -81,7 +78,6 @@ export default async function Compliance({
8178 // Use scanId from URL, or select the first scan if not provided
8279 const selectedScanId =
8380 resolvedSearchParams . scanId || expandedScansData [ 0 ] ?. id || null ;
84- const query = ( filters [ "filter[search]" ] as string ) || "" ;
8581
8682 // Find the selected scan
8783 const selectedScan = expandedScansData . find (
@@ -102,7 +98,6 @@ export default async function Compliance({
10298 // Fetch metadata if we have a selected scan
10399 const metadataInfoData = selectedScanId
104100 ? await getComplianceOverviewMetadataInfo ( {
105- query,
106101 filters : {
107102 "filter[scan_id]" : selectedScanId ,
108103 } ,
@@ -131,28 +126,38 @@ export default async function Compliance({
131126 < ContentLayout title = "Compliance" icon = "lucide:shield-check" >
132127 { selectedScanId ? (
133128 < >
134- < div className = "mb-6 flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between" >
135- < div className = "min-w-0 flex-1" >
136- < ComplianceHeader
137- scans = { expandedScansData }
138- uniqueRegions = { uniqueRegions }
139- />
140- </ div >
141- { threatScoreData &&
142- typeof selectedScanId === "string" &&
143- selectedScan && (
144- < div className = "w-full lg:w-[360px] lg:flex-shrink-0" >
145- < ThreatScoreBadge
146- score = { threatScoreData . score }
147- scanId = { selectedScanId }
148- provider = { selectedScan . providerInfo . provider }
149- selectedScan = { selectedScanData }
150- sectionScores = { threatScoreData . sectionScores }
151- />
152- </ div >
153- ) }
129+ { /* Row 1: Filters */ }
130+ < div className = "mb-6" >
131+ < ComplianceFilters
132+ scans = { expandedScansData }
133+ uniqueRegions = { uniqueRegions }
134+ />
154135 </ div >
155- < Suspense key = { searchParamsKey } fallback = { < ComplianceSkeletonGrid /> } >
136+
137+ { /* Row 2: ThreatScore card — full width, horizontal */ }
138+ { threatScoreData &&
139+ typeof selectedScanId === "string" &&
140+ selectedScan && (
141+ < div className = "mb-6" >
142+ < ThreatScoreBadge
143+ score = { threatScoreData . score }
144+ scanId = { selectedScanId }
145+ provider = { selectedScan . providerInfo . provider }
146+ selectedScan = { selectedScanData }
147+ sectionScores = { threatScoreData . sectionScores }
148+ />
149+ </ div >
150+ ) }
151+
152+ { /* Row 3: Compliance grid with client-side search */ }
153+ < Suspense
154+ key = { searchParamsKey }
155+ fallback = {
156+ < ComplianceOverviewPanel >
157+ < ComplianceSkeletonGrid />
158+ </ ComplianceOverviewPanel >
159+ }
160+ >
156161 < SSRComplianceGrid
157162 searchParams = { resolvedSearchParams }
158163 selectedScan = { selectedScanData }
@@ -176,25 +181,23 @@ const SSRComplianceGrid = async ({
176181 const scanId = searchParams . scanId ?. toString ( ) || "" ;
177182 const regionFilter = searchParams [ "filter[region__in]" ] ?. toString ( ) || "" ;
178183
179- // Extract all filter parameters
180- const filters = Object . fromEntries (
181- Object . entries ( searchParams ) . filter ( ( [ key ] ) => key . startsWith ( "filter[" ) ) ,
182- ) ;
183-
184- // Extract query from filters
185- const query = ( filters [ "filter[search]" ] as string ) || "" ;
186-
187184 // Only fetch compliance data if we have a valid scanId
188185 const compliancesData =
189186 scanId && scanId . trim ( ) !== ""
190187 ? await getCompliancesOverview ( {
191188 scanId,
192189 region : regionFilter ,
193- query,
194190 } )
195191 : { data : [ ] , errors : [ ] } ;
196192
197193 const type = compliancesData ?. data ?. type ;
194+ const frameworks = compliancesData ?. data
195+ ?. filter ( ( compliance : ComplianceOverviewData ) => {
196+ return compliance . attributes . framework !== "ProwlerThreatScore" ;
197+ } )
198+ . sort ( ( a : ComplianceOverviewData , b : ComplianceOverviewData ) =>
199+ a . attributes . framework . localeCompare ( b . attributes . framework ) ,
200+ ) ;
198201
199202 // Check if the response contains no data
200203 if (
@@ -204,58 +207,49 @@ const SSRComplianceGrid = async ({
204207 type === "tasks"
205208 ) {
206209 return (
207- < div className = "flex h-full items-center" >
208- < div className = "text-default-500 text-sm" >
209- No compliance data available for the selected scan.
210- </ div >
211- </ div >
210+ < Alert variant = "info" >
211+ < Info className = "size-4" />
212+ < AlertDescription >
213+ This scan has no compliance data available yet, please select a
214+ different one.
215+ </ AlertDescription >
216+ </ Alert >
212217 ) ;
213218 }
214219
215220 // Handle errors returned by the API
216221 if ( compliancesData ?. errors ?. length > 0 ) {
217222 return (
218- < div className = "flex h-full items-center" >
219- < div className = "text-default-500 text-sm" > Provide a valid scan ID.</ div >
220- </ div >
223+ < Alert variant = "info" >
224+ < Info className = "size-4" />
225+ < AlertDescription > Provide a valid scan ID.</ AlertDescription >
226+ </ Alert >
221227 ) ;
222228 }
223229
224230 return (
225- < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4" >
226- { compliancesData . data
227- . filter ( ( compliance : ComplianceOverviewData ) => {
228- // Filter out ProwlerThreatScore from the grid
229- return compliance . attributes . framework !== "ProwlerThreatScore" ;
230- } )
231- . sort ( ( a : ComplianceOverviewData , b : ComplianceOverviewData ) =>
232- a . attributes . framework . localeCompare ( b . attributes . framework ) ,
233- )
234- . map ( ( compliance : ComplianceOverviewData ) => {
235- const { attributes, id } = compliance ;
236- const {
237- framework,
238- version,
239- requirements_passed,
240- total_requirements,
241- } = attributes ;
231+ < ComplianceOverviewPanel >
232+ < ComplianceOverviewGrid
233+ frameworks = { frameworks }
234+ scanId = { scanId }
235+ selectedScan = { selectedScan }
236+ />
237+ </ ComplianceOverviewPanel >
238+ ) ;
239+ } ;
242240
243- return (
244- < ComplianceCard
245- key = { id }
246- title = { framework }
247- version = { version }
248- passingRequirements = { requirements_passed }
249- totalRequirements = { total_requirements }
250- prevPassingRequirements = { requirements_passed }
251- prevTotalRequirements = { total_requirements }
252- scanId = { scanId }
253- complianceId = { id }
254- id = { id }
255- selectedScan = { selectedScan }
256- />
257- ) ;
258- } ) }
259- </ div >
241+ const ComplianceOverviewPanel = ( {
242+ children,
243+ } : {
244+ children : React . ReactNode ;
245+ } ) => {
246+ return (
247+ < Card
248+ variant = "base"
249+ padding = "none"
250+ className = "minimal-scrollbar shadow-small relative z-0 w-full gap-4 overflow-auto"
251+ >
252+ < CardContent className = "flex flex-col gap-4 p-4" > { children } </ CardContent >
253+ </ Card >
260254 ) ;
261255} ;
0 commit comments