diff --git a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx index 8afb8a2df1..d61e2188d4 100644 --- a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx +++ b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx @@ -4,7 +4,7 @@ export function RiskSeverityChartSkeleton() { return ( diff --git a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx index 9a8b11cd8f..00e397ce2f 100644 --- a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx +++ b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx @@ -79,7 +79,7 @@ export const RiskSeverityChart = ({ return ( Risk Severity diff --git a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.skeleton.tsx b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.skeleton.tsx index 61fb836caf..ce27db3845 100644 --- a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.skeleton.tsx +++ b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.skeleton.tsx @@ -4,7 +4,7 @@ export function StatusChartSkeleton() { return ( diff --git a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx index 8f6540ecb4..e10a3869b8 100644 --- a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx +++ b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx @@ -93,7 +93,7 @@ export const StatusChart = ({ return ( Check Findings diff --git a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.skeleton.tsx b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.skeleton.tsx index bc817e8c55..6c72188ef0 100644 --- a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.skeleton.tsx +++ b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.skeleton.tsx @@ -4,7 +4,7 @@ export function ThreatScoreSkeleton() { return ( diff --git a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.test.tsx b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.test.tsx index df2fcdf4c3..a0444b37ae 100644 --- a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.test.tsx +++ b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.test.tsx @@ -2,16 +2,40 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { ThreatScore } from "./threat-score"; +import { ThreatScoreSkeleton } from "./threat-score.skeleton"; describe("ThreatScore", () => { - it("keeps the card full width until the overview row becomes horizontal", () => { + it("keeps the card compact when the overview row is horizontal", () => { + // Given / When render(); const card = screen .getByText("Prowler ThreatScore") .closest('[data-slot="card"]'); - expect(card).toHaveClass("xl:max-w-[312px]"); - expect(card).not.toHaveClass("lg:max-w-[312px]"); + // Then + expect(card).toHaveClass("w-full", "min-w-0", "lg:max-w-[312px]"); + ["max-w-[312px]", "xl:max-w-[312px]", "min-[485px]:min-w-[312px]"].forEach( + (className) => expect(card).not.toHaveClass(className), + ); + }); + + it("keeps the skeleton width aligned with the loaded card", () => { + // Given + const { container, rerender } = render(); + const skeletonCard = container.querySelector('[data-slot="card"]'); + const skeletonClassName = skeletonCard?.className; + + // When + rerender(); + const loadedCard = screen + .getByText("Prowler ThreatScore") + .closest('[data-slot="card"]'); + + // Then + expect(skeletonClassName).toBeDefined(); + expect(loadedCard?.className.split(" ").sort()).toEqual( + skeletonClassName?.split(" ").sort(), + ); }); }); diff --git a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.tsx b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.tsx index 267bdba303..39a11743a4 100644 --- a/ui/app/(prowler)/_overview/threat-score/_components/threat-score.tsx +++ b/ui/app/(prowler)/_overview/threat-score/_components/threat-score.tsx @@ -115,7 +115,7 @@ export function ThreatScore({ return ( Prowler ThreatScore diff --git a/ui/app/(prowler)/page.test.tsx b/ui/app/(prowler)/page.test.tsx index 78ddc4c550..a4dbbd3bf0 100644 --- a/ui/app/(prowler)/page.test.tsx +++ b/ui/app/(prowler)/page.test.tsx @@ -9,6 +9,16 @@ describe("Overview page", () => { const filePath = path.join(currentDir, "page.tsx"); const source = readFileSync(filePath, "utf8"); + const getBaseCardClasses = (componentSource: string) => { + const className = componentSource.match( + / { // Given const firstBannerPosition = source.indexOf(" { expect(firstProviderFilterPosition).toBeGreaterThan(-1); expect(bannersRenderBeforeFilters).toBe(true); }); + + it("uses the responsive container breakpoint for the primary metric row", () => { + // Given + const metricRowClassName = source.match( + /
\s*}>/, + )?.[1]; + + // When + const metricRowClasses = metricRowClassName?.split(" ") ?? []; + + // Then + expect(metricRowClassName).toBeDefined(); + expect(metricRowClasses.sort()).toEqual( + [ + "flex", + "flex-col", + "gap-6", + "lg:flex-row", + "lg:flex-wrap", + "lg:items-stretch", + ].sort(), + ); + }); + + it("keeps findings cards fluid below the desktop row breakpoint", () => { + // Given + const statusChartSource = readFileSync( + path.join( + currentDir, + "_overview/status-chart/_components/status-chart.tsx", + ), + "utf8", + ); + const statusChartSkeletonSource = readFileSync( + path.join( + currentDir, + "_overview/status-chart/_components/status-chart.skeleton.tsx", + ), + "utf8", + ); + const riskSeveritySource = readFileSync( + path.join( + currentDir, + "_overview/risk-severity/_components/risk-severity-chart.tsx", + ), + "utf8", + ); + const riskSeveritySkeletonSource = readFileSync( + path.join( + currentDir, + "_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx", + ), + "utf8", + ); + + // When + const cardClassLists = [ + getBaseCardClasses(statusChartSource), + getBaseCardClasses(statusChartSkeletonSource), + getBaseCardClasses(riskSeveritySource), + getBaseCardClasses(riskSeveritySkeletonSource), + ]; + + // Then + cardClassLists.forEach((cardClasses) => { + expect(cardClasses).toEqual( + expect.arrayContaining([ + "w-full", + "min-w-0", + "flex-1", + "lg:w-auto", + "lg:min-w-[485px]", + ]), + ); + const widthClasses = cardClasses.filter((className) => + className.includes("w-"), + ); + + expect(widthClasses.sort()).toEqual( + ["w-full", "min-w-0", "lg:w-auto", "lg:min-w-[485px]"].sort(), + ); + expect( + cardClasses.some((className) => className.startsWith("min-[")), + ).toBe(false); + }); + }); + + it("keeps findings skeletons aligned with their loaded cards", () => { + // Given + const sourcePairs = [ + [ + "_overview/status-chart/_components/status-chart.tsx", + "_overview/status-chart/_components/status-chart.skeleton.tsx", + ], + [ + "_overview/risk-severity/_components/risk-severity-chart.tsx", + "_overview/risk-severity/_components/risk-severity-chart.skeleton.tsx", + ], + ]; + + // When / Then + sourcePairs.forEach(([componentPath, skeletonPath]) => { + const componentClasses = getBaseCardClasses( + readFileSync(path.join(currentDir, componentPath), "utf8"), + ); + const skeletonClasses = getBaseCardClasses( + readFileSync(path.join(currentDir, skeletonPath), "utf8"), + ); + + expect(skeletonClasses).toEqual(componentClasses); + }); + }); }); diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 36a3959cc5..c2b864cf4a 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -90,7 +90,7 @@ export default async function Home({
-
+
}> diff --git a/ui/changelog.d/overview-metric-breakpoint.fixed.md b/ui/changelog.d/overview-metric-breakpoint.fixed.md new file mode 100644 index 0000000000..2f1386efe6 --- /dev/null +++ b/ui/changelog.d/overview-metric-breakpoint.fixed.md @@ -0,0 +1 @@ +Overview metric cards stack below the desktop layout threshold and preserve readable widths when aligned diff --git a/ui/changelog.d/overview-metric-row.fixed.md b/ui/changelog.d/overview-metric-row.fixed.md new file mode 100644 index 0000000000..b80c2e4ee8 --- /dev/null +++ b/ui/changelog.d/overview-metric-row.fixed.md @@ -0,0 +1 @@ +Overview metric cards now align horizontally at medium desktop widths