Skip to content

Commit 3097513

Browse files
fix(ui): filter Risk Pipeline chart by selected providers and show zero-data legends (#9340)
1 parent 6af9ff4 commit 3097513

File tree

5 files changed

+203
-19
lines changed

5 files changed

+203
-19
lines changed

ui/actions/overview/sankey.adapter.ts

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,21 @@ export interface SankeyLink {
1616
value: number;
1717
}
1818

19+
export interface ZeroDataProvider {
20+
id: string;
21+
displayName: string;
22+
}
23+
1924
export interface SankeyData {
2025
nodes: SankeyNode[];
2126
links: SankeyLink[];
27+
zeroDataProviders: ZeroDataProvider[];
28+
}
29+
30+
export interface SankeyFilters {
31+
providerTypes?: string[];
32+
/** All selected provider types - used to show missing providers in legend */
33+
allSelectedProviderTypes?: string[];
2234
}
2335

2436
interface AggregatedProvider {
@@ -66,22 +78,86 @@ const SEVERITY_ORDER = [
6678
/**
6779
* Adapts providers overview and findings severity API responses to Sankey chart format.
6880
* Severity distribution is calculated proportionally based on each provider's fail count.
81+
*
82+
* @param providersResponse - The providers overview API response
83+
* @param severityResponse - The findings severity API response
84+
* @param filters - Optional filters to restrict which providers are shown.
85+
* When filters are set, only selected providers are shown.
86+
* When no filters, all providers are shown.
6987
*/
7088
export function adaptProvidersOverviewToSankey(
7189
providersResponse: ProvidersOverviewResponse | undefined,
7290
severityResponse?: FindingsSeverityOverviewResponse | undefined,
91+
filters?: SankeyFilters,
7392
): SankeyData {
7493
if (!providersResponse?.data || providersResponse.data.length === 0) {
75-
return { nodes: [], links: [] };
94+
return { nodes: [], links: [], zeroDataProviders: [] };
7695
}
7796

7897
const aggregatedProviders = aggregateProvidersByType(providersResponse.data);
79-
const providersWithFailures = aggregatedProviders.filter((p) => p.fail > 0);
8098

99+
// Filter providers based on selection:
100+
// - If providerTypes filter is set: show only those provider types
101+
// - Otherwise: show all providers from the API response
102+
const hasProviderTypeFilter =
103+
filters?.providerTypes && filters.providerTypes.length > 0;
104+
105+
let providersToShow: AggregatedProvider[];
106+
if (hasProviderTypeFilter) {
107+
// Show only selected provider types
108+
providersToShow = aggregatedProviders.filter((p) =>
109+
filters.providerTypes!.includes(p.id.toLowerCase()),
110+
);
111+
} else {
112+
// No provider type filter - show all providers from the API response
113+
// Providers with no findings (pass=0, fail=0) will appear in the legend
114+
providersToShow = aggregatedProviders;
115+
}
116+
117+
if (providersToShow.length === 0) {
118+
return { nodes: [], links: [], zeroDataProviders: [] };
119+
}
120+
121+
// Separate providers with and without failures
122+
const providersWithFailures = providersToShow.filter((p) => p.fail > 0);
123+
const providersWithoutFailures = providersToShow.filter((p) => p.fail === 0);
124+
125+
// Zero-data providers to show as legends below the chart
126+
const zeroDataProviders: ZeroDataProvider[] = providersWithoutFailures.map(
127+
(p) => ({
128+
id: p.id,
129+
displayName: p.displayName,
130+
}),
131+
);
132+
133+
// Add selected provider types that are completely missing from API response
134+
// (these are providers with zero findings - not even in the response)
135+
if (
136+
filters?.allSelectedProviderTypes &&
137+
filters.allSelectedProviderTypes.length > 0
138+
) {
139+
const existingProviderIds = new Set(
140+
aggregatedProviders.map((p) => p.id.toLowerCase()),
141+
);
142+
143+
for (const selectedType of filters.allSelectedProviderTypes) {
144+
const normalizedType = selectedType.toLowerCase();
145+
if (!existingProviderIds.has(normalizedType)) {
146+
// This provider type was selected but has no data at all
147+
zeroDataProviders.push({
148+
id: normalizedType,
149+
displayName: getProviderDisplayName(normalizedType),
150+
});
151+
}
152+
}
153+
}
154+
155+
// If no providers have failures, return empty chart with legends
81156
if (providersWithFailures.length === 0) {
82-
return { nodes: [], links: [] };
157+
return { nodes: [], links: [], zeroDataProviders };
83158
}
84159

160+
// Only include providers WITH failures in the chart
85161
const providerNodes: SankeyNode[] = providersWithFailures.map((p) => ({
86162
name: p.displayName,
87163
}));
@@ -136,5 +212,5 @@ export function adaptProvidersOverviewToSankey(
136212
});
137213
}
138214

139-
return { nodes, links };
215+
return { nodes, links, zeroDataProviders };
140216
}

ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
adaptProvidersOverviewToSankey,
33
getFindingsBySeverity,
44
getProvidersOverview,
5+
SankeyFilters,
56
} from "@/actions/overview";
7+
import { getProviders } from "@/actions/providers";
68
import { SankeyChart } from "@/components/graphs/sankey-chart";
79
import { SearchParamsProps } from "@/types";
810

@@ -15,30 +17,98 @@ export async function RiskPipelineViewSSR({
1517
}) {
1618
const filters = pickFilterParams(searchParams);
1719

18-
// Fetch both endpoints in parallel
19-
const [providersResponse, severityResponse] = await Promise.all([
20-
getProvidersOverview({ filters }),
21-
getFindingsBySeverity({ filters }),
22-
]);
20+
// Check if any provider/account filter is active
21+
const providerTypeFilter = filters["filter[provider_type__in]"];
22+
const providerIdFilter = filters["filter[provider_id__in]"];
23+
24+
// Fetch data in parallel
25+
const [providersResponse, severityResponse, providersListResponse] =
26+
await Promise.all([
27+
getProvidersOverview({ filters }),
28+
getFindingsBySeverity({ filters }),
29+
// Only fetch providers list if we need to look up account IDs
30+
providerIdFilter && !providerTypeFilter
31+
? getProviders({ pageSize: 200 })
32+
: Promise.resolve(null),
33+
]);
34+
35+
// Determine provider types to show
36+
let providerTypesToShow: string[] | undefined;
37+
38+
if (providerTypeFilter) {
39+
// Provider type filter is set - use it directly
40+
providerTypesToShow = String(providerTypeFilter)
41+
.split(",")
42+
.map((t) => t.trim().toLowerCase());
43+
} else if (providerIdFilter && providersListResponse?.data) {
44+
// Account filter is set - look up provider types from account IDs
45+
const selectedAccountIds = String(providerIdFilter)
46+
.split(",")
47+
.map((id) => id.trim());
48+
49+
const providerTypesSet = new Set<string>();
50+
for (const accountId of selectedAccountIds) {
51+
const provider = providersListResponse.data.find(
52+
(p) => p.id === accountId,
53+
);
54+
if (provider) {
55+
providerTypesSet.add(provider.attributes.provider.toLowerCase());
56+
}
57+
}
58+
providerTypesToShow = Array.from(providerTypesSet);
59+
}
60+
61+
// Build sankey filters
62+
const sankeyFilters: SankeyFilters = {
63+
providerTypes: providerTypesToShow,
64+
allSelectedProviderTypes: providerTypesToShow,
65+
};
2366

2467
const sankeyData = adaptProvidersOverviewToSankey(
2568
providersResponse,
2669
severityResponse,
70+
sankeyFilters,
2771
);
2872

29-
if (sankeyData.nodes.length === 0) {
73+
// If no chart data and no zero-data providers, show empty state message
74+
if (
75+
sankeyData.nodes.length === 0 &&
76+
sankeyData.zeroDataProviders.length === 0
77+
) {
3078
return (
3179
<div className="flex h-[460px] w-full items-center justify-center">
3280
<p className="text-text-neutral-tertiary text-sm">
33-
No provider data available
81+
No findings data available for the selected filters
3482
</p>
3583
</div>
3684
);
3785
}
3886

87+
// If no chart data but there are zero-data providers, show only the legend
88+
if (sankeyData.nodes.length === 0) {
89+
return (
90+
<div className="flex h-[460px] w-full items-center justify-center">
91+
<div className="text-center">
92+
<p className="text-text-neutral-tertiary mb-4 text-sm">
93+
No failed findings for the selected accounts
94+
</p>
95+
<SankeyChart
96+
data={sankeyData}
97+
zeroDataProviders={sankeyData.zeroDataProviders}
98+
height={100}
99+
/>
100+
</div>
101+
</div>
102+
);
103+
}
104+
39105
return (
40106
<div className="w-full flex-1 overflow-visible">
41-
<SankeyChart data={sankeyData} height={460} />
107+
<SankeyChart
108+
data={sankeyData}
109+
zeroDataProviders={sankeyData.zeroDataProviders}
110+
height={460}
111+
/>
42112
</div>
43113
);
44114
}

ui/app/(prowler)/compliance/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export default async function Compliance({
6161

6262
// Find the provider data in the included array
6363
const providerData = scansData.included?.find(
64-
(item: any) => item.type === "providers" && item.id === providerId,
64+
(item: { type: string; id: string }) =>
65+
item.type === "providers" && item.id === providerId,
6566
);
6667

6768
if (!providerData) {

ui/app/(prowler)/scans/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,22 @@ const SSRDataTableScans = async ({
148148
include: "provider",
149149
});
150150

151-
// Process scans with provider information from included data
151+
const scans = scansData?.data;
152+
const included = scansData?.included;
153+
const meta = scansData && "meta" in scansData ? scansData.meta : undefined;
154+
152155
const expandedScansData =
153-
scansData?.data?.map((scan: any) => {
156+
scans?.map((scan: ScanProps) => {
154157
const providerId = scan.relationships?.provider?.data?.id;
155158

156159
if (!providerId) {
157160
return { ...scan, providerInfo: null };
158161
}
159162

160163
// Find the provider data in the included array
161-
const providerData = scansData.included?.find(
162-
(item: any) => item.type === "providers" && item.id === providerId,
164+
const providerData = included?.find(
165+
(item: { type: string; id: string }) =>
166+
item.type === "providers" && item.id === providerId,
163167
);
164168

165169
if (!providerData) {
@@ -181,7 +185,7 @@ const SSRDataTableScans = async ({
181185
key={`scans-${Date.now()}`}
182186
columns={ColumnGetScans}
183187
data={expandedScansData || []}
184-
metadata={scansData?.meta}
188+
metadata={meta}
185189
/>
186190
);
187191
};

ui/components/graphs/sankey-chart.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ interface SankeyLink {
3535
value: number;
3636
}
3737

38+
interface ZeroDataProvider {
39+
id: string;
40+
displayName: string;
41+
}
42+
3843
interface SankeyChartProps {
3944
data: {
4045
nodes: SankeyNode[];
4146
links: SankeyLink[];
4247
};
48+
zeroDataProviders?: ZeroDataProvider[];
4349
height?: number;
4450
}
4551

@@ -382,7 +388,11 @@ const CustomLink = ({
382388
);
383389
};
384390

385-
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
391+
export function SankeyChart({
392+
data,
393+
zeroDataProviders = [],
394+
height = 400,
395+
}: SankeyChartProps) {
386396
const router = useRouter();
387397
const searchParams = useSearchParams();
388398
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
@@ -579,6 +589,29 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
579589
/>
580590
</div>
581591
)}
592+
{zeroDataProviders.length > 0 && (
593+
<div className="border-divider-primary mt-4 border-t pt-4">
594+
<p className="text-text-neutral-tertiary mb-3 text-xs font-medium tracking-wide uppercase">
595+
Providers with no failed findings
596+
</p>
597+
<div className="flex flex-wrap gap-4">
598+
{zeroDataProviders.map((provider) => {
599+
const IconComponent = PROVIDER_ICONS[provider.displayName];
600+
return (
601+
<div
602+
key={provider.id}
603+
className="flex items-center gap-2 text-sm"
604+
>
605+
{IconComponent && <IconComponent width={20} height={20} />}
606+
<span className="text-text-neutral-secondary">
607+
{provider.displayName}
608+
</span>
609+
</div>
610+
);
611+
})}
612+
</div>
613+
</div>
614+
)}
582615
</div>
583616
);
584617
}

0 commit comments

Comments
 (0)