diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 9059414c40..467fc2dd8f 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixes - Fix sync between filter buttons and URL when filters change. [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928) +- Improve heatmap perfomance. [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934) ### 🚀 Added diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 189a33531a..98a83d403b 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -24,7 +24,6 @@ import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance" import { ContentLayout } from "@/components/ui"; import { calculateCategoryHeatmapData, - calculateRegionHeatmapData, getComplianceMapper, } from "@/lib/compliance/commons"; import { ScanProps } from "@/types"; @@ -154,6 +153,7 @@ export default async function ComplianceDetail({ uniqueRegions={uniqueRegions} showSearch={false} framework={compliancetitle} + showProviders={false} /> @@ -192,16 +187,12 @@ const SSRComplianceContent = async ({ region, filter, logoPath, - uniqueRegions, - isRegionFiltered, }: { complianceId: string; scanId: string; region?: string; filter?: string; logoPath?: string; - uniqueRegions: string[]; - isRegionFiltered: boolean; }) => { if (!scanId) { return ( @@ -209,14 +200,7 @@ const SSRComplianceContent = async ({ - + @@ -236,20 +220,15 @@ const SSRComplianceContent = async ({ // Determine framework from the first attribute item const framework = attributesData?.data?.[0]?.attributes?.framework; const mapper = getComplianceMapper(framework); + + // Use the same data for both compliance view and heatmap const data = mapper.mapComplianceData( attributesData, requirementsData, filter, ); - // Calculate region heatmap data using already obtained data - const regionHeatmapData = await calculateRegionHeatmapData( - complianceId, - scanId, - uniqueRegions, - attributesData, - mapper, - ); + // Calculate category heatmap data const categoryHeatmapData = calculateCategoryHeatmapData(data); const totalRequirements: RequirementsTotals = data.reduce( @@ -277,12 +256,7 @@ const SSRComplianceContent = async ({ manual={totalRequirements.manual} /> - + diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx index d22c4b2fe4..ae13888bdd 100644 --- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx +++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx @@ -12,7 +12,7 @@ import { Accordion } from "@/components/ui/accordion/Accordion"; import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib"; import { getComplianceMapper } from "@/lib/compliance/commons"; -import { ComplianceId, Requirement } from "@/types/compliance"; +import { Requirement } from "@/types/compliance"; import { FindingProps, FindingsResponse } from "@/types/components"; interface ClientAccordionContentProps { @@ -32,7 +32,7 @@ export const ClientAccordionContent = ({ const [expandedFindings, setExpandedFindings] = useState([]); const searchParams = useSearchParams(); const pageNumber = searchParams.get("page") || "1"; - const complianceId = searchParams.get("complianceId") as ComplianceId; + const complianceId = searchParams.get("complianceId"); const defaultSort = "severity,status,-inserted_at"; const sort = searchParams.get("sort") || defaultSort; const loadedPageRef = useRef(null); @@ -116,8 +116,8 @@ export const ClientAccordionContent = ({ return (
{renderDetails()} -

- This requirement has no checks; therefore, there are no findings. +

+ ⚠️ This requirement has no checks; therefore, there are no findings.

); @@ -164,7 +164,11 @@ export const ClientAccordionContent = ({ ); } - return
There are no findings for this regions
; + return ( +
+ ⚠️ There are no findings for this regions +
+ ); }; return ( @@ -172,7 +176,7 @@ export const ClientAccordionContent = ({ {renderDetails()} {checks.length > 0 && ( -
+
{ return (
-
- {name} +
+ {name}
diff --git a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx index 0f3f681b59..c9fa69aa8d 100644 --- a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx +++ b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx @@ -24,7 +24,7 @@ export const ComplianceAccordionTitle = ({
{label.charAt(0).toUpperCase() + label.slice(1)} diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 63f4c0150b..720c5acd77 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -71,14 +71,15 @@ export const ComplianceCard: React.FC = ({ return "success"; }; + const isPressable = + id.includes("ens") || + id.includes("iso") || + id.includes("cis_") || + id.includes("pillar"); + const navigateToDetail = () => { // We will unlock this while developing the rest of complainces. - if ( - !id.includes("ens") && - !id.includes("iso") && - !id.includes("cis_") && - !id.includes("pillar") - ) { + if (!isPressable) { return; } @@ -106,7 +107,7 @@ export const ComplianceCard: React.FC = ({ fullWidth isHoverable shadow="sm" - isPressable + isPressable={isPressable} onPress={navigateToDetail} > diff --git a/ui/components/compliance/compliance-charts/heatmap-chart.tsx b/ui/components/compliance/compliance-charts/heatmap-chart.tsx index d4cb3b3a0a..bf5474595f 100644 --- a/ui/components/compliance/compliance-charts/heatmap-chart.tsx +++ b/ui/components/compliance/compliance-charts/heatmap-chart.tsx @@ -1,15 +1,13 @@ "use client"; +import { cn } from "@nextui-org/react"; import { useTheme } from "next-themes"; import { useState } from "react"; -import { CategoryData, RegionData } from "@/types/compliance"; +import { CategoryData } from "@/types/compliance"; interface HeatmapChartProps { - regions: RegionData[]; categories?: CategoryData[]; - isRegionFiltered?: boolean; // Indicates if a region filter is active - filteredRegionName?: string; // Name of the filtered region } const getHeatmapColor = (percentage: number): string => { @@ -32,48 +30,32 @@ const capitalizeFirstLetter = (text: string): string => { ); }; -export const HeatmapChart = ({ - regions, - categories = [], - isRegionFiltered = false, -}: HeatmapChartProps) => { +export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { const { theme } = useTheme(); - const [hoveredItem, setHoveredItem] = useState< - RegionData | CategoryData | null - >(null); + const [hoveredItem, setHoveredItem] = useState(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - // Determine what data to show and prepare it - const dataToShow = isRegionFiltered ? categories : regions; - const heatmapData = dataToShow + // Use categories data and prepare it + const heatmapData = categories .filter((item) => item.totalRequirements > 0) .sort((a, b) => b.failurePercentage - a.failurePercentage) .slice(0, 9); // Exactly 9 items for 3x3 grid // Check if there are no items with data - if (!dataToShow || dataToShow.length === 0 || heatmapData.length === 0) { - const noDataMessage = isRegionFiltered - ? "No category data available" - : "No regional data available"; - + if (!categories.length || heatmapData.length === 0) { return (

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-

{noDataMessage}

+

No category data available

); } - const handleMouseEnter = ( - item: RegionData | CategoryData, - event: React.MouseEvent, - ) => { + const handleMouseEnter = (item: CategoryData, event: React.MouseEvent) => { setHoveredItem(item); setMousePosition({ x: event.clientX, y: event.clientY }); }; @@ -90,19 +72,27 @@ export const HeatmapChart = ({

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-
- {/* 3x3 Grid */} -
+
+
{heatmapData.map((item) => (
-
+
- {isRegionFiltered - ? capitalizeFirstLetter(item.name) - : item.name} + {capitalizeFirstLetter(item.name)}
- {isRegionFiltered - ? capitalizeFirstLetter(hoveredItem.name) - : hoveredItem.name} + {capitalizeFirstLetter(hoveredItem.name)}
Failure Rate: {hoveredItem.failurePercentage}%
diff --git a/ui/components/compliance/compliance-header/compliance-header.tsx b/ui/components/compliance/compliance-header/compliance-header.tsx index e6bfe0dd2d..abe54adf11 100644 --- a/ui/components/compliance/compliance-header/compliance-header.tsx +++ b/ui/components/compliance/compliance-header/compliance-header.tsx @@ -14,6 +14,7 @@ interface ComplianceHeaderProps { showSearch?: boolean; showRegionFilter?: boolean; framework?: string; // Framework name to show specific filters + showProviders?: boolean; } export const ComplianceHeader = ({ @@ -22,6 +23,7 @@ export const ComplianceHeader = ({ showSearch = true, showRegionFilter = true, framework, + showProviders = true, }: ComplianceHeaderProps) => { const frameworkFilters = []; @@ -56,10 +58,10 @@ export const ComplianceHeader = ({ <> {showSearch && } - + {showProviders && } {allFilters.length > 0 && ( <> - + {showProviders && } )} diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx index 1bbcbc9ed8..8117d5eea6 100644 --- a/ui/components/ui/accordion/Accordion.tsx +++ b/ui/components/ui/accordion/Accordion.tsx @@ -138,7 +138,7 @@ export const Accordion = ({ indicator={} classNames={{ base: index === 0 || index === 1 ? "my-1" : "my-1", - title: "text-sm font-medium max-w-full overflow-hidden truncate", + title: "text-sm", subtitle: "text-xs text-gray-500", trigger: "py-2 px-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center", diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 7317bb6c0f..c62933b635 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -10,7 +10,6 @@ import { CategoryData, FailedSection, Framework, - RegionData, Requirement, RequirementsData, } from "@/types/compliance"; @@ -138,85 +137,6 @@ export const getComplianceMapper = (framework?: string): ComplianceMapper => { return complianceMappers[framework] || defaultMapper; }; -export const calculateRegionHeatmapData = async ( - complianceId: string, - scanId: string, - uniqueRegions: string[], - attributesData: AttributesData, - mapper: ComplianceMapper, -): Promise => { - if (!complianceId || !scanId || !uniqueRegions?.length) { - return []; - } - - try { - const { getComplianceRequirements } = await import("@/actions/compliances"); - - // Get data for each region in parallel - const regionPromises = uniqueRegions.map(async (region) => { - try { - // Only need to fetch requirements data per region - const regionRequirementsData = await getComplianceRequirements({ - complianceId, - scanId, - region, // Filter by specific region - }); - - // Map the data using the provided mapper - const mappedData = mapper.mapComplianceData( - attributesData, - regionRequirementsData, - ); - - // Calculate totals for this region - const regionTotals = mappedData.reduce( - (acc, framework) => ({ - pass: acc.pass + framework.pass, - fail: acc.fail + framework.fail, - manual: acc.manual + framework.manual, - }), - { pass: 0, fail: 0, manual: 0 }, - ); - - const totalRequirements = - regionTotals.pass + regionTotals.fail + regionTotals.manual; - const failurePercentage = - totalRequirements > 0 - ? Math.round((regionTotals.fail / totalRequirements) * 100) - : 0; - - return { - name: region, - failurePercentage, - totalRequirements, - failedRequirements: regionTotals.fail, - }; - } catch (error) { - console.error(`Error fetching data for region ${region}:`, error); - return { - name: region, - failurePercentage: 0, - totalRequirements: 0, - failedRequirements: 0, - }; - } - }); - - const regionData = await Promise.all(regionPromises); - - // Filter, sort and limit to top 9 regions for 3x3 grid - const filteredData = regionData - .filter((region) => region.totalRequirements > 0) - .sort((a, b) => b.failurePercentage - a.failurePercentage) - .slice(0, 9); - - return filteredData; - } catch (error) { - console.error("Error calculating region heatmap data:", error); - return []; - } -}; - export const calculateCategoryHeatmapData = ( complianceData: Framework[], ): CategoryData[] => { diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 8cfaab0246..994c90d200 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -1,16 +1,5 @@ export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings"; -export type ComplianceId = - | "ens_rd2022_aws" - | "iso27001_2013_aws" - | "iso27001_2022_aws" - | "cis_1.4_aws" - | "cis_1.5_aws" - | "cis_2.0_aws" - | "cis_3.0_aws" - | "cis_4.0_aws" - | "cis_5.0_aws"; - export interface CompliancesOverview { data: ComplianceOverviewData[]; }