Skip to content

Commit 79b952c

Browse files
committed
feat: generify
1 parent 82a4341 commit 79b952c

File tree

10 files changed

+573
-667
lines changed

10 files changed

+573
-667
lines changed

public/locales/en.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,8 @@
377377
"progressAvailable": "% Available",
378378
"noResources": "No Resources",
379379
"inactive": "Inactive",
380-
"activate": "Activate"
380+
"activate": "Activate",
381+
"resources": "Resources"
381382
},
382383
"GitOpsHint": {
383384
"title": "Flux",
@@ -386,7 +387,8 @@
386387
"progressAvailable": "% Available",
387388
"noResources": "No Resources",
388389
"inactive": "Inactive",
389-
"activate": "Activate"
390+
"activate": "Activate",
391+
"managed": "Managed"
390392
},
391393
"VaultHint": {
392394
"title": "Vault",
@@ -399,7 +401,8 @@
399401
},
400402
"common": {
401403
"loading": "Loading...",
402-
"errorLoadingResources": "Error loading resources"
404+
"errorLoadingResources": "Error loading resources",
405+
"activate": "Activate"
403406
}
404407
}
405408
}
Lines changed: 9 additions & 313 deletions
Original file line numberDiff line numberDiff line change
@@ -1,314 +1,10 @@
1-
import { Card, CardHeader, Button } from '@ui5/webcomponents-react';
2-
import { RadarChart } from '@ui5/webcomponents-react-charts';
3-
import { useTranslation } from 'react-i18next';
4-
import cx from 'clsx';
5-
import { APIError } from '../../lib/api/error';
6-
import { styles } from './Hints';
7-
import { ManagedResourceItem, Condition } from '../../lib/shared/types';
8-
import { MultiPercentageBar } from '../Shared/MultiPercentageBar';
9-
import React, { useMemo } from 'react';
10-
11-
interface CrossplaneHintProps {
12-
enabled?: boolean;
13-
version?: string;
14-
onActivate?: () => void;
15-
allItems?: ManagedResourceItem[];
16-
isLoading?: boolean;
17-
error?: APIError;
18-
}
19-
20-
export const CrossplaneHint: React.FC<CrossplaneHintProps> = ({
21-
enabled = false,
22-
version,
23-
onActivate,
24-
allItems = [],
25-
isLoading,
26-
error,
27-
}) => {
28-
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;
76-
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-
};
88-
}
89-
90-
const healthyCount = allItems.filter((item: ManagedResourceItem) => {
91-
const conditions = item.status?.conditions || [];
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;
95-
}).length;
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-
110-
return {
111-
totalCount,
112-
healthyCount,
113-
creatingCount,
114-
unhealthyCount,
115-
healthyPercentage,
116-
creatingPercentage,
117-
unhealthyPercentage,
118-
isCurrentlyHealthy
119-
};
120-
}, [allItems]);
121-
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]);
142-
143-
const totalCount = allItems.length;
144-
145-
return (
146-
<div style={{ position: 'relative', width: '100%' }}>
147-
<Card
148-
header={
149-
<CardHeader
150-
additionalText={enabled ? `v${version ?? ''}` : undefined}
151-
avatar={
152-
<img
153-
src="/crossplane-icon.png"
154-
alt="Crossplane"
155-
style={{ width: 50, height: 50, borderRadius: '50%', background: 'transparent', objectFit: 'cover' }}
156-
/>
157-
}
158-
titleText={t('Hints.CrossplaneHint.title')}
159-
subtitleText={t('Hints.CrossplaneHint.subtitle')}
160-
interactive={enabled}
161-
/>
162-
}
163-
className={cx({
164-
[styles['disabled']]: !enabled,
165-
})}
166-
onClick={enabled ? () => {
167-
const el = document.querySelector('.crossplane-table-element');
168-
if (el) {
169-
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
170-
}
171-
} : undefined}
172-
onMouseEnter={enabled ? () => setHovered(true) : undefined}
173-
onMouseLeave={enabled ? () => setHovered(false) : undefined}
174-
>
175-
{/* Disabled overlay */}
176-
{!enabled && <div className={styles.disabledOverlay} />}
177-
178-
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '1rem 0' }}>
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>
262-
</div>
263-
{/* RadarChart for resource healthiness, only show on hover when enabled */}
264-
{enabled && hovered && !isLoading && !error && radarDataset.length > 0 && (
265-
<div style={{
266-
width: '100%',
267-
height: 300,
268-
display: 'flex',
269-
justifyContent: 'center',
270-
alignItems: 'center',
271-
margin: '1rem 0',
272-
overflow: 'visible'
273-
}}>
274-
<RadarChart
275-
dataset={radarDataset}
276-
dimensions={[{ accessor: 'type' }]}
277-
measures={[
278-
{
279-
accessor: 'healthy',
280-
color: '#28a745',
281-
hideDataLabel: true,
282-
label: 'Healthy (%)'
283-
},
284-
{
285-
accessor: 'creating',
286-
color: '#fd7e14',
287-
hideDataLabel: true,
288-
label: 'Creating (%)'
289-
}
290-
]}
291-
style={{ width: '100%', height: '100%', minWidth: 280, minHeight: 280 }}
292-
noLegend={false}
293-
/>
294-
</div>
295-
)}
296-
{!enabled && (
297-
<div
298-
style={{
299-
position: 'absolute',
300-
top: '16px',
301-
right: '16px',
302-
zIndex: 2,
303-
pointerEvents: 'auto',
304-
}}
305-
>
306-
<Button design="Emphasized" onClick={onActivate}>
307-
{t('Hints.CrossplaneHint.activate')}
308-
</Button>
309-
</div>
310-
)}
311-
</Card>
312-
</div>
313-
);
1+
import React from 'react';
2+
import { GenericHint } from './GenericHint';
3+
import { useCrossplaneHintConfig } from './hintConfigs';
4+
import { GenericHintProps } from './types';
5+
6+
// New modular CrossplaneHint using the generic component
7+
export const CrossplaneHint: React.FC<Omit<GenericHintProps, 'config'>> = (props) => {
8+
const config = useCrossplaneHintConfig();
9+
return <GenericHint {...props} config={config} />;
31410
};

0 commit comments

Comments
 (0)