11import { getProviderDisplayName } from "@/types/providers" ;
22
3- import {
4- FindingsSeverityOverviewResponse ,
5- ProviderOverview ,
6- ProvidersOverviewResponse ,
7- } from "./types" ;
8-
93export interface SankeyNode {
104 name : string ;
115}
@@ -27,44 +21,16 @@ export interface SankeyData {
2721 zeroDataProviders : ZeroDataProvider [ ] ;
2822}
2923
30- export interface SankeyFilters {
31- providerTypes ?: string [ ] ;
32- /** All selected provider types - used to show missing providers in legend */
33- allSelectedProviderTypes ?: string [ ] ;
24+ export interface SeverityData {
25+ critical : number ;
26+ high : number ;
27+ medium : number ;
28+ low : number ;
29+ informational : number ;
3430}
3531
36- interface AggregatedProvider {
37- id : string ;
38- displayName : string ;
39- pass : number ;
40- fail : number ;
41- }
42-
43- // API can return multiple entries for the same provider type, so we sum their findings
44- function aggregateProvidersByType (
45- providers : ProviderOverview [ ] ,
46- ) : AggregatedProvider [ ] {
47- const aggregated = new Map < string , AggregatedProvider > ( ) ;
48-
49- for ( const provider of providers ) {
50- const { id, attributes } = provider ;
51-
52- const existing = aggregated . get ( id ) ;
53-
54- if ( existing ) {
55- existing . pass += attributes . findings . pass ;
56- existing . fail += attributes . findings . fail ;
57- } else {
58- aggregated . set ( id , {
59- id,
60- displayName : getProviderDisplayName ( id ) ,
61- pass : attributes . findings . pass ,
62- fail : attributes . findings . fail ,
63- } ) ;
64- }
65- }
66-
67- return Array . from ( aggregated . values ( ) ) ;
32+ export interface SeverityByProviderType {
33+ [ providerType : string ] : SeverityData ;
6834}
6935
7036const SEVERITY_ORDER = [
@@ -75,142 +41,122 @@ const SEVERITY_ORDER = [
7541 "Informational" ,
7642] as const ;
7743
44+ const SEVERITY_KEYS : ( keyof SeverityData ) [ ] = [
45+ "critical" ,
46+ "high" ,
47+ "medium" ,
48+ "low" ,
49+ "informational" ,
50+ ] ;
51+
7852/**
79- * Adapts providers overview and findings severity API responses to Sankey chart format.
80- * Severity distribution is calculated proportionally based on each provider's fail count.
53+ * Adapts severity by provider type data to Sankey chart format.
8154 *
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.
55+ * @param severityByProviderType - Severity breakdown per provider type from the API
56+ * @param selectedProviderTypes - Provider types that were selected but may have no data
8757 */
88- export function adaptProvidersOverviewToSankey (
89- providersResponse : ProvidersOverviewResponse | undefined ,
90- severityResponse ?: FindingsSeverityOverviewResponse | undefined ,
91- filters ?: SankeyFilters ,
58+ export function adaptToSankeyData (
59+ severityByProviderType : SeverityByProviderType ,
60+ selectedProviderTypes ?: string [ ] ,
9261) : SankeyData {
93- if ( ! providersResponse ?. data || providersResponse . data . length === 0 ) {
94- return { nodes : [ ] , links : [ ] , zeroDataProviders : [ ] } ;
95- }
96-
97- const aggregatedProviders = aggregateProvidersByType ( providersResponse . data ) ;
98-
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 ;
62+ if ( Object . keys ( severityByProviderType ) . length === 0 ) {
63+ // No data - check if there are selected providers to show as zero-data
64+ const zeroDataProviders : ZeroDataProvider [ ] = (
65+ selectedProviderTypes || [ ]
66+ ) . map ( ( type ) => ( {
67+ id : type . toLowerCase ( ) ,
68+ displayName : getProviderDisplayName ( type ) ,
69+ } ) ) ;
70+ return { nodes : [ ] , links : [ ] , zeroDataProviders } ;
11571 }
11672
117- if ( providersToShow . length === 0 ) {
118- return { nodes : [ ] , links : [ ] , zeroDataProviders : [ ] } ;
73+ // Calculate total fails per provider to identify which have data
74+ const providersWithData : {
75+ id : string ;
76+ displayName : string ;
77+ totalFail : number ;
78+ } [ ] = [ ] ;
79+ const providersWithoutData : ZeroDataProvider [ ] = [ ] ;
80+
81+ for ( const [ providerType , severity ] of Object . entries (
82+ severityByProviderType ,
83+ ) ) {
84+ const totalFail =
85+ severity . critical +
86+ severity . high +
87+ severity . medium +
88+ severity . low +
89+ severity . informational ;
90+
91+ const normalizedType = providerType . toLowerCase ( ) ;
92+
93+ if ( totalFail > 0 ) {
94+ providersWithData . push ( {
95+ id : normalizedType ,
96+ displayName : getProviderDisplayName ( normalizedType ) ,
97+ totalFail,
98+ } ) ;
99+ } else {
100+ providersWithoutData . push ( {
101+ id : normalizedType ,
102+ displayName : getProviderDisplayName ( normalizedType ) ,
103+ } ) ;
104+ }
119105 }
120106
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- ) {
107+ // Add selected provider types that are not in the response at all
108+ if ( selectedProviderTypes && selectedProviderTypes . length > 0 ) {
139109 const existingProviderIds = new Set (
140- aggregatedProviders . map ( ( p ) => p . id . toLowerCase ( ) ) ,
110+ Object . keys ( severityByProviderType ) . map ( ( t ) => t . toLowerCase ( ) ) ,
141111 ) ;
142112
143- for ( const selectedType of filters . allSelectedProviderTypes ) {
113+ for ( const selectedType of selectedProviderTypes ) {
144114 const normalizedType = selectedType . toLowerCase ( ) ;
145115 if ( ! existingProviderIds . has ( normalizedType ) ) {
146- // This provider type was selected but has no data at all
147- zeroDataProviders . push ( {
116+ providersWithoutData . push ( {
148117 id : normalizedType ,
149118 displayName : getProviderDisplayName ( normalizedType ) ,
150119 } ) ;
151120 }
152121 }
153122 }
154123
155- // If no providers have failures, return empty chart with legends
156- if ( providersWithFailures . length === 0 ) {
157- return { nodes : [ ] , links : [ ] , zeroDataProviders } ;
124+ // If no providers have failures, return empty chart with zero-data legends
125+ if ( providersWithData . length === 0 ) {
126+ return { nodes : [ ] , links : [ ] , zeroDataProviders : providersWithoutData } ;
158127 }
159128
160- // Only include providers WITH failures in the chart
161- const providerNodes : SankeyNode [ ] = providersWithFailures . map ( ( p ) => ( {
129+ // Build nodes: providers first, then severities
130+ const providerNodes : SankeyNode [ ] = providersWithData . map ( ( p ) => ( {
162131 name : p . displayName ,
163132 } ) ) ;
164133 const severityNodes : SankeyNode [ ] = SEVERITY_ORDER . map ( ( severity ) => ( {
165134 name : severity ,
166135 } ) ) ;
167136 const nodes = [ ...providerNodes , ...severityNodes ] ;
137+
138+ // Build links
168139 const severityStartIndex = providerNodes . length ;
169140 const links : SankeyLink [ ] = [ ] ;
170141
171- if ( severityResponse ?. data ?. attributes ) {
172- const { critical, high, medium, low, informational } =
173- severityResponse . data . attributes ;
174-
175- const severityValues = [ critical , high , medium , low , informational ] ;
176- const totalSeverity = severityValues . reduce ( ( sum , v ) => sum + v , 0 ) ;
177-
178- if ( totalSeverity > 0 ) {
179- const totalFails = providersWithFailures . reduce (
180- ( sum , p ) => sum + p . fail ,
181- 0 ,
182- ) ;
183-
184- providersWithFailures . forEach ( ( provider , sourceIndex ) => {
185- const providerRatio = provider . fail / totalFails ;
186-
187- severityValues . forEach ( ( severityValue , severityIndex ) => {
188- const value = Math . round ( severityValue * providerRatio ) ;
189-
190- if ( value > 0 ) {
191- links . push ( {
192- source : sourceIndex ,
193- target : severityStartIndex + severityIndex ,
194- value,
195- } ) ;
196- }
197- } ) ;
142+ providersWithData . forEach ( ( provider , sourceIndex ) => {
143+ const severity =
144+ severityByProviderType [ provider . id ] ||
145+ severityByProviderType [ provider . id . toUpperCase ( ) ] ;
146+
147+ if ( severity ) {
148+ SEVERITY_KEYS . forEach ( ( key , severityIndex ) => {
149+ const value = severity [ key ] ;
150+ if ( value > 0 ) {
151+ links . push ( {
152+ source : sourceIndex ,
153+ target : severityStartIndex + severityIndex ,
154+ value,
155+ } ) ;
156+ }
198157 } ) ;
199158 }
200- } else {
201- // Fallback when no severity data available
202- const failNode : SankeyNode = { name : "Fail" } ;
203- nodes . push ( failNode ) ;
204- const failIndex = nodes . length - 1 ;
205-
206- providersWithFailures . forEach ( ( provider , sourceIndex ) => {
207- links . push ( {
208- source : sourceIndex ,
209- target : failIndex ,
210- value : provider . fail ,
211- } ) ;
212- } ) ;
213- }
159+ } ) ;
214160
215- return { nodes, links, zeroDataProviders } ;
161+ return { nodes, links, zeroDataProviders : providersWithoutData } ;
216162}
0 commit comments