mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
fix(ui): align overview metric cards (#12323)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
co-authored by
alejandrobailo
parent
f727f1bb50
commit
cd4693168a
+1
-1
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user