Compare commits

...

1 Commits

Author SHA1 Message Date
Alan Buscaglia
078d4de2be feat(ui): add clickable resource count in scans table with filter badge
- Make resource count in Scan Jobs table clickable, linking to Resources filtered by scan
- Create generic ActiveFilterBadge component for displaying active URL filters
- Add ScanFilterBadge and CheckIdFilterBadge as pre-configured adapters
- Remove old ActiveCheckIdFilter in favor of generic component
- Show active scan filter badge in Resources view for better UX
- Fix any type in ScanDetailsCell props
2025-12-10 09:59:49 +01:00
5 changed files with 121 additions and 43 deletions

View File

@@ -1,35 +0,0 @@
"use client";
import { X } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Badge } from "@/components/shadcn";
import { useUrlFilters } from "@/hooks/use-url-filters";
export const ActiveCheckIdFilter = () => {
const searchParams = useSearchParams();
const { clearFilter } = useUrlFilters();
const checkIdFilter = searchParams.get("filter[check_id__in]");
if (!checkIdFilter) {
return null;
}
const checkIds = checkIdFilter.split(",");
const displayText =
checkIds.length > 1
? `${checkIds.length} Check IDs filtered`
: `Check ID: ${checkIds[0]}`;
return (
<Badge
variant="outline"
className="flex cursor-pointer items-center gap-1 px-3 py-1.5"
onClick={() => clearFilter("check_id__in")}
>
<span className="max-w-[200px] truncate text-sm">{displayText}</span>
<X className="size-3.5 shrink-0" />
</Badge>
);
};

View File

@@ -0,0 +1,90 @@
"use client";
import { X } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Badge } from "@/components/shadcn";
import { useUrlFilters } from "@/hooks/use-url-filters";
export interface ActiveFilterBadgeProps {
/**
* The filter key without the "filter[]" wrapper.
* Example: "scan__in", "check_id__in", "provider__in"
*/
filterKey: string;
/**
* Label to display before the value.
* Example: "Scan", "Check ID", "Provider"
*/
label: string;
/**
* Optional function to format a single value for display.
* Useful for truncating UUIDs, etc.
* Default: shows value as-is
*/
formatValue?: (value: string) => string;
/**
* Optional function to format the display when multiple values are selected.
* Default: "{count} {label}s filtered"
*/
formatMultiple?: (count: number, label: string) => string;
}
export const ActiveFilterBadge = ({
filterKey,
label,
formatValue = (v) => v,
formatMultiple = (count, lbl) => `${count} ${lbl}s filtered`,
}: ActiveFilterBadgeProps) => {
const searchParams = useSearchParams();
const { clearFilter } = useUrlFilters();
const fullKey = filterKey.startsWith("filter[")
? filterKey
: `filter[${filterKey}]`;
const filterValue = searchParams.get(fullKey);
if (!filterValue) {
return null;
}
const values = filterValue.split(",");
const displayText =
values.length > 1
? formatMultiple(values.length, label)
: `${label}: ${formatValue(values[0])}`;
return (
<Badge
variant="outline"
className="flex cursor-pointer items-center gap-1 px-3 py-1.5"
onClick={() => clearFilter(filterKey)}
>
<span className="max-w-[200px] truncate text-sm">{displayText}</span>
<X className="size-3.5 shrink-0" />
</Badge>
);
};
/**
* Pre-configured filter badges for common use cases
*/
export const ScanFilterBadge = () => (
<ActiveFilterBadge
filterKey="scan__in"
label="Scan"
formatValue={(id) => `${id.slice(0, 8)}...`}
/>
);
export const CheckIdFilterBadge = () => (
<ActiveFilterBadge
filterKey="check_id__in"
label="Check ID"
formatMultiple={(count) => `${count} Check IDs filtered`}
/>
);

View File

@@ -1,4 +1,4 @@
export * from "./active-check-id-filter";
export * from "./active-filter-badge";
export * from "./clear-filters-button";
export * from "./custom-account-selection";
export * from "./custom-checkbox-muted-findings";

View File

@@ -1,9 +1,11 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { ColumnDef, Row } from "@tanstack/react-table";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { InfoIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { TableLink } from "@/components/ui/custom";
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
import { TriggerSheet } from "@/components/ui/sheet";
@@ -19,7 +21,7 @@ const getScanData = (row: { original: ScanProps }) => {
return row.original;
};
const ScanDetailsCell = ({ row }: { row: any }) => {
const ScanDetailsCell = ({ row }: { row: Row<ScanProps> }) => {
const router = useRouter();
const searchParams = useSearchParams();
const scanId = searchParams.get("scanId");
@@ -192,11 +194,28 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
),
cell: ({ row }) => {
const {
attributes: { unique_resource_count },
id,
attributes: { unique_resource_count, state },
} = getScanData(row);
const isCompleted = state === "completed";
if (!isCompleted) {
return (
<div className="flex w-fit items-center justify-center">
<span className="text-default-500 text-xs font-medium">
{unique_resource_count ?? "-"}
</span>
</div>
);
}
return (
<div className="flex w-fit items-center justify-center">
<span className="text-xs font-medium">{unique_resource_count}</span>
<Button asChild variant="link" size="sm" className="text-xs">
<Link href={`/resources?filter[scan__in]=${id}`}>
{unique_resource_count}
</Link>
</Button>
</div>
);
},

View File

@@ -3,7 +3,10 @@
import { useSearchParams } from "next/navigation";
import { ComplianceScanInfo } from "@/components/compliance/compliance-header/compliance-scan-info";
import { ActiveCheckIdFilter } from "@/components/filters/active-check-id-filter";
import {
CheckIdFilterBadge,
ScanFilterBadge,
} from "@/components/filters/active-filter-badge";
import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
import {
MultiSelect,
@@ -165,8 +168,9 @@ export const DataTableFilterCustom = ({
</MultiSelect>
);
})}
<div className="flex items-center justify-start gap-2">
<ActiveCheckIdFilter />
<div className="flex flex-wrap items-center justify-start gap-2">
<ScanFilterBadge />
<CheckIdFilterBadge />
<ClearFiltersButton />
</div>
</div>