mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
|
|
import { WatchlistToggle } from "@/components/compliance/watchlist/watchlist-toggle";
|
|
import { ProviderTypeIcon } from "@/components/icons/providers-badge/provider-type-icon";
|
|
import { Progress } from "@/components/shadcn/progress";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/shadcn/tooltip";
|
|
import { formatComplianceFrameworkTitle } from "@/lib/compliance/framework-title";
|
|
import type { ScoreColorVariant } from "@/lib/compliance/score-utils";
|
|
import { cn } from "@/lib/utils";
|
|
import { PROVIDER_DISPLAY_NAMES } from "@/types/providers";
|
|
|
|
import { buildCrossProviderDetailHref } from "../_lib/cross-provider-frameworks";
|
|
import type { UniversalWatchlistState } from "../_lib/universal-watchlist";
|
|
import type { CrossProviderFrameworkSummary } from "../_types";
|
|
|
|
import { AggregatedFrameworkCard } from "./aggregated-framework-card";
|
|
|
|
interface CrossProviderFrameworkCardProps
|
|
extends CrossProviderFrameworkSummary {
|
|
/** Resolved watchlist state of this universal framework's single card
|
|
* (see `resolveUniversalWatchlistState`). */
|
|
watchlist?: UniversalWatchlistState;
|
|
/** MANAGE_SCANS. Without it the toggle is not rendered. */
|
|
canManageWatchlist?: boolean;
|
|
}
|
|
|
|
export const CrossProviderFrameworkCard = ({
|
|
complianceId,
|
|
title,
|
|
version,
|
|
description,
|
|
requirementsPassed,
|
|
requirementsFailed,
|
|
requirementsManual,
|
|
totalRequirements,
|
|
providerBreakdown,
|
|
watchlist,
|
|
canManageWatchlist = false,
|
|
}: CrossProviderFrameworkCardProps) => {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
|
|
const formattedTitle = formatComplianceFrameworkTitle(title, version);
|
|
|
|
const ratingPercentage =
|
|
totalRequirements > 0
|
|
? Math.floor((requirementsPassed / totalRequirements) * 100)
|
|
: 0;
|
|
|
|
// Same thresholds as the per-scan ComplianceCard.
|
|
const getRatingVariant = (value: number): ScoreColorVariant => {
|
|
if (value <= 10) return "danger";
|
|
if (value <= 40) return "warning";
|
|
return "success";
|
|
};
|
|
|
|
const navigateToDetail = () => {
|
|
router.push(
|
|
buildCrossProviderDetailHref(
|
|
{ complianceId, title, version },
|
|
Object.fromEntries(searchParams.entries()),
|
|
),
|
|
);
|
|
};
|
|
|
|
return (
|
|
<AggregatedFrameworkCard
|
|
frameworkTitle={title}
|
|
formattedTitle={formattedTitle}
|
|
ariaLabel={formattedTitle}
|
|
onActivate={navigateToDetail}
|
|
tooltip={description}
|
|
actions={
|
|
watchlist?.target && canManageWatchlist ? (
|
|
<WatchlistToggle
|
|
target={watchlist.target}
|
|
state={watchlist.state}
|
|
entryId={watchlist.entryId}
|
|
/>
|
|
) : undefined
|
|
}
|
|
subtitle={
|
|
<small className="truncate">
|
|
<span className="mr-1 text-xs font-semibold">
|
|
{requirementsPassed} / {totalRequirements}
|
|
</span>
|
|
Passing Requirements
|
|
</small>
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between gap-3 text-xs">
|
|
<span className="text-text-neutral-secondary font-medium tracking-wider">
|
|
Score:
|
|
</span>
|
|
<span className="text-text-neutral-secondary">
|
|
{ratingPercentage}%
|
|
</span>
|
|
</div>
|
|
<Progress
|
|
aria-label="Cross-provider compliance score"
|
|
value={ratingPercentage}
|
|
variant={getRatingVariant(ratingPercentage)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
{providerBreakdown.map((entry) => (
|
|
<Tooltip key={entry.provider}>
|
|
<TooltipTrigger asChild>
|
|
<span
|
|
data-testid={`provider-chip-${entry.provider}`}
|
|
data-unscanned={entry.unscanned || undefined}
|
|
className={cn(
|
|
"inline-flex items-center",
|
|
entry.unscanned && "opacity-35 grayscale",
|
|
)}
|
|
>
|
|
<ProviderTypeIcon type={entry.provider} size={18} />
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{PROVIDER_DISPLAY_NAMES[entry.provider]}
|
|
{entry.unscanned
|
|
? " — no completed scan yet"
|
|
: ` — ${entry.score}% passing`}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
</div>
|
|
<span className="text-text-neutral-secondary text-xs whitespace-nowrap">
|
|
{requirementsFailed} failed · {requirementsManual} manual
|
|
</span>
|
|
</div>
|
|
</AggregatedFrameworkCard>
|
|
);
|
|
};
|