mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat: Include/exclude muted findings (#8228)
This commit is contained in:
@@ -59,9 +59,9 @@ export const getFindingsByStatus = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
// Handle multiple filters
|
||||
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
if (key !== "filter[search]" && key !== "filter[muted]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
@@ -102,9 +102,9 @@ export const getFindingsBySeverity = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
// Handle multiple filters
|
||||
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
if (key !== "filter[search]" && key !== "filter[muted]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function Home({
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
return (
|
||||
<ContentLayout title="Overview" icon="solar:pie-chart-2-outline">
|
||||
<FilterControls providers />
|
||||
<FilterControls providers mutedFindings showClearButton={false} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-12 lg:gap-6">
|
||||
<div className="col-span-12 lg:col-span-4">
|
||||
@@ -59,7 +59,7 @@ export default function Home({
|
||||
key={searchParamsKey}
|
||||
fallback={<SkeletonTableNewFindings />}
|
||||
>
|
||||
<SSRDataNewFindingsTable />
|
||||
<SSRDataNewFindingsTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +124,11 @@ const SSRFindingsBySeverity = async ({
|
||||
);
|
||||
};
|
||||
|
||||
const SSRDataNewFindingsTable = async () => {
|
||||
const SSRDataNewFindingsTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps | undefined | null;
|
||||
}) => {
|
||||
const page = 1;
|
||||
const sort = "severity,-inserted_at";
|
||||
|
||||
@@ -133,11 +137,21 @@ const SSRDataNewFindingsTable = async () => {
|
||||
"filter[delta]": "new",
|
||||
};
|
||||
|
||||
const filters = searchParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(searchParams).filter(([key]) =>
|
||||
key.startsWith("filter["),
|
||||
),
|
||||
)
|
||||
: {};
|
||||
|
||||
const combinedFilters = { ...defaultFilters, ...filters };
|
||||
|
||||
const findingsData = await getLatestFindings({
|
||||
query: undefined,
|
||||
page,
|
||||
sort,
|
||||
filters: defaultFilters,
|
||||
filters: combinedFilters,
|
||||
});
|
||||
|
||||
// Create dictionaries for resources, scans, and providers
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { Checkbox } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useUrlFilters } from "@/hooks/use-url-filters";
|
||||
|
||||
export const CustomCheckboxMutedFindings = () => {
|
||||
const { updateFilter } = useUrlFilters();
|
||||
const searchParams = useSearchParams();
|
||||
const [excludeMuted, setExcludeMuted] = useState(
|
||||
searchParams.get("filter[muted]") === "false",
|
||||
);
|
||||
|
||||
const handleMutedChange = (value: boolean) => {
|
||||
setExcludeMuted(value);
|
||||
updateFilter("muted", value ? "false" : "true");
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
classNames={{
|
||||
label: "text-small",
|
||||
wrapper: "checkbox-update xl:-mt-8",
|
||||
}}
|
||||
size="md"
|
||||
color="danger"
|
||||
aria-label="Include Mutelist"
|
||||
>
|
||||
Include Mutelist
|
||||
</Checkbox>
|
||||
<div className="flex h-full">
|
||||
<Checkbox
|
||||
classNames={{
|
||||
label: "text-small",
|
||||
wrapper: "checkbox-update",
|
||||
}}
|
||||
size="md"
|
||||
color="primary"
|
||||
aria-label="Include Mutelist"
|
||||
isSelected={excludeMuted}
|
||||
onValueChange={handleMutedChange}
|
||||
>
|
||||
Exclude muted findings
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Spacer } from "@nextui-org/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { FilterControlsProps } from "@/types";
|
||||
import { FilterOption } from "@/types";
|
||||
|
||||
import { DataTableFilterCustom } from "../ui/table";
|
||||
import { ClearFiltersButton } from "./clear-filters-button";
|
||||
@@ -15,6 +15,17 @@ import { CustomRegionSelection } from "./custom-region-selection";
|
||||
import { CustomSearchInput } from "./custom-search-input";
|
||||
import { CustomSelectProvider } from "./custom-select-provider";
|
||||
|
||||
export interface FilterControlsProps {
|
||||
search?: boolean;
|
||||
providers?: boolean;
|
||||
date?: boolean;
|
||||
regions?: boolean;
|
||||
accounts?: boolean;
|
||||
mutedFindings?: boolean;
|
||||
customFilters?: FilterOption[];
|
||||
showClearButton?: boolean;
|
||||
}
|
||||
|
||||
export const FilterControls: React.FC<FilterControlsProps> = ({
|
||||
search = false,
|
||||
providers = false,
|
||||
@@ -22,16 +33,17 @@ export const FilterControls: React.FC<FilterControlsProps> = ({
|
||||
regions = false,
|
||||
accounts = false,
|
||||
mutedFindings = false,
|
||||
showClearButton = true,
|
||||
customFilters,
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const [showClearButton, setShowClearButton] = useState(false);
|
||||
const [hasFilters, setHasFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasFilters = Array.from(searchParams.keys()).some(
|
||||
(key) => key.startsWith("filter[") || key === "sort",
|
||||
);
|
||||
setShowClearButton(hasFilters);
|
||||
setHasFilters(hasFilters);
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
@@ -43,7 +55,9 @@ export const FilterControls: React.FC<FilterControlsProps> = ({
|
||||
{regions && <CustomRegionSelection />}
|
||||
{accounts && <CustomAccountSelection />}
|
||||
{mutedFindings && <CustomCheckboxMutedFindings />}
|
||||
{!customFilters && showClearButton && <ClearFiltersButton />}
|
||||
{!customFilters && hasFilters && showClearButton && (
|
||||
<ClearFiltersButton />
|
||||
)}
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
{customFilters && (
|
||||
|
||||
@@ -38,6 +38,7 @@ export const FindingsFilters = ({
|
||||
<FilterControls
|
||||
search
|
||||
date
|
||||
mutedFindings
|
||||
customFilters={[
|
||||
...filterFindings,
|
||||
{
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Card, CardBody } from "@nextui-org/react";
|
||||
import { Chip } from "@nextui-org/react";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useMemo } from "react";
|
||||
import { Label, Pie, PieChart } from "recharts";
|
||||
|
||||
import { NotificationIcon, SuccessIcon } from "@/components/icons";
|
||||
import { MutedIcon, NotificationIcon, SuccessIcon } from "@/components/icons";
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -32,8 +33,10 @@ interface FindingsByStatusChartProps {
|
||||
attributes: {
|
||||
fail: number;
|
||||
pass: number;
|
||||
muted: number;
|
||||
pass_new: number;
|
||||
fail_new: number;
|
||||
muted_new: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
@@ -52,19 +55,29 @@ const chartConfig = {
|
||||
label: "Fail",
|
||||
color: "hsl(var(--chart-fail))",
|
||||
},
|
||||
muted: {
|
||||
label: "Muted",
|
||||
color: "hsl(var(--chart-muted))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
findingsByStatus,
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const shouldShowMuted = searchParams.get("filter[muted]") !== "false";
|
||||
|
||||
const {
|
||||
fail = 0,
|
||||
pass = 0,
|
||||
muted = 0,
|
||||
pass_new = 0,
|
||||
fail_new = 0,
|
||||
muted_new = 0,
|
||||
} = findingsByStatus?.data?.attributes || {};
|
||||
const chartData = useMemo(
|
||||
() => [
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const data = [
|
||||
{
|
||||
findings: "Success",
|
||||
number: pass,
|
||||
@@ -75,9 +88,18 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
number: fail,
|
||||
fill: "var(--color-fail)",
|
||||
},
|
||||
],
|
||||
[pass, fail],
|
||||
);
|
||||
];
|
||||
|
||||
if (shouldShowMuted) {
|
||||
data.push({
|
||||
findings: "Muted",
|
||||
number: muted,
|
||||
fill: "var(--color-muted)",
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [pass, fail, muted, shouldShowMuted]);
|
||||
|
||||
const updatedChartData = calculatePercent(chartData);
|
||||
|
||||
@@ -86,6 +108,8 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
[chartData],
|
||||
);
|
||||
|
||||
const hasDataToShow = totalFindings > 0;
|
||||
|
||||
const emptyChartData = [
|
||||
{
|
||||
findings: "Empty",
|
||||
@@ -105,7 +129,7 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Pie
|
||||
data={totalFindings > 0 ? chartData : emptyChartData}
|
||||
data={hasDataToShow ? chartData : emptyChartData}
|
||||
dataKey="number"
|
||||
nameKey="findings"
|
||||
innerRadius={65}
|
||||
@@ -126,7 +150,7 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-xl font-bold"
|
||||
>
|
||||
{totalFindings > 0
|
||||
{hasDataToShow
|
||||
? totalFindings.toLocaleString()
|
||||
: "0"}
|
||||
</tspan>
|
||||
@@ -135,7 +159,7 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
y={(viewBox.cy || 0) + 20}
|
||||
className="fill-foreground text-xs"
|
||||
>
|
||||
Findings
|
||||
{"Findings"}
|
||||
</tspan>
|
||||
</text>
|
||||
);
|
||||
@@ -204,6 +228,46 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shouldShowMuted && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
href="/findings?filter[muted]=true"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Chip
|
||||
className="h-5"
|
||||
variant="flat"
|
||||
startContent={<MutedIcon size={18} />}
|
||||
color="warning"
|
||||
radius="lg"
|
||||
size="md"
|
||||
>
|
||||
{chartData.find((item) => item.findings === "Muted")
|
||||
?.number || 0}
|
||||
</Chip>
|
||||
<span>
|
||||
{updatedChartData.find(
|
||||
(item) => item.findings === "Muted",
|
||||
)?.percent || "0%"}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-xs font-medium leading-none">
|
||||
{muted_new > 0 ? (
|
||||
<>
|
||||
+{muted_new} muted findings from last day{" "}
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
</>
|
||||
) : muted_new < 0 ? (
|
||||
<>{muted_new} muted findings from last day</>
|
||||
) : (
|
||||
"No change from last day"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:root {
|
||||
--chart-success: 146 80% 35%;
|
||||
--chart-fail: 339 90% 51%;
|
||||
--chart-muted: 45 93% 47%;
|
||||
--chart-critical: 336 75% 39%;
|
||||
--chart-high: 339 90% 51%;
|
||||
--chart-medium: 26 100% 55%;
|
||||
@@ -19,6 +20,7 @@
|
||||
.dark {
|
||||
--chart-success: 146 80% 35%;
|
||||
--chart-fail: 339 90% 51%;
|
||||
--chart-muted: 45 93% 47%;
|
||||
--chart-critical: 336 75% 39%;
|
||||
--chart-high: 339 90% 51%;
|
||||
--chart-medium: 26 100% 55%;
|
||||
@@ -32,7 +34,6 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
/* Hide scrollbar */
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
@@ -44,11 +45,10 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
||||
.animate-download-icon polyline,
|
||||
.animate-download-icon line {
|
||||
@apply animate-drop-arrow;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,11 @@ export interface FindingsByStatusData {
|
||||
attributes: {
|
||||
fail: number;
|
||||
pass: number;
|
||||
muted: number;
|
||||
total: number;
|
||||
fail_new: number;
|
||||
pass_new: number;
|
||||
muted_new: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,16 +19,6 @@ export interface CustomDropdownFilterProps {
|
||||
onFilterChange: (key: string, values: string[]) => void;
|
||||
}
|
||||
|
||||
export interface FilterControlsProps {
|
||||
search?: boolean;
|
||||
providers?: boolean;
|
||||
date?: boolean;
|
||||
regions?: boolean;
|
||||
accounts?: boolean;
|
||||
mutedFindings?: boolean;
|
||||
customFilters?: FilterOption[];
|
||||
}
|
||||
|
||||
export enum FilterType {
|
||||
SCAN = "scan__in",
|
||||
PROVIDER_UID = "provider_uid__in",
|
||||
|
||||
Reference in New Issue
Block a user