mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(ui): add interactive charts with filter navigation (#9333)
This commit is contained in:
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -128,23 +128,41 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) {
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
{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 (
|
||||
<MultiSelectItem
|
||||
key={id}
|
||||
value={id}
|
||||
badgeLabel={displayName}
|
||||
aria-label={`${displayName} account (${providerType.toUpperCase()})`}
|
||||
>
|
||||
<span aria-hidden="true">{icon}</span>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</MultiSelectItem>
|
||||
);
|
||||
})
|
||||
<>
|
||||
<div
|
||||
role="option"
|
||||
aria-selected={selectedIds.length === 0}
|
||||
aria-label="Select all accounts (clears current selection to show all)"
|
||||
tabIndex={0}
|
||||
className="text-text-neutral-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-4 py-3 text-sm font-semibold hover:bg-slate-200 dark:hover:bg-slate-700/50"
|
||||
onClick={() => handleMultiValueChange([])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleMultiValueChange([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Select All
|
||||
</div>
|
||||
{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 (
|
||||
<MultiSelectItem
|
||||
key={id}
|
||||
value={id}
|
||||
badgeLabel={displayName}
|
||||
aria-label={`${displayName} account (${providerType.toUpperCase()})`}
|
||||
>
|
||||
<span aria-hidden="true">{icon}</span>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</MultiSelectItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<div className="px-3 py-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{selectedTypesList.length > 0
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<TabId>("findings");
|
||||
const contentRef = useRef<HTMLDivElement>(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) => {
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{GRAPH_TABS.map((tab) =>
|
||||
activeTab === tab.id ? (
|
||||
<TabsContent
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="mt-10 flex flex-1 overflow-visible"
|
||||
>
|
||||
{tabsContent[tab.id]}
|
||||
</TabsContent>
|
||||
) : null,
|
||||
)}
|
||||
<div ref={contentRef}>
|
||||
{GRAPH_TABS.map((tab) =>
|
||||
activeTab === tab.id ? (
|
||||
<TabsContent
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="mt-10 flex flex-1 overflow-visible"
|
||||
>
|
||||
{tabsContent[tab.id]}
|
||||
</TabsContent>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const GRAPH_TABS = [
|
||||
{
|
||||
id: "findings",
|
||||
label: "Findings",
|
||||
label: "New Findings",
|
||||
},
|
||||
{
|
||||
id: "risk-pipeline",
|
||||
|
||||
@@ -195,17 +195,35 @@ export const ProviderTypeSelector = ({
|
||||
</MultiSelectTrigger>
|
||||
<MultiSelectContent search={false}>
|
||||
{availableTypes.length > 0 ? (
|
||||
availableTypes.map((providerType) => (
|
||||
<MultiSelectItem
|
||||
key={providerType}
|
||||
value={providerType}
|
||||
badgeLabel={PROVIDER_DATA[providerType].label}
|
||||
aria-label={`${PROVIDER_DATA[providerType].label} provider`}
|
||||
<>
|
||||
<div
|
||||
role="option"
|
||||
aria-selected={selectedTypes.length === 0}
|
||||
aria-label="Select all providers (clears current selection to show all)"
|
||||
tabIndex={0}
|
||||
className="text-text-neutral-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-4 py-3 text-sm font-semibold hover:bg-slate-200 dark:hover:bg-slate-700/50"
|
||||
onClick={() => handleMultiValueChange([])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleMultiValueChange([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">{renderIcon(providerType)}</span>
|
||||
<span>{PROVIDER_DATA[providerType].label}</span>
|
||||
</MultiSelectItem>
|
||||
))
|
||||
Select All
|
||||
</div>
|
||||
{availableTypes.map((providerType) => (
|
||||
<MultiSelectItem
|
||||
key={providerType}
|
||||
value={providerType}
|
||||
badgeLabel={PROVIDER_DATA[providerType].label}
|
||||
aria-label={`${PROVIDER_DATA[providerType].label} provider`}
|
||||
>
|
||||
<span aria-hidden="true">{renderIcon(providerType)}</span>
|
||||
<span>{PROVIDER_DATA[providerType].label}</span>
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="px-3 py-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
No connected providers available
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 (
|
||||
<WatchlistCard
|
||||
title="Service Watchlist"
|
||||
items={sortedItems}
|
||||
ctaLabel="Services Dashboard"
|
||||
ctaHref="/services"
|
||||
headerAction={
|
||||
<SortToggleButton
|
||||
isAscending={isAsc}
|
||||
@@ -40,9 +51,8 @@ export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => {
|
||||
}
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
description: "to add services to your watchlist.",
|
||||
linkText: "Services Dashboard",
|
||||
}}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,14 +51,15 @@ export interface WatchlistCardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
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 (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[405px] min-w-[312px] flex-col justify-between"
|
||||
>
|
||||
<Card variant="base" className="flex min-h-[405px] min-w-[312px] flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{headerAction}
|
||||
@@ -93,7 +92,7 @@ export const WatchlistCard = ({
|
||||
|
||||
{/* Description with link */}
|
||||
<p className="text-text-neutral-tertiary w-full text-sm leading-6">
|
||||
{emptyState?.description && (
|
||||
{emptyState?.description && ctaHref && (
|
||||
<>
|
||||
Visit the{" "}
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
@@ -122,12 +121,25 @@ export const WatchlistCard = ({
|
||||
? getScoreTextColor(numericValue)
|
||||
: "text-text-neutral-tertiary";
|
||||
|
||||
const isClickable = !!onItemClick;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
role={isClickable ? "button" : undefined}
|
||||
tabIndex={isClickable ? 0 : undefined}
|
||||
onClick={() => onItemClick?.(item)}
|
||||
onKeyDown={(e) => {
|
||||
if (isClickable && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
onItemClick?.(item);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-[54px] items-center justify-between gap-2 px-3 py-[11px]",
|
||||
!isLast && "border-border-neutral-tertiary border-b",
|
||||
isClickable &&
|
||||
"hover:bg-bg-neutral-tertiary cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<div className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white">
|
||||
@@ -154,11 +166,13 @@ export const WatchlistCard = ({
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="mb-6">
|
||||
<Button variant="link" size="link-sm" asChild className="w-full">
|
||||
<Link href={ctaHref}>{ctaLabel}</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
{ctaLabel && ctaHref && (
|
||||
<CardFooter className="mb-6">
|
||||
<Button variant="link" size="link-sm" asChild className="w-full">
|
||||
<Link href={ctaHref}>{ctaLabel}</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<ContentLayout title="Findings" icon="lucide:tag">
|
||||
<FindingsFilters
|
||||
providerUIDs={providerUIDs}
|
||||
providerIds={providerIds}
|
||||
providerDetails={providerDetails}
|
||||
completedScans={completedScans || []}
|
||||
completedScanIds={completedScanIds}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ThreatScoreSSR,
|
||||
} from "./_new-overview/components/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
ServiceWatchlistSSR,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./_new-overview/components/watchlist";
|
||||
@@ -51,18 +50,13 @@ export default async function Home({
|
||||
<Suspense fallback={<RiskSeverityChartSkeleton />}>
|
||||
<RiskSeverityChartSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 md:flex-row">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Suspense fallback={<RiskPipelineViewSkeleton />}>
|
||||
<GraphsTabsWrapper searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex items-center gap-6"
|
||||
style={{ cursor: isClickable ? "pointer" : "default" }}
|
||||
className={cn(
|
||||
"flex items-center gap-6",
|
||||
isClickable && "cursor-pointer",
|
||||
)}
|
||||
role={isClickable ? "button" : undefined}
|
||||
tabIndex={isClickable ? 0 : undefined}
|
||||
onMouseEnter={() => !isEmpty && setHoveredIndex(index)}
|
||||
|
||||
@@ -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<string, string> = Object.entries(
|
||||
PROVIDER_DISPLAY_NAMES,
|
||||
).reduce(
|
||||
(acc, [type, displayName]) => {
|
||||
acc[displayName] = type;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
interface SankeyNode {
|
||||
name: string;
|
||||
newFindings?: number;
|
||||
@@ -105,6 +118,7 @@ interface CustomLinkProps {
|
||||
onLinkHover?: (index: number, data: Omit<LinkTooltipState, "show">) => 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 (
|
||||
<g>
|
||||
<path
|
||||
@@ -355,6 +376,7 @@ const CustomLink = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
@@ -362,6 +384,7 @@ const CustomLink = ({
|
||||
|
||||
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
|
||||
const [colors, setColors] = useState<Record<string, string>>({});
|
||||
const [linkTooltip, setLinkTooltip] = useState<LinkTooltipState>({
|
||||
@@ -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"
|
||||
>,
|
||||
) => (
|
||||
<CustomLink
|
||||
@@ -462,6 +510,7 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
onLinkHover={handleLinkHover}
|
||||
onLinkMove={handleLinkMove}
|
||||
onLinkLeave={handleLinkLeave}
|
||||
onLinkClick={handleLinkClick}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
Fail: "FAIL",
|
||||
Pass: "PASS",
|
||||
};
|
||||
|
||||
export function ThreatMap({
|
||||
data,
|
||||
height = MAP_CONFIG.defaultHeight,
|
||||
}: ThreatMapProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedLocation, setSelectedLocation] =
|
||||
@@ -559,7 +570,28 @@ export function ThreatMap({
|
||||
Findings
|
||||
</p>
|
||||
</div>
|
||||
<HorizontalBarChart data={selectedLocation.severityData} />
|
||||
<HorizontalBarChart
|
||||
data={selectedLocation.severityData}
|
||||
onBarClick={(dataPoint) => {
|
||||
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()}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState />
|
||||
|
||||
@@ -18,6 +18,7 @@ export const ScansFilters = ({
|
||||
providerUIDs,
|
||||
providerDetails,
|
||||
enableScanRelation: false,
|
||||
providerFilterType: FilterType.PROVIDER_UID,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<string[]>(completedScanIds);
|
||||
const [availableProviderUIDs, setAvailableProviderUIDs] =
|
||||
useState<string[]>(providerUIDs);
|
||||
|
||||
// Use providerIds if provided (for findings), otherwise use providerUIDs (for scans)
|
||||
const providers = providerIds.length > 0 ? providerIds : providerUIDs;
|
||||
const [availableProviders, setAvailableProviders] =
|
||||
useState<string[]>(providers);
|
||||
const previousProviders = useRef<string[]>([]);
|
||||
const previousProviderTypes = useRef<ProviderType[]>([]);
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user