feat(ui:overview) overview findings by status and severity (#5925)

This commit is contained in:
Pablo Lara authored and GitHub committed 2024-11-27 13:53:16 +01:00
1 parent 4a8150d613
commit 677e20a1a4
17 files changed
+754 -293

No files matched your search

-76
View File
@@ -1,76 +0,0 @@
"use client";
import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart/Chart";
const chartData = [
{ severity: "critical", findings: 32, fill: "var(--color-critical)" },
{ severity: "high", findings: 78, fill: "var(--color-high)" },
{ severity: "medium", findings: 117, fill: "var(--color-medium)" },
{ severity: "low", findings: 39, fill: "var(--color-low)" },
];
const chartConfig = {
findings: {
label: "Findings",
},
critical: {
label: "Critical",
color: "hsl(var(--chart-critical))",
},
high: {
label: "High",
color: "hsl(var(--chart-fail))",
},
medium: {
label: "Medium",
color: "hsl(var(--chart-medium))",
},
low: {
label: "Low",
color: "hsl(var(--chart-low))",
},
} satisfies ChartConfig;
export const SeverityChart = () => {
return (
<div className="my-auto">
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={chartData} layout="vertical">
<YAxis
dataKey="severity"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) =>
chartConfig[value as keyof typeof chartConfig]?.label
}
/>
<XAxis dataKey="findings" type="number" hide>
<LabelList position="insideTop" offset={12} fontSize={12} />
</XAxis>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
/>
<Bar dataKey="findings" layout="vertical" radius={12}>
<LabelList
position="insideRight"
offset={10}
className="fill-foreground font-bold"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</div>
);
};
-148
View File
@@ -1,148 +0,0 @@
"use client";
import { Chip, Divider, Spacer } from "@nextui-org/react";
import { TrendingUp } from "lucide-react";
import * as React from "react";
import { Label, Pie, PieChart } from "recharts";
import { NotificationIcon, SuccessIcon } from "../icons";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "../ui";
const calculatePercent = (
chartData: { findings: string; number: number; fill: string }[],
) => {
const total = chartData.reduce((sum, item) => sum + item.number, 0);
return chartData.map((item) => ({
...item,
percent: Math.round((item.number / total) * 100) + "%",
}));
};
const chartData = [
{
findings: "Success",
number: 436,
fill: "var(--color-success)",
},
{ findings: "Fail", number: 293, fill: "var(--color-fail)" },
];
const updatedChartData = calculatePercent(chartData);
const chartConfig = {
number: {
label: "Findings",
},
success: {
label: "Success",
color: "hsl(var(--chart-success))",
},
fail: {
label: "Fail",
color: "hsl(var(--chart-fail))",
},
} satisfies ChartConfig;
export function StatusChart() {
const totalVisitors = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.number, 0);
}, []);
return (
<div className="my-auto flex items-center justify-center self-center">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square min-w-[200px] md:min-h-[250px]"
>
<PieChart>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Pie
data={chartData}
dataKey="number"
nameKey="findings"
innerRadius={60}
strokeWidth={50}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
radius={70}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
{totalVisitors.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-foreground"
>
Findings
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
<div className="mx-6 flex flex-col justify-center gap-2 text-small">
<div className="flex space-x-4">
<Chip
className="h-5"
variant="flat"
startContent={<SuccessIcon size={18} />}
color="success"
radius="lg"
size="md"
>
{chartData[0].number}
</Chip>
<Divider orientation="vertical" />
<span>{updatedChartData[0].percent}</span>
<Divider orientation="vertical" />
</div>
<div className="flex items-center font-light leading-none">
No change from last scan
</div>
<Spacer y={4} />
<div className="text-muted-foreground flex flex-col gap-2 leading-none">
<div className="flex space-x-4">
<Chip
className="h-5"
variant="flat"
startContent={<NotificationIcon size={18} />}
color="danger"
radius="lg"
size="md"
>
{chartData[1].number}
</Chip>
<Divider orientation="vertical" />
<span>{updatedChartData[1].percent}</span>
<Divider orientation="vertical" />
</div>
<div className="flex items-center gap-1 font-medium leading-none">
+2 findings from last scan <TrendingUp className="h-4 w-4" />
</div>
</div>
</div>
</div>
);
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./SeverityChart";
export * from "./StatusChart";
@@ -60,6 +60,8 @@ export const CustomDatePicker = () => {
<div className="flex w-full flex-col md:gap-2">
<DatePicker
aria-label="Select a Date"
label="Date"
labelPlacement="inside"
CalendarTopContent={
<ButtonGroup
fullWidth
@@ -1,5 +1,8 @@
"use client";
import { Select, SelectItem } from "@nextui-org/react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react";
const regions = [
{ key: "af-south-1", label: "AF South 1" },
@@ -33,7 +36,29 @@ const regions = [
{ key: "us-west-2", label: "US West 2" },
];
export const CustomRegionSelection = () => {
export const CustomRegionSelection: React.FC = () => {
const router = useRouter();
const searchParams = useSearchParams();
// Memoize selected keys based on the URL
const selectedKeys = useMemo(() => {
const params = searchParams.get("filter[regions]");
return params ? params.split(",") : [];
}, [searchParams]);
const applyRegionFilter = useCallback(
(values: string[]) => {
const params = new URLSearchParams(searchParams.toString());
if (values.length > 0) {
params.set("filter[regions]", values.join(","));
} else {
params.delete("filter[regions]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
[router, searchParams],
);
return (
<Select
label="Region"
@@ -42,9 +67,11 @@ export const CustomRegionSelection = () => {
selectionMode="multiple"
className="w-full"
size="sm"
selectedKeys={selectedKeys}
onSelectionChange={(keys) => applyRegionFilter(Array.from(keys))}
>
{regions.map((acc) => (
<SelectItem key={acc.key}>{acc.label}</SelectItem>
{regions.map((region) => (
<SelectItem key={region.key}>{region.label}</SelectItem>
))}
</Select>
);
@@ -39,8 +39,9 @@ export const CustomSearchInput: React.FC = () => {
<Input
variant="flat"
aria-label="Search"
label="Search"
placeholder="Search..."
labelPlacement="outside"
labelPlacement="inside"
value={searchQuery}
startContent={<SearchIcon className="text-default-400" width={16} />}
onChange={(e) => {
@@ -66,7 +66,8 @@ export const CustomSelectProvider: React.FC = () => {
items={dataInputsProvider}
aria-label="Select a Provider"
placeholder="Select a provider"
labelPlacement="outside"
label="Provider"
labelPlacement="inside"
size="sm"
onChange={(e) => {
const value = e.target.value;
@@ -0,0 +1,108 @@
"use client";
import { Card, CardBody } from "@nextui-org/react";
import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart/Chart";
import { FindingsSeverityOverview } from "@/types/components";
const chartConfig = {
critical: {
label: "Critical",
color: "hsl(var(--chart-critical))",
},
high: {
label: "High",
color: "hsl(var(--chart-fail))",
},
medium: {
label: "Medium",
color: "hsl(var(--chart-medium))",
},
low: {
label: "Low",
color: "hsl(var(--chart-low))",
},
informational: {
label: "Informational",
color: "hsl(var(--chart-informational))",
},
} satisfies ChartConfig;
export const FindingsBySeverityChart = ({
findingsBySeverity,
}: {
findingsBySeverity: FindingsSeverityOverview;
}) => {
const defaultAttributes = {
critical: 0,
high: 0,
medium: 0,
low: 0,
informational: 0,
};
const attributes = findingsBySeverity?.data?.attributes || defaultAttributes;
const chartData = Object.entries(attributes).map(([severity, findings]) => ({
severity,
findings,
fill: chartConfig[severity as keyof typeof chartConfig]?.color,
}));
return (
<Card className="dark:bg-prowler-blue-400">
<CardBody>
<div className="my-auto">
<ChartContainer config={chartConfig}>
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
barGap={2}
height={200}
margin={{ left: 50 }}
width={500}
>
<YAxis
dataKey="severity"
type="category"
tickLine={false}
tickMargin={20}
axisLine={false}
tickFormatter={(value) =>
chartConfig[value as keyof typeof chartConfig]?.label
}
/>
<XAxis dataKey="findings" type="number" hide>
<LabelList position="insideTop" offset={1} fontSize={12} />
</XAxis>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
/>
<Bar
dataKey="findings"
layout="vertical"
radius={12}
barSize={20}
>
<LabelList
position="insideRight"
offset={10}
className="fill-foreground font-bold"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</div>
</CardBody>
</Card>
);
};
@@ -0,0 +1,62 @@
import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react";
export const SkeletonFindingsBySeverityChart = () => {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-1/3 rounded-lg">
<div className="h-6 bg-default-200"></div>
</Skeleton>
</CardHeader>
<CardBody>
<div className="flex flex-col gap-4">
{/* Critical */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-1/4 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
{/* High */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-2/4 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
{/* Medium */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-3/4 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
{/* Low */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-1/2 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
{/* Informational */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-1/6 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-12 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
</div>
</CardBody>
</Card>
);
};
@@ -0,0 +1,196 @@
"use client";
import { Card, CardBody } from "@nextui-org/react";
import { Chip } from "@nextui-org/react";
import { TrendingUp } from "lucide-react";
import React, { useMemo } from "react";
import { Label, Pie, PieChart } from "recharts";
import { NotificationIcon, SuccessIcon } from "@/components/icons";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart/Chart";
const calculatePercent = (
chartData: { findings: string; number: number; fill: string }[],
) => {
const total = chartData.reduce((sum, item) => sum + item.number, 0);
return chartData.map((item) => ({
...item,
percent: total > 0 ? Math.round((item.number / total) * 100) + "%" : "0%",
}));
};
interface FindingsByStatusChartProps {
findingsByStatus: {
data: {
attributes: {
fail: number;
pass: number;
pass_new: number;
fail_new: number;
total: number;
};
};
};
}
const chartConfig = {
number: {
label: "Findings",
},
success: {
label: "Success",
color: "hsl(var(--chart-success))",
},
fail: {
label: "Fail",
color: "hsl(var(--chart-fail))",
},
} satisfies ChartConfig;
export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
findingsByStatus,
}) => {
const {
fail = 0,
pass = 0,
pass_new = 0,
fail_new = 0,
} = findingsByStatus?.data?.attributes || {};
const chartData = [
{
findings: "Success",
number: pass,
fill: "var(--color-success)",
},
{
findings: "Fail",
number: fail,
fill: "var(--color-fail)",
},
];
const updatedChartData = calculatePercent(chartData);
const totalFindings = useMemo(
() => chartData.reduce((acc, curr) => acc + curr.number, 0),
[chartData],
);
const emptyChartData = [
{
findings: "Empty",
number: 1,
fill: "hsl(var(--nextui-default-200))",
},
];
return (
<Card className="dark:bg-prowler-blue-400">
<CardBody>
<div className="flex items-center gap-6">
<ChartContainer
config={chartConfig}
className="aspect-square w-[150px] min-w-[150px]"
>
<PieChart>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<Pie
data={totalFindings > 0 ? chartData : emptyChartData}
dataKey="number"
nameKey="findings"
innerRadius={40}
strokeWidth={35}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-xl font-bold"
>
{totalFindings > 0
? totalFindings.toLocaleString()
: "0"}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 20}
className="fill-foreground text-xs"
>
Findings
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<Chip
className="h-5"
variant="flat"
startContent={<SuccessIcon size={18} />}
color="success"
radius="lg"
size="md"
>
{chartData[0].number}
</Chip>
<span>{updatedChartData[0].percent}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1 text-xs font-medium leading-none">
{pass_new > 0 ? (
<>
+{pass_new} pass findings from last day{" "}
<TrendingUp className="h-4 w-4" />
</>
) : pass_new < 0 ? (
<>{pass_new} pass findings from last day</>
) : (
"No change from last day"
)}
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<Chip
className="h-5"
variant="flat"
startContent={<NotificationIcon size={18} />}
color="danger"
radius="lg"
size="md"
>
{chartData[1].number}
</Chip>
<span>{updatedChartData[1].percent}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1 text-xs font-medium leading-none">
+{fail_new} fail findings from last day{" "}
<TrendingUp className="h-4 w-4" />
</div>
</div>
</div>
</div>
</CardBody>
</Card>
);
};
@@ -0,0 +1,54 @@
import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react";
export const SkeletonFindingsByStatusChart = () => {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-1/4 rounded-lg">
<div className="h-6 bg-default-200"></div>
</Skeleton>
</CardHeader>
<CardBody>
<div className="flex items-center gap-6">
{/* Circle Chart Skeleton */}
<Skeleton className="aspect-square h-[150px] w-[150px] rounded-full">
<div className="h-[150px] w-[150px] bg-default-200"></div>
</Skeleton>
{/* Text Details Skeleton */}
<div className="flex flex-col gap-4">
{/* Pass Findings */}
<div className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-16 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-10 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
</div>
<Skeleton className="h-4 w-48 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
{/* Fail Findings */}
<div className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-16 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-5 w-10 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
</div>
<Skeleton className="h-4 w-48 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</div>
</div>
</div>
</CardBody>
</Card>
);
};
+4
View File
@@ -1,4 +1,8 @@
export * from "./AttackSurface";
export * from "./findings-by-severity-chart/findings-by-severity-chart";
export * from "./findings-by-severity-chart/skeleton-findings-severity-chart";
export * from "./findings-by-status-chart/findings-by-status-chart";
export * from "./findings-by-status-chart/skeleton-findings-status-chart";
export * from "./new-findings-table/link-to-findings/link-to-findings";
export * from "./provider-overview/provider-overview";
export * from "./provider-overview/skeleton-provider-overview";
@@ -7,7 +7,7 @@ export const LinkToFindings = () => {
return (
<div className="mt-4 flex w-full items-center justify-end">
<CustomButton
asLink="/findings?filter[severity]=critical&filter[delta__in]=new&filter[status__in]=FAIL"
asLink="/findings?filter[delta__in]=new&filter[status__in]=FAIL"
ariaLabel="Go to Findings page"
variant="solid"
color="action"