fix(ui): align overview metric cards (#12323)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Alan Buscaglia
2026-08-06 12:21:14 +02:00
committed by GitHub
co-authored by alejandrobailo
parent f727f1bb50
commit cd4693168a
11 changed files with 158 additions and 10 deletions
@@ -4,7 +4,7 @@ export function RiskSeverityChartSkeleton() {
return (
<Card
variant="base"
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col md:min-w-[380px]"
className="flex min-h-[372px] w-full min-w-0 flex-1 flex-col lg:w-auto lg:min-w-[485px]"
>
<CardHeader>
<Skeleton className="h-7 w-[260px] rounded-xl" />
@@ -79,7 +79,7 @@ export const RiskSeverityChart = ({
return (
<Card
variant="base"
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col md:min-w-[380px]"
className="flex min-h-[372px] w-full min-w-0 flex-1 flex-col lg:w-auto lg:min-w-[485px]"
>
<CardHeader>
<CardTitle>Risk Severity</CardTitle>
@@ -4,7 +4,7 @@ export function StatusChartSkeleton() {
return (
<Card
variant="base"
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col justify-between md:min-w-[380px]"
className="flex min-h-[372px] w-full min-w-0 flex-1 flex-col justify-between lg:w-auto lg:min-w-[485px]"
>
<CardHeader>
<Skeleton className="h-7 w-[260px] rounded-xl" />
@@ -93,7 +93,7 @@ export const StatusChart = ({
return (
<Card
variant="base"
className="flex min-h-[372px] min-w-[312px] flex-1 flex-col justify-between md:min-w-[380px]"
className="flex min-h-[372px] w-full min-w-0 flex-1 flex-col justify-between lg:w-auto lg:min-w-[485px]"
>
<CardHeader>
<CardTitle>Check Findings</CardTitle>
@@ -4,7 +4,7 @@ export function ThreatScoreSkeleton() {
return (
<Card
variant="base"
className="flex min-h-[372px] w-full flex-col justify-between lg:max-w-[312px]"
className="flex min-h-[372px] w-full min-w-0 flex-col justify-between lg:max-w-[312px]"
>
<CardHeader>
<Skeleton className="h-7 w-36 rounded-xl" />
@@ -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(<ThreatScore score={75} />);
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(<ThreatScoreSkeleton />);
const skeletonCard = container.querySelector('[data-slot="card"]');
const skeletonClassName = skeletonCard?.className;
// When
rerender(<ThreatScore score={75} />);
const loadedCard = screen
.getByText("Prowler ThreatScore")
.closest('[data-slot="card"]');
// Then
expect(skeletonClassName).toBeDefined();
expect(loadedCard?.className.split(" ").sort()).toEqual(
skeletonClassName?.split(" ").sort(),
);
});
});
@@ -115,7 +115,7 @@ export function ThreatScore({
return (
<Card
variant="base"
className="flex min-h-[372px] w-full flex-col justify-between xl:max-w-[312px]"
className="flex min-h-[372px] w-full min-w-0 flex-col justify-between lg:max-w-[312px]"
>
<CardHeader>
<CardTitle>Prowler ThreatScore</CardTitle>
+122
View File
@@ -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(
/<Card\s+variant="base"\s+className="([^"]+)"/,
)?.[1];
expect(className).toBeDefined();
return className?.split(" ") ?? [];
};
it("renders the overview banners before the provider filters", () => {
// Given
const firstBannerPosition = source.indexOf("<OverviewBanner");
@@ -25,4 +35,116 @@ describe("Overview page", () => {
expect(firstProviderFilterPosition).toBeGreaterThan(-1);
expect(bannersRenderBeforeFilters).toBe(true);
});
it("uses the responsive container breakpoint for the primary metric row", () => {
// Given
const metricRowClassName = source.match(
/<div className="([^"]+)">\s*<Suspense fallback={<ThreatScoreSkeleton \/>}>/,
)?.[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);
});
});
});
+1 -1
View File
@@ -90,7 +90,7 @@ export default async function Home({
<ProviderGroupSelector groups={providerGroupsData?.data ?? []} />
</div>
<div className="flex flex-col gap-6 xl:flex-row xl:flex-wrap xl:items-stretch">
<div className="flex flex-col gap-6 lg:flex-row lg:flex-wrap lg:items-stretch">
<Suspense fallback={<ThreatScoreSkeleton />}>
<ThreatScoreSSR searchParams={resolvedSearchParams} />
</Suspense>
@@ -0,0 +1 @@
Overview metric cards stack below the desktop layout threshold and preserve readable widths when aligned
@@ -0,0 +1 @@
Overview metric cards now align horizontally at medium desktop widths