1- import { Card , CardHeader , ProgressIndicator , Button } from '@ui5/webcomponents-react' ;
1+ import { Card , CardHeader , Button } from '@ui5/webcomponents-react' ;
22import { RadarChart } from '@ui5/webcomponents-react-charts' ;
33import { useTranslation } from 'react-i18next' ;
44import cx from 'clsx' ;
55import { APIError } from '../../lib/api/error' ;
66import { styles } from './Hints' ;
77import { ManagedResourceItem , Condition } from '../../lib/shared/types' ;
8- import React from 'react' ;
8+ import { MultiPercentageBar } from '../Shared/MultiPercentageBar' ;
9+ import React , { useMemo } from 'react' ;
910
1011interface CrossplaneHintProps {
1112 enabled ?: boolean ;
@@ -25,68 +26,122 @@ export const CrossplaneHint: React.FC<CrossplaneHintProps> = ({
2526 error,
2627} ) => {
2728 const { t } = useTranslation ( ) ;
29+ const [ hovered , setHovered ] = React . useState ( false ) ;
30+
31+ // Memoize resource type health calculations
32+ const { resourceTypeHealth, resourceTypeTotal } = useMemo ( ( ) => {
33+ const typeHealth : Record < string , number > = { } ;
34+ const typeTotal : Record < string , number > = { } ;
35+
36+ allItems . forEach ( ( item : ManagedResourceItem ) => {
37+ const type = item . kind || 'Unknown' ;
38+ typeTotal [ type ] = ( typeTotal [ type ] || 0 ) + 1 ;
39+ const conditions = item . status ?. conditions || [ ] ;
40+ const ready = conditions . find ( ( c : Condition ) => c . type === 'Ready' && c . status === 'True' ) ;
41+ const synced = conditions . find ( ( c : Condition ) => c . type === 'Synced' && c . status === 'True' ) ;
42+ if ( ready && synced ) {
43+ typeHealth [ type ] = ( typeHealth [ type ] || 0 ) + 1 ;
44+ }
45+ } ) ;
46+
47+ return { resourceTypeHealth : typeHealth , resourceTypeTotal : typeTotal } ;
48+ } , [ allItems ] ) ;
49+
50+ // Memoize radar chart dataset
51+ const radarDataset = useMemo ( ( ) => {
52+ return Object . keys ( resourceTypeTotal ) . map ( type => {
53+ const total = resourceTypeTotal [ type ] ;
54+ const healthy = resourceTypeHealth [ type ] || 0 ;
55+
56+ // Count creating resources (no conditions yet or unknown status)
57+ const creating = allItems . filter ( ( item : ManagedResourceItem ) => {
58+ if ( item . kind !== type ) return false ;
59+ const conditions = item . status ?. conditions || [ ] ;
60+ const hasReadyCondition = conditions . some ( ( c : Condition ) => c . type === 'Ready' ) ;
61+ const hasSyncedCondition = conditions . some ( ( c : Condition ) => c . type === 'Synced' ) ;
62+ return ! hasReadyCondition || ! hasSyncedCondition ;
63+ } ) . length ;
64+
65+ return {
66+ type,
67+ healthy : Math . round ( ( healthy / total ) * 100 ) ,
68+ creating : Math . round ( ( creating / total ) * 100 )
69+ } ;
70+ } ) ;
71+ } , [ allItems , resourceTypeHealth , resourceTypeTotal ] ) ;
72+
73+ // Memoize health status calculations
74+ const healthStats = useMemo ( ( ) => {
75+ const totalCount = allItems . length ;
2876
29- // Aggregate healthiness by resource type
30- const resourceTypeHealth : Record < string , number > = { } ;
31- const resourceTypeTotal : Record < string , number > = { } ;
32- allItems . forEach ( ( item : ManagedResourceItem ) => {
33- const type = item . kind || 'Unknown' ;
34- resourceTypeTotal [ type ] = ( resourceTypeTotal [ type ] || 0 ) + 1 ;
35- const conditions = item . status ?. conditions || [ ] ;
36- const ready = conditions . find ( ( c : Condition ) => c . type === 'Ready' && c . status === 'True' ) ;
37- const synced = conditions . find ( ( c : Condition ) => c . type === 'Synced' && c . status === 'True' ) ;
38- if ( ready && synced ) {
39- resourceTypeHealth [ type ] = ( resourceTypeHealth [ type ] || 0 ) + 1 ;
77+ if ( totalCount === 0 ) {
78+ return {
79+ totalCount : 0 ,
80+ healthyCount : 0 ,
81+ creatingCount : 0 ,
82+ unhealthyCount : 0 ,
83+ healthyPercentage : 0 ,
84+ creatingPercentage : 0 ,
85+ unhealthyPercentage : 0 ,
86+ isCurrentlyHealthy : false
87+ } ;
4088 }
41- } ) ;
4289
43- // Prepare radar chart dataset: each resource type is a dimension, values are counts for healthy and creating
44- const radarDataset = Object . keys ( resourceTypeTotal ) . map ( type => {
45- const total = resourceTypeTotal [ type ] ;
46- const healthy = resourceTypeHealth [ type ] || 0 ;
47-
48- // Count creating resources (no conditions yet or unknown status)
49- const creating = allItems . filter ( ( item : ManagedResourceItem ) => {
50- if ( item . kind !== type ) return false ;
90+ const healthyCount = allItems . filter ( ( item : ManagedResourceItem ) => {
5191 const conditions = item . status ?. conditions || [ ] ;
52- const hasReadyCondition = conditions . some ( ( c : Condition ) => c . type === 'Ready' ) ;
53- const hasSyncedCondition = conditions . some ( ( c : Condition ) => c . type === 'Synced' ) ;
54- return ! hasReadyCondition || ! hasSyncedCondition ;
92+ const ready = conditions . find ( ( c : Condition ) => c . type === 'Ready' && c . status === 'True ') ;
93+ const synced = conditions . find ( ( c : Condition ) => c . type === 'Synced' && c . status === 'True ') ;
94+ return ! ! ready && ! ! synced ;
5595 } ) . length ;
56-
96+
97+ const creatingCount = allItems . filter ( ( item : ManagedResourceItem ) => {
98+ const conditions = item . status ?. conditions || [ ] ;
99+ const ready = conditions . find ( ( c : Condition ) => c . type === 'Ready' && c . status === 'True' ) ;
100+ const synced = conditions . find ( ( c : Condition ) => c . type === 'Synced' && c . status === 'True' ) ;
101+ return ! ! synced && ! ready ;
102+ } ) . length ;
103+
104+ const unhealthyCount = totalCount - healthyCount - creatingCount ;
105+ const healthyPercentage = Math . round ( ( healthyCount / totalCount ) * 100 ) ;
106+ const creatingPercentage = Math . round ( ( creatingCount / totalCount ) * 100 ) ;
107+ const unhealthyPercentage = Math . round ( ( unhealthyCount / totalCount ) * 100 ) ;
108+ const isCurrentlyHealthy = healthyPercentage === 100 && totalCount > 0 ;
109+
57110 return {
58- type,
59- healthy : Math . round ( ( healthy / total ) * 100 ) ,
60- creating : Math . round ( ( creating / total ) * 100 )
111+ totalCount,
112+ healthyCount,
113+ creatingCount,
114+ unhealthyCount,
115+ healthyPercentage,
116+ creatingPercentage,
117+ unhealthyPercentage,
118+ isCurrentlyHealthy
61119 } ;
62- } ) ;
120+ } , [ allItems ] ) ;
63121
64- // Progress bar logic (unchanged)
65- const healthyCount = allItems . filter ( ( item : ManagedResourceItem ) => {
66- const conditions = item . status ?. conditions || [ ] ;
67- const ready = conditions . find ( ( c : Condition ) => c . type === 'Ready' && c . status === 'True' ) ;
68- const synced = conditions . find ( ( c : Condition ) => c . type === 'Synced' && c . status === 'True' ) ;
69- return ! ! ready && ! ! synced ;
70- } ) . length ;
122+ // Memoize segments for the percentage bar
123+ const segments = useMemo ( ( ) => {
124+ return [
125+ {
126+ percentage : healthStats . healthyPercentage ,
127+ color : '#28a745' ,
128+ label : 'Healthy'
129+ } ,
130+ {
131+ percentage : healthStats . creatingPercentage ,
132+ color : '#e9730c' ,
133+ label : 'Creating'
134+ } ,
135+ {
136+ percentage : healthStats . unhealthyPercentage ,
137+ color : '#d22020ff' ,
138+ label : 'Unhealthy'
139+ }
140+ ] ;
141+ } , [ healthStats ] ) ;
71142
72143 const totalCount = allItems . length ;
73144
74- const progressValue = totalCount > 0 ? Math . round ( ( healthyCount / totalCount ) * 100 ) : 0 ;
75- const progressDisplay = enabled
76- ? allItems . length > 0
77- ? `${ Math . round ( ( healthyCount / totalCount ) * 100 ) } ${ t ( 'Hints.CrossplaneHint.progressAvailable' ) } `
78- : t ( 'Hints.CrossplaneHint.noResources' )
79- : t ( 'Hints.CrossplaneHint.inactive' ) ;
80- const progressValueState = enabled
81- ? allItems . length > 0
82- ? healthyCount >= totalCount / 2 && totalCount > 0
83- ? 'Positive'
84- : 'Critical'
85- : 'None'
86- : 'None' ;
87-
88- const [ hovered , setHovered ] = React . useState ( false ) ;
89-
90145 return (
91146 < div style = { { position : 'relative' , width : '100%' } } >
92147 < Card
@@ -121,40 +176,89 @@ export const CrossplaneHint: React.FC<CrossplaneHintProps> = ({
121176 { ! enabled && < div className = { styles . disabledOverlay } /> }
122177
123178 < div style = { { display : 'flex' , flexDirection : 'column' , alignItems : 'center' , padding : '1rem 0' } } >
124- { isLoading ? (
125- < ProgressIndicator
126- value = { 0 }
127- displayValue = { t ( 'Hints.common.loading' ) }
128- valueState = "None"
129- style = { {
130- width : '80%' ,
131- maxWidth : 500 ,
132- minWidth : 120 ,
133- } }
134- />
135- ) : error ? (
136- < ProgressIndicator
137- value = { 0 }
138- displayValue = { t ( 'Hints.common.errorLoadingResources' ) }
139- valueState = "Negative"
140- style = { {
141- width : '80%' ,
142- maxWidth : 500 ,
143- minWidth : 120 ,
144- } }
145- />
146- ) : (
147- < ProgressIndicator
148- value = { progressValue }
149- displayValue = { progressDisplay }
150- valueState = { progressValueState }
151- style = { {
152- width : '80%' ,
153- maxWidth : 500 ,
154- minWidth : 120 ,
155- } }
156- />
157- ) }
179+ < div style = { {
180+ display : 'flex' ,
181+ gap : '8px' ,
182+ width : '100%' ,
183+ maxWidth : 500 ,
184+ padding : '0 1rem'
185+ } } >
186+ { ( ( ) => {
187+ if ( isLoading ) {
188+ return (
189+ < MultiPercentageBar
190+ segments = { [ {
191+ percentage : 100 ,
192+ color : '#e9e9e9ff' ,
193+ label : 'Loading'
194+ } ] }
195+ style = { { width : '100%' } }
196+ label = { t ( 'Hints.common.loading' ) }
197+ showPercentage = { false }
198+ isHealthy = { false }
199+ />
200+ ) ;
201+ }
202+
203+ if ( error ) {
204+ return (
205+ < MultiPercentageBar
206+ segments = { [ {
207+ percentage : 100 ,
208+ color : '#d22020ff' ,
209+ label : 'Error'
210+ } ] }
211+ style = { { width : '100%' } }
212+ label = { t ( 'Hints.common.errorLoadingResources' ) }
213+ showPercentage = { false }
214+ isHealthy = { false }
215+ />
216+ ) ;
217+ }
218+
219+ if ( ! enabled ) {
220+ return (
221+ < MultiPercentageBar
222+ segments = { [ {
223+ percentage : 100 ,
224+ color : '#e9e9e9ff' ,
225+ label : 'Inactive'
226+ } ] }
227+ style = { { width : '100%' } }
228+ label = { t ( 'Hints.CrossplaneHint.inactive' ) }
229+ showPercentage = { false }
230+ isHealthy = { false }
231+ />
232+ ) ;
233+ }
234+
235+ if ( totalCount === 0 ) {
236+ return (
237+ < MultiPercentageBar
238+ segments = { [ {
239+ percentage : 100 ,
240+ color : '#e9e9e9ff' ,
241+ label : 'No Resources'
242+ } ] }
243+ style = { { width : '100%' } }
244+ label = { t ( 'Hints.CrossplaneHint.noResources' ) }
245+ showPercentage = { false }
246+ isHealthy = { false }
247+ />
248+ ) ;
249+ }
250+
251+ return (
252+ < MultiPercentageBar
253+ segments = { segments }
254+ style = { { width : '100%' } }
255+ label = "Resources"
256+ showPercentage = { true }
257+ isHealthy = { healthStats . isCurrentlyHealthy }
258+ />
259+ ) ;
260+ } ) ( ) }
261+ </ div >
158262 </ div >
159263 { /* RadarChart for resource healthiness, only show on hover when enabled */ }
160264 { enabled && hovered && ! isLoading && ! error && radarDataset . length > 0 && (
0 commit comments