mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-02 01:15:31 +00:00
feat(ui:overview) overview findings by status and severity (#5925)
This commit is contained in:
1 parent
4a8150d613
commit
677e20a1a4
17 files changed
+754
-293
No files matched your search
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+62
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user