diff --git a/ui/actions/overview/threat-map.adapter.ts b/ui/actions/overview/threat-map.adapter.ts index fc048963c4..d197f142de 100644 --- a/ui/actions/overview/threat-map.adapter.ts +++ b/ui/actions/overview/threat-map.adapter.ts @@ -6,6 +6,8 @@ export interface ThreatMapLocation { id: string; name: string; region: string; + regionCode: string; + providerType: string; coordinates: [number, number]; totalFindings: number; riskLevel: "low-high" | "high" | "critical"; @@ -173,8 +175,8 @@ function getRiskLevel(failRate: number): "low-high" | "high" | "critical" { } // CSS variables are used for Recharts inline styles, not className -function buildSeverityData(fail: number, pass: number, muted: number) { - const total = fail + pass + muted; +function buildSeverityData(fail: number, pass: number) { + const total = fail + pass; const pct = (value: number) => total > 0 ? Math.round((value / total) * 100) : 0; @@ -191,12 +193,6 @@ function buildSeverityData(fail: number, pass: number, muted: number) { percentage: pct(pass), color: "var(--color-bg-pass)", }, - { - name: "Muted", - value: muted, - percentage: pct(muted), - color: "var(--color-bg-data-muted)", - }, ]; } @@ -254,14 +250,12 @@ export function adaptRegionsOverviewToThreatMap( id, name: formatRegionName(attributes.provider_type, attributes.region), region: providerRegion, + regionCode: attributes.region, + providerType: attributes.provider_type, coordinates, totalFindings: attributes.fail, riskLevel: getRiskLevel(failRate), - severityData: buildSeverityData( - attributes.fail, - attributes.pass, - attributes.muted, - ), + severityData: buildSeverityData(attributes.fail, attributes.pass), }); } diff --git a/ui/app/(prowler)/_new-overview/components/accounts-selector.tsx b/ui/app/(prowler)/_new-overview/components/accounts-selector.tsx index 8cc9f5c49c..1593e6388d 100644 --- a/ui/app/(prowler)/_new-overview/components/accounts-selector.tsx +++ b/ui/app/(prowler)/_new-overview/components/accounts-selector.tsx @@ -128,23 +128,41 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { {visibleProviders.length > 0 ? ( - visibleProviders.map((p) => { - const id = p.id; - const displayName = p.attributes.alias || p.attributes.uid; - const providerType = p.attributes.provider as ProviderType; - const icon = PROVIDER_ICON[providerType]; - return ( - - - {displayName} - - ); - }) + <> +
handleMultiValueChange([])} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleMultiValueChange([]); + } + }} + > + Select All +
+ {visibleProviders.map((p) => { + const id = p.id; + const displayName = p.attributes.alias || p.attributes.uid; + const providerType = p.attributes.provider as ProviderType; + const icon = PROVIDER_ICON[providerType]; + return ( + + + {displayName} + + ); + })} + ) : (
{selectedTypesList.length > 0 diff --git a/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx b/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx index 20e0c5e77f..0e0b2de402 100644 --- a/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx @@ -1,5 +1,4 @@ import { getFindingsByStatus } from "@/actions/overview/overview"; -import { getProviders } from "@/actions/providers"; import { SearchParamsProps } from "@/types"; import { pickFilterParams } from "../../lib/filter-params"; @@ -12,10 +11,7 @@ export const CheckFindingsSSR = async ({ }) => { const filters = pickFilterParams(searchParams); - const [findingsByStatus, providersData] = await Promise.all([ - getFindingsByStatus({ filters }), - getProviders({ page: 1, pageSize: 200 }), - ]); + const findingsByStatus = await getFindingsByStatus({ filters }); if (!findingsByStatus) { return ( @@ -39,7 +35,6 @@ export const CheckFindingsSSR = async ({ total: pass, new: pass_new, }} - providers={providersData?.data} /> ); }; diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx b/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx index ba5064371c..32cfdd6ec8 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx @@ -36,7 +36,16 @@ export async function FindingsViewSSR({ searchParams }: FindingsViewSSRProps) { }; const filters = pickFilterParams(searchParams); - const combinedFilters = { ...defaultFilters, ...filters }; + + // Map provider_id__in to provider__in for findings API + const mappedFilters = { ...filters }; + if (mappedFilters["filter[provider_id__in]"]) { + mappedFilters["filter[provider__in]"] = + mappedFilters["filter[provider_id__in]"]; + delete mappedFilters["filter[provider_id__in]"]; + } + + const combinedFilters = { ...defaultFilters, ...mappedFilters }; const findingsData = await getLatestFindings({ query: undefined, diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx b/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx index 3bc2a4cd90..0dfd999ea4 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx +++ b/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/shadcn"; @@ -12,9 +12,18 @@ interface GraphsTabsClientProps { export const GraphsTabsClient = ({ tabsContent }: GraphsTabsClientProps) => { const [activeTab, setActiveTab] = useState("findings"); + const contentRef = useRef(null); const handleValueChange = (value: string) => { setActiveTab(value as TabId); + + // Scroll to the end of the tab content after a short delay for render + setTimeout(() => { + contentRef.current?.scrollIntoView({ + behavior: "smooth", + block: "end", + }); + }, 100); }; return ( @@ -35,17 +44,19 @@ export const GraphsTabsClient = ({ tabsContent }: GraphsTabsClientProps) => { ))} - {GRAPH_TABS.map((tab) => - activeTab === tab.id ? ( - - {tabsContent[tab.id]} - - ) : null, - )} +
+ {GRAPH_TABS.map((tab) => + activeTab === tab.id ? ( + + {tabsContent[tab.id]} + + ) : null, + )} +
); }; diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts b/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts index f3fb3eaf76..4480efda68 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts +++ b/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts @@ -1,7 +1,7 @@ export const GRAPH_TABS = [ { id: "findings", - label: "Findings", + label: "New Findings", }, { id: "risk-pipeline", diff --git a/ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx b/ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx index 5c6a2a5f43..9dadf46b97 100644 --- a/ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx @@ -195,17 +195,35 @@ export const ProviderTypeSelector = ({ {availableTypes.length > 0 ? ( - availableTypes.map((providerType) => ( - +
handleMultiValueChange([])} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleMultiValueChange([]); + } + }} > - - {PROVIDER_DATA[providerType].label} - - )) + Select All +
+ {availableTypes.map((providerType) => ( + + + {PROVIDER_DATA[providerType].label} + + ))} + ) : (
No connected providers available diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx b/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx index a4de34e1a0..f3aaaa10aa 100644 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx @@ -1,5 +1,4 @@ import { getFindingsBySeverity } from "@/actions/overview/overview"; -import { getProviders } from "@/actions/providers"; import { SearchParamsProps } from "@/types"; import { pickFilterParams } from "../../lib/filter-params"; @@ -12,10 +11,7 @@ export const RiskSeverityChartDetailSSR = async ({ }) => { const filters = pickFilterParams(searchParams); - const [findingsBySeverity, providersData] = await Promise.all([ - getFindingsBySeverity({ filters }), - getProviders({ page: 1, pageSize: 200 }), - ]); + const findingsBySeverity = await getFindingsBySeverity({ filters }); if (!findingsBySeverity) { return ( @@ -40,7 +36,6 @@ export const RiskSeverityChartDetailSSR = async ({ medium={medium} low={low} informational={informational} - providers={providersData?.data} /> ); }; diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx b/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx index 8cdb86f12a..579ae0b220 100644 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx +++ b/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx @@ -11,26 +11,16 @@ import { CardTitle, Skeleton, } from "@/components/shadcn"; +import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; -interface ProviderAttributes { - uid: string; - provider: string; -} - -interface Provider { - id: string; - attributes: ProviderAttributes; -} - interface RiskSeverityChartProps { critical: number; high: number; medium: number; low: number; informational: number; - providers?: Provider[]; } export const RiskSeverityChart = ({ @@ -39,7 +29,6 @@ export const RiskSeverityChart = ({ medium, low, informational, - providers = [], }: RiskSeverityChartProps) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -48,25 +37,7 @@ export const RiskSeverityChart = ({ // Build the URL with current filters plus severity and muted const params = new URLSearchParams(searchParams.toString()); - // Convert filter[provider_id__in] to filter[provider_uid__in] for findings page - const providerIds = params.get("filter[provider_id__in]"); - if (providerIds) { - params.delete("filter[provider_id__in]"); - // Remove provider_type__in since provider_id__in is more specific - params.delete("filter[provider_type__in]"); - - const ids = providerIds.split(","); - const uids = ids - .map((id) => { - const provider = providers.find((p) => p.id === id); - return provider?.attributes.uid; - }) - .filter(Boolean); - - if (uids.length > 0) { - params.set("filter[provider_uid__in]", uids.join(",")); - } - } + mapProviderFiltersForFindings(params); const severity = SEVERITY_FILTER_MAP[dataPoint.name]; if (severity) { diff --git a/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx b/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx index ea6fdde55f..44a9c209c0 100644 --- a/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx +++ b/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx @@ -14,32 +14,21 @@ import { ResourceStatsCard, Skeleton, } from "@/components/shadcn"; +import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; interface FindingsData { total: number; new: number; } -interface ProviderAttributes { - uid: string; - provider: string; -} - -interface Provider { - id: string; - attributes: ProviderAttributes; -} - interface StatusChartProps { failFindingsData: FindingsData; passFindingsData: FindingsData; - providers?: Provider[]; } export const StatusChart = ({ failFindingsData, passFindingsData, - providers = [], }: StatusChartProps) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -51,25 +40,7 @@ export const StatusChart = ({ // Build the URL with current filters plus status and muted const params = new URLSearchParams(searchParams.toString()); - // Convert filter[provider_id__in] to filter[provider_uid__in] for findings page - const providerIds = params.get("filter[provider_id__in]"); - if (providerIds) { - params.delete("filter[provider_id__in]"); - // Remove provider_type__in since provider_id__in is more specific - params.delete("filter[provider_type__in]"); - - const ids = providerIds.split(","); - const uids = ids - .map((id) => { - const provider = providers.find((p) => p.id === id); - return provider?.attributes.uid; - }) - .filter(Boolean); - - if (uids.length > 0) { - params.set("filter[provider_uid__in]", uids.join(",")); - } - } + mapProviderFiltersForFindings(params); // Add status filter based on which segment was clicked if (dataPoint.name === "Fail Findings") { diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx b/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx index 330886e9c6..8d53736a26 100644 --- a/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx +++ b/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx @@ -1,14 +1,18 @@ "use client"; +import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { ServiceOverview } from "@/actions/overview"; +import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { SortToggleButton } from "./sort-toggle-button"; -import { WatchlistCard } from "./watchlist-card"; +import { WatchlistCard, WatchlistItem } from "./watchlist-card"; export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => { - const [isAsc, setIsAsc] = useState(true); + const router = useRouter(); + const searchParams = useSearchParams(); + const [isAsc, setIsAsc] = useState(false); const sortedItems = [...items] .sort((a, b) => @@ -24,12 +28,19 @@ export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => { value: item.attributes.fail, })); + const handleItemClick = (item: WatchlistItem) => { + const params = new URLSearchParams(searchParams.toString()); + + mapProviderFiltersForFindings(params); + + params.set("filter[service__in]", item.key); + router.push(`/findings?${params.toString()}`); + }; + return ( { } emptyState={{ message: "This space is looking empty.", - description: "to add services to your watchlist.", - linkText: "Services Dashboard", }} + onItemClick={handleItemClick} /> ); }; diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx b/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx index 8d38c7695c..9efe0d2bd0 100644 --- a/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx +++ b/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx @@ -51,14 +51,15 @@ export interface WatchlistCardProps extends React.HTMLAttributes { title: string; items: WatchlistItem[]; - ctaLabel: string; - ctaHref: string; + ctaLabel?: string; + ctaHref?: string; headerAction?: React.ReactNode; emptyState?: { message?: string; description?: string; linkText?: string; }; + onItemClick?: (item: WatchlistItem) => void; } export const WatchlistCard = ({ @@ -68,14 +69,12 @@ export const WatchlistCard = ({ ctaHref, headerAction, emptyState, + onItemClick, }: WatchlistCardProps) => { const isEmpty = items.length === 0; return ( - +
{title} {headerAction} @@ -93,7 +92,7 @@ export const WatchlistCard = ({ {/* Description with link */}

- {emptyState?.description && ( + {emptyState?.description && ctaHref && ( <> Visit the{" "} - + {ctaLabel && ctaHref && ( + + + + )} ); }; diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index f41b556375..a25abfe411 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -24,8 +24,8 @@ import { hasDateOrScanFilter, } from "@/lib"; import { - createProviderDetailsMapping, - extractProviderUIDs, + createProviderDetailsMappingById, + extractProviderIds, } from "@/lib/provider-helpers"; import { FilterEntity, ScanEntity, ScanProps } from "@/types"; import { FindingProps, SearchParamsProps } from "@/types/components"; @@ -59,11 +59,11 @@ export default async function Findings({ const uniqueResourceTypes = metadataInfoData?.data?.attributes?.resource_types || []; - // Extract provider UIDs and details using helper functions - const providerUIDs = providersData ? extractProviderUIDs(providersData) : []; + // Extract provider IDs and details using helper functions + const providerIds = providersData ? extractProviderIds(providersData) : []; const providerDetails = providersData - ? (createProviderDetailsMapping(providerUIDs, providersData) as { - [uid: string]: FilterEntity; + ? (createProviderDetailsMappingById(providerIds, providersData) as { + [id: string]: FilterEntity; }[]) : []; @@ -85,7 +85,7 @@ export default async function Findings({ return ( }> + + }> + +

-
-
- }> - - - }> - - -
- +
}> diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx index d94d0c668d..90cdf354a1 100644 --- a/ui/components/findings/findings-filters.tsx +++ b/ui/components/findings/findings-filters.tsx @@ -6,8 +6,8 @@ import { useRelatedFilters } from "@/hooks"; import { FilterEntity, FilterType, ScanEntity, ScanProps } from "@/types"; interface FindingsFiltersProps { - providerUIDs: string[]; - providerDetails: { [uid: string]: FilterEntity }[]; + providerIds: string[]; + providerDetails: { [id: string]: FilterEntity }[]; completedScans: ScanProps[]; completedScanIds: string[]; scanDetails: { [key: string]: ScanEntity }[]; @@ -17,7 +17,7 @@ interface FindingsFiltersProps { } export const FindingsFilters = ({ - providerUIDs, + providerIds, providerDetails, completedScanIds, scanDetails, @@ -25,8 +25,8 @@ export const FindingsFilters = ({ uniqueServices, uniqueResourceTypes, }: FindingsFiltersProps) => { - const { availableProviderUIDs, availableScans } = useRelatedFilters({ - providerUIDs, + const { availableProviderIds, availableScans } = useRelatedFilters({ + providerIds, providerDetails, completedScanIds, scanDetails, @@ -42,9 +42,9 @@ export const FindingsFilters = ({ customFilters={[ ...filterFindings, { - key: FilterType.PROVIDER_UID, - labelCheckboxGroup: "Provider UID", - values: availableProviderUIDs, + key: FilterType.PROVIDER, + labelCheckboxGroup: "Provider", + values: availableProviderIds, valueLabelMapping: providerDetails, index: 6, }, diff --git a/ui/components/graphs/donut-chart.tsx b/ui/components/graphs/donut-chart.tsx index 54dfb406ce..f0f9e3a2a5 100644 --- a/ui/components/graphs/donut-chart.tsx +++ b/ui/components/graphs/donut-chart.tsx @@ -8,20 +8,57 @@ import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart"; import { ChartLegend } from "./shared/chart-legend"; import { DonutDataPoint } from "./types"; +const CHART_COLORS = { + emptyState: "var(--border-neutral-tertiary)", +}; + +interface TooltipPayloadData { + percentage?: number; + change?: number; + color?: string; +} + +interface TooltipPayloadEntry { + name: string; + color?: string; + payload?: TooltipPayloadData; +} + +interface CustomTooltipProps { + active?: boolean; + payload?: TooltipPayloadEntry[]; +} + +interface LegendPayloadData { + percentage?: number; +} + +interface LegendPayloadEntry { + value: string; + color: string; + payload: LegendPayloadData; +} + +interface CustomLegendProps { + payload: LegendPayloadEntry[]; +} + +interface CenterLabel { + value: string | number; + label: string; +} + interface DonutChartProps { data: DonutDataPoint[]; height?: number; innerRadius?: number; outerRadius?: number; showLegend?: boolean; - centerLabel?: { - value: string | number; - label: string; - }; + centerLabel?: CenterLabel; onSegmentClick?: (dataPoint: DonutDataPoint, index: number) => void; } -const CustomTooltip = ({ active, payload }: any) => { +const CustomTooltip = ({ active, payload }: CustomTooltipProps) => { if (!active || !payload || !payload.length) return null; const entry = payload[0]; @@ -58,9 +95,9 @@ const CustomTooltip = ({ active, payload }: any) => { ); }; -const CustomLegend = ({ payload }: any) => { - const items = payload.map((entry: any) => ({ - label: `${entry.value} (${entry.payload.percentage}%)`, +const CustomLegend = ({ payload }: CustomLegendProps) => { + const items = payload.map((entry: LegendPayloadEntry) => ({ + label: `${entry.value} (${entry.payload.percentage ?? 0}%)`, color: entry.color, })); @@ -104,8 +141,8 @@ export function DonutChart({ { name: "No data", value: 1, - fill: "var(--border-neutral-tertiary)", - color: "var(--border-neutral-tertiary)", + fill: CHART_COLORS.emptyState, + color: CHART_COLORS.emptyState, percentage: 0, change: undefined, }, @@ -145,9 +182,9 @@ export function DonutChart({ key={`cell-${index}`} fill={entry.fill} opacity={opacity} + className={isClickable ? "cursor-pointer" : ""} style={{ transition: "opacity 0.2s", - cursor: isClickable ? "pointer" : "default", }} onMouseEnter={() => setHoveredIndex(index)} onMouseLeave={() => setHoveredIndex(null)} diff --git a/ui/components/graphs/horizontal-bar-chart.tsx b/ui/components/graphs/horizontal-bar-chart.tsx index 6dbbe5a186..2f71be423e 100644 --- a/ui/components/graphs/horizontal-bar-chart.tsx +++ b/ui/components/graphs/horizontal-bar-chart.tsx @@ -3,6 +3,8 @@ import { Bell } from "lucide-react"; import { useState } from "react"; +import { cn } from "@/lib/utils"; + import { SEVERITY_ORDER } from "./shared/constants"; import { getSeverityColorByName } from "./shared/utils"; import { BarDataPoint } from "./types"; @@ -62,8 +64,10 @@ export function HorizontalBarChart({ return (
!isEmpty && setHoveredIndex(index)} diff --git a/ui/components/graphs/sankey-chart.tsx b/ui/components/graphs/sankey-chart.tsx index 49f5e29554..e1ee019114 100644 --- a/ui/components/graphs/sankey-chart.tsx +++ b/ui/components/graphs/sankey-chart.tsx @@ -1,15 +1,28 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts"; import { PROVIDER_ICONS } from "@/components/icons/providers-badge"; import { initializeChartColors } from "@/lib/charts/colors"; +import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; +import { PROVIDER_DISPLAY_NAMES } from "@/types/providers"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; import { ChartTooltip } from "./shared/chart-tooltip"; +// Reverse mapping from display name to provider type for URL filters +const PROVIDER_TYPE_MAP: Record = Object.entries( + PROVIDER_DISPLAY_NAMES, +).reduce( + (acc, [type, displayName]) => { + acc[displayName] = type; + return acc; + }, + {} as Record, +); + interface SankeyNode { name: string; newFindings?: number; @@ -105,6 +118,7 @@ interface CustomLinkProps { onLinkHover?: (index: number, data: Omit) => void; onLinkMove?: (position: { x: number; y: number }) => void; onLinkLeave?: () => void; + onLinkClick?: (sourceName: string, targetName: string) => void; } const CustomTooltip = ({ active, payload }: TooltipProps) => { @@ -281,6 +295,7 @@ const CustomLink = ({ onLinkHover, onLinkMove, onLinkLeave, + onLinkClick, }: CustomLinkProps) => { const sourceName = payload.source?.name || ""; const targetName = payload.target?.name || ""; @@ -344,6 +359,12 @@ const CustomLink = ({ onLinkLeave?.(); }; + const handleClick = () => { + if (!isHidden && onLinkClick) { + onLinkClick(sourceName, targetName); + } + }; + return ( ); @@ -362,6 +384,7 @@ const CustomLink = ({ export function SankeyChart({ data, height = 400 }: SankeyChartProps) { const router = useRouter(); + const searchParams = useSearchParams(); const [hoveredLink, setHoveredLink] = useState(null); const [colors, setColors] = useState>({}); const [linkTooltip, setLinkTooltip] = useState({ @@ -428,7 +451,27 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) { const handleNodeClick = (nodeName: string) => { const severityFilter = SEVERITY_FILTER_MAP[nodeName]; if (severityFilter) { - router.push(`/findings?filter[severity]=${severityFilter}`); + const params = new URLSearchParams(searchParams.toString()); + + mapProviderFiltersForFindings(params); + + params.set("filter[severity__in]", severityFilter); + router.push(`/findings?${params.toString()}`); + } + }; + + const handleLinkClick = (sourceName: string, targetName: string) => { + const providerType = PROVIDER_TYPE_MAP[sourceName]; + const severityFilter = SEVERITY_FILTER_MAP[targetName]; + + if (providerType && severityFilter) { + const params = new URLSearchParams(searchParams.toString()); + + mapProviderFiltersForFindings(params); + + params.set("filter[provider_type__in]", providerType); + params.set("filter[severity__in]", severityFilter); + router.push(`/findings?${params.toString()}`); } }; @@ -452,7 +495,12 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) { const wrappedCustomLink = ( props: Omit< CustomLinkProps, - "colors" | "hoveredLink" | "onLinkHover" | "onLinkMove" | "onLinkLeave" + | "colors" + | "hoveredLink" + | "onLinkHover" + | "onLinkMove" + | "onLinkLeave" + | "onLinkClick" >, ) => ( ); diff --git a/ui/components/graphs/threat-map.tsx b/ui/components/graphs/threat-map.tsx index 47e8219d50..e2cadca1f7 100644 --- a/ui/components/graphs/threat-map.tsx +++ b/ui/components/graphs/threat-map.tsx @@ -8,6 +8,7 @@ import type { Geometry, } from "geojson"; import { AlertTriangle, ChevronDown, Info, MapPin } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { feature } from "topojson-client"; import type { @@ -17,6 +18,7 @@ import type { } from "topojson-specification"; import { Card } from "@/components/shadcn/card/card"; +import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { HorizontalBarChart } from "./horizontal-bar-chart"; import { BarDataPoint } from "./types"; @@ -91,6 +93,8 @@ interface LocationPoint { id: string; name: string; region: string; + regionCode: string; + providerType: string; coordinates: [number, number]; totalFindings: number; riskLevel: RiskLevel; @@ -226,10 +230,17 @@ function LoadingState({ height }: { height: number }) { ); } +const STATUS_FILTER_MAP: Record = { + Fail: "FAIL", + Pass: "PASS", +}; + export function ThreatMap({ data, height = MAP_CONFIG.defaultHeight, }: ThreatMapProps) { + const router = useRouter(); + const searchParams = useSearchParams(); const svgRef = useRef(null); const containerRef = useRef(null); const [selectedLocation, setSelectedLocation] = @@ -559,7 +570,28 @@ export function ThreatMap({ Findings

- + { + const status = STATUS_FILTER_MAP[dataPoint.name]; + if (status && selectedLocation.providerType) { + const params = new URLSearchParams(searchParams.toString()); + + mapProviderFiltersForFindings(params); + + params.set( + "filter[provider_type__in]", + selectedLocation.providerType, + ); + params.set( + "filter[region__in]", + selectedLocation.regionCode, + ); + params.set("filter[status__in]", status); + router.push(`/findings?${params.toString()}`); + } + }} + />
) : ( diff --git a/ui/components/scans/scans-filters.tsx b/ui/components/scans/scans-filters.tsx index caeff4e93a..300cd29b68 100644 --- a/ui/components/scans/scans-filters.tsx +++ b/ui/components/scans/scans-filters.tsx @@ -18,6 +18,7 @@ export const ScansFilters = ({ providerUIDs, providerDetails, enableScanRelation: false, + providerFilterType: FilterType.PROVIDER_UID, }); return ( diff --git a/ui/hooks/use-related-filters.ts b/ui/hooks/use-related-filters.ts index 328c723b99..0fc4ce2de5 100644 --- a/ui/hooks/use-related-filters.ts +++ b/ui/hooks/use-related-filters.ts @@ -12,26 +12,33 @@ import { } from "@/types"; interface UseRelatedFiltersProps { - providerUIDs: string[]; - providerDetails: { [uid: string]: FilterEntity }[]; + providerIds?: string[]; + providerUIDs?: string[]; + providerDetails: { [key: string]: FilterEntity }[]; completedScanIds?: string[]; scanDetails?: { [key: string]: ScanEntity }[]; enableScanRelation?: boolean; + providerFilterType?: FilterType.PROVIDER | FilterType.PROVIDER_UID; } export const useRelatedFilters = ({ - providerUIDs, + providerIds = [], + providerUIDs = [], providerDetails, completedScanIds = [], scanDetails = [], enableScanRelation = false, + providerFilterType = FilterType.PROVIDER, }: UseRelatedFiltersProps) => { const searchParams = useSearchParams(); const { updateFilter } = useUrlFilters(); const [availableScans, setAvailableScans] = useState(completedScanIds); - const [availableProviderUIDs, setAvailableProviderUIDs] = - useState(providerUIDs); + + // Use providerIds if provided (for findings), otherwise use providerUIDs (for scans) + const providers = providerIds.length > 0 ? providerIds : providerUIDs; + const [availableProviders, setAvailableProviders] = + useState(providers); const previousProviders = useRef([]); const previousProviderTypes = useRef([]); const isManualDeselection = useRef(false); @@ -52,13 +59,13 @@ export const useRelatedFilters = ({ return scanDetail ? scanDetail[scanId]?.providerInfo?.provider : null; }; - const getProviderType = (providerUid: string): ProviderType | null => { + const getProviderType = (providerKey: string): ProviderType | null => { const providerDetail = providerDetails.find( - (detail) => Object.keys(detail)[0] === providerUid, + (detail) => Object.keys(detail)[0] === providerKey, ); if (!providerDetail) return null; - const entity = providerDetail[providerUid]; + const entity = providerDetail[providerKey]; if (!isScanEntity(entity as ScanEntity)) { return (entity as ProviderEntity).provider; } @@ -69,9 +76,7 @@ export const useRelatedFilters = ({ const scanParam = enableScanRelation ? searchParams.get(`filter[${FilterType.SCAN}]`) : null; - const providerParam = searchParams.get( - `filter[${FilterType.PROVIDER_UID}]`, - ); + const providerParam = searchParams.get(`filter[${providerFilterType}]`); const providerTypeParam = searchParams.get( `filter[${FilterType.PROVIDER_TYPE}]`, ); @@ -162,25 +167,25 @@ export const useRelatedFilters = ({ // Update available providers if (currentProviderTypes.length > 0) { - const filteredProviderUIDs = providerUIDs.filter((uid) => { - const providerType = getProviderType(uid); + const filteredProviders = providers.filter((key) => { + const providerType = getProviderType(key); return providerType && currentProviderTypes.includes(providerType); }); - setAvailableProviderUIDs(filteredProviderUIDs); + setAvailableProviders(filteredProviders); - const validProviders = currentProviders.filter((uid) => { - const providerType = getProviderType(uid); + const validProviders = currentProviders.filter((key) => { + const providerType = getProviderType(key); return providerType && currentProviderTypes.includes(providerType); }); if (validProviders.length !== currentProviders.length) { updateFilter( - FilterType.PROVIDER_UID, + providerFilterType, validProviders.length > 0 ? validProviders : null, ); } } else { - setAvailableProviderUIDs(providerUIDs); + setAvailableProviders(providers); } // Update available scans @@ -207,7 +212,8 @@ export const useRelatedFilters = ({ }, [searchParams]); return { - availableProviderUIDs, + availableProviderIds: providerIds.length > 0 ? availableProviders : [], + availableProviderUIDs: providerUIDs.length > 0 ? availableProviders : [], availableScans, }; }; diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index 1bcf5d80bb..f3a322602a 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -5,6 +5,22 @@ import { ProviderType, } from "@/types/providers"; +/** + * Maps overview provider filters to findings page provider filters. + * Converts provider_id__in to provider__in and removes provider_type__in + * since provider__in is more specific. + */ +export const mapProviderFiltersForFindings = ( + params: URLSearchParams, +): void => { + const providerIds = params.get("filter[provider_id__in]"); + if (providerIds) { + params.delete("filter[provider_id__in]"); + params.delete("filter[provider_type__in]"); + params.set("filter[provider__in]", providerIds); + } +}; + export const extractProviderUIDs = ( providersData: ProvidersApiResponse, ): string[] => { @@ -19,6 +35,16 @@ export const extractProviderUIDs = ( ); }; +export const extractProviderIds = ( + providersData: ProvidersApiResponse, +): string[] => { + if (!providersData?.data) return []; + + return providersData.data + .map((provider: ProviderProps) => provider.id) + .filter(Boolean); +}; + export const createProviderDetailsMapping = ( providerUIDs: string[], providersData: ProvidersApiResponse, @@ -40,6 +66,25 @@ export const createProviderDetailsMapping = ( }); }; +export const createProviderDetailsMappingById = ( + providerIds: string[], + providersData: ProvidersApiResponse, +): Array<{ [id: string]: ProviderEntity }> => { + if (!providersData?.data) return []; + + return providerIds.map((id) => { + const provider = providersData.data.find((p: ProviderProps) => p.id === id); + + return { + [id]: { + provider: provider?.attributes?.provider || "aws", + uid: provider?.attributes?.uid || "", + alias: provider?.attributes?.alias ?? null, + }, + }; + }); +}; + // Helper function to determine which form type to show export type ProviderFormType = | "selector" diff --git a/ui/types/filters.ts b/ui/types/filters.ts index 7f7986d6e6..ddd1845074 100644 --- a/ui/types/filters.ts +++ b/ui/types/filters.ts @@ -24,6 +24,7 @@ export interface CustomDropdownFilterProps { export enum FilterType { SCAN = "scan__in", + PROVIDER = "provider__in", PROVIDER_UID = "provider_uid__in", PROVIDER_TYPE = "provider_type__in", REGION = "region__in",