feat: Include/exclude muted findings (#8228)

This commit is contained in:
Alejandro Bailo
2025-07-09 16:06:05 +02:00
committed by GitHub
parent a319f80701
commit 8d2f6aa30c
9 changed files with 151 additions and 46 deletions

View File

@@ -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));
}
});

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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 && (

View File

@@ -38,6 +38,7 @@ export const FindingsFilters = ({
<FilterControls
search
date
mutedFindings
customFilters={[
...filterFindings,
{

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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;
};
};

View File

@@ -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",