feat: add watchlist component (#9199)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
Alejandro Bailo
2025-11-25 16:01:38 +01:00
committed by GitHub
parent 8e7e376e4f
commit e020b3f74b
17 changed files with 553 additions and 20 deletions
@@ -0,0 +1,137 @@
import { StaticImageData } from "next/image";
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
import { MetaDataProps } from "@/types";
import { ComplianceOverviewData } from "@/types/compliance";
/**
* Raw API response from /compliance-overviews endpoint
*/
export interface ComplianceOverviewsResponse {
data: ComplianceOverviewData[];
meta?: {
pagination?: {
page: number;
pages: number;
count: number;
};
};
}
/**
* Enriched compliance overview with computed fields
*/
export interface EnrichedComplianceOverview {
id: string;
framework: string;
version: string;
requirements_passed: number;
requirements_failed: number;
requirements_manual: number;
total_requirements: number;
score: number;
label: string;
icon: string | StaticImageData | undefined;
}
/**
* Formats framework name for display by replacing hyphens with spaces
* e.g., "FedRAMP-20x-KSI-Low" -> "FedRAMP 20x KSI Low"
*/
function formatFrameworkName(framework: string): string {
return framework.replace(/-/g, " ");
}
/**
* Adapts the raw API response to enriched compliance data
* - Computes score percentage (rounded)
* - Formats label (framework + version)
* - Resolves framework icon
* - Preserves pagination metadata
*
* @param response - Raw API response with data and optional pagination
* @returns Object with enriched compliance data and metadata
*/
export function adaptComplianceOverviewsResponse(
response: ComplianceOverviewsResponse | undefined,
): {
data: EnrichedComplianceOverview[];
metadata?: MetaDataProps;
} {
if (!response?.data) {
return { data: [] };
}
const enrichedData = response.data.map((compliance) => {
const { id, attributes } = compliance;
const {
framework,
version,
requirements_passed,
requirements_failed,
requirements_manual,
total_requirements,
} = attributes;
const totalRequirements = Number(total_requirements) || 0;
const passedRequirements = Number(requirements_passed) || 0;
const score =
totalRequirements > 0
? Math.round((passedRequirements / totalRequirements) * 100)
: 0;
const formattedFramework = formatFrameworkName(framework);
const label = version
? `${formattedFramework} - ${version}`
: formattedFramework;
const icon = getComplianceIcon(framework);
return {
id,
framework,
version,
requirements_passed,
requirements_failed,
requirements_manual,
total_requirements,
score,
label,
icon,
};
});
const metadata: MetaDataProps | undefined = response.meta?.pagination
? {
pagination: {
page: response.meta.pagination.page,
pages: response.meta.pagination.pages,
count: response.meta.pagination.count,
itemsPerPage: [10, 25, 50, 100],
},
version: "1.0",
}
: undefined;
return { data: enrichedData, metadata };
}
/**
* Sorts compliances for watchlist display:
* - Excludes ProwlerThreatScore
* - Sorted by score ascending (worst/lowest scores first)
* - Limited to specified count
*
* @param data - Enriched compliance data
* @param limit - Maximum number of items to return (default: 9)
* @returns Sorted and limited compliance data
*/
export function sortCompliancesForWatchlist(
data: EnrichedComplianceOverview[],
limit: number = 9,
): EnrichedComplianceOverview[] {
return [...data]
.filter((item) => item.framework !== "ProwlerThreatScore")
.sort((a, b) => a.score - b.score)
.slice(0, limit);
}
+18 -9
View File
@@ -7,22 +7,31 @@ export const getCompliancesOverview = async ({
scanId,
region,
query,
filters = {},
}: {
scanId: string;
scanId?: string;
region?: string | string[];
query?: string;
}) => {
filters?: Record<string, string | string[] | undefined>;
} = {}) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/compliance-overviews`);
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
if (query) url.searchParams.append("filter[search]", query);
const setParam = (key: string, value?: string | string[]) => {
if (!value) return;
if (region) {
const regionValue = Array.isArray(region) ? region.join(",") : region;
url.searchParams.append("filter[region__in]", regionValue);
}
const serializedValue = Array.isArray(value) ? value.join(",") : value;
if (serializedValue.trim().length > 0) {
url.searchParams.set(key, serializedValue);
}
};
Object.entries(filters).forEach(([key, value]) => setParam(key, value));
setParam("filter[scan_id]", scanId);
setParam("filter[region__in]", region);
if (query) url.searchParams.set("filter[search]", query);
try {
const response = await fetch(url.toString(), {
@@ -31,7 +40,7 @@ export const getCompliancesOverview = async ({
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching providers:", error);
console.error("Error fetching compliances overview:", error);
return undefined;
}
};
+1
View File
@@ -1 +1,2 @@
export * from "./compliances";
export * from "./compliances.adapter";