mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-08 20:33:41 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import Link from "next/link";
|
|
|
|
import { IssuesLoadError } from "@/components/issues/issues-load-error";
|
|
import { Card, CardContent, CardTitle } from "@/components/shadcn";
|
|
import { Badge } from "@/components/shadcn/badge/badge";
|
|
import { Button } from "@/components/shadcn/button/button";
|
|
import { riskFactors } from "@/lib/issues/risk-factors";
|
|
import { issuesHref } from "@/lib/issues/scope";
|
|
import { VERDICTS } from "@/lib/issues/verdicts";
|
|
import { IssueResource } from "@/types/issues";
|
|
|
|
/** Enough to show the combination; the rest is on the issue. */
|
|
const TOP_FACTORS = 3;
|
|
|
|
interface TopIssuesProps {
|
|
issues: IssueResource[] | null;
|
|
scope: Record<string, string>;
|
|
}
|
|
|
|
/**
|
|
* The worst issues in scope, each on its own tile. Two tiles per row once the
|
|
* charts column is wide enough; the list is short, so a single column would
|
|
* leave most of the card empty on desktop.
|
|
*/
|
|
export const TopIssues = ({ issues, scope }: TopIssuesProps) => (
|
|
<Card variant="base" className="w-full min-w-0">
|
|
<CardTitle>Top issues</CardTitle>
|
|
<CardContent className="flex flex-col gap-4">
|
|
{issues === null ? (
|
|
<IssuesLoadError />
|
|
) : issues.length > 0 ? (
|
|
<ul className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
{issues.map(({ id, attributes, relationships }) => {
|
|
const verdict = VERDICTS.find(
|
|
(item) => item.id === attributes.verdict,
|
|
);
|
|
const providerId = relationships?.provider?.data?.id;
|
|
const factors = riskFactors(attributes)
|
|
.slice(0, TOP_FACTORS)
|
|
.map((factor) => factor.label)
|
|
.join(" · ");
|
|
return (
|
|
<li key={id} className="min-w-0">
|
|
<Card variant="inner" padding="md" className="h-full gap-2">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<Badge variant={verdict?.variant ?? "tag"}>
|
|
{verdict?.label ?? attributes.verdict}
|
|
</Badge>
|
|
<span className="text-text-neutral-tertiary shrink-0 text-xs">
|
|
{attributes.findings_count}{" "}
|
|
{attributes.findings_count === 1 ? "finding" : "findings"}
|
|
</span>
|
|
</div>
|
|
<div className="min-w-0">
|
|
<Button
|
|
asChild
|
|
variant="link"
|
|
size="link-sm"
|
|
className="max-w-full"
|
|
>
|
|
<Link
|
|
href={issuesHref(scope, {
|
|
"filter[pattern]": attributes.pattern,
|
|
...(providerId
|
|
? { "filter[provider_id__in]": providerId }
|
|
: {}),
|
|
})}
|
|
title={attributes.title}
|
|
>
|
|
<span className="truncate">{attributes.title}</span>
|
|
</Link>
|
|
</Button>
|
|
<p className="text-text-neutral-tertiary truncate text-xs">
|
|
{attributes.account}
|
|
</p>
|
|
{/* The reason to look is the combination, not the rank. */}
|
|
{factors && (
|
|
<p className="text-text-neutral-secondary truncate text-xs">
|
|
{factors}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : (
|
|
<p className="text-text-neutral-tertiary py-8 text-center text-sm">
|
|
No issues in this scope.
|
|
</p>
|
|
)}
|
|
<Button asChild variant="link" size="link-sm" className="self-end">
|
|
<Link href={issuesHref(scope)}>View all issues</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|