Skip to content

Commit 82a4341

Browse files
committed
feat: nice bars!!
1 parent 4939d67 commit 82a4341

File tree

4 files changed

+675
-147
lines changed

4 files changed

+675
-147
lines changed

src/components/Hints/CrossplaneHint.tsx

Lines changed: 191 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Card, CardHeader, ProgressIndicator, Button } from '@ui5/webcomponents-react';
1+
import { Card, CardHeader, Button } from '@ui5/webcomponents-react';
22
import { RadarChart } from '@ui5/webcomponents-react-charts';
33
import { useTranslation } from 'react-i18next';
44
import cx from 'clsx';
55
import { APIError } from '../../lib/api/error';
66
import { styles } from './Hints';
77
import { ManagedResourceItem, Condition } from '../../lib/shared/types';
8-
import React from 'react';
8+
import { MultiPercentageBar } from '../Shared/MultiPercentageBar';
9+
import React, { useMemo } from 'react';
910

1011
interface 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

Comments
 (0)