Skip to content

Commit 291c54e

Browse files
committed
feat: re-usable hover
1 parent 5dace2d commit 291c54e

File tree

8 files changed

+260
-102
lines changed

8 files changed

+260
-102
lines changed

public/locales/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,12 @@
399399
"noResources": "No Resources",
400400
"inactive": "Inactive",
401401
"activate": "Activate",
402-
"managed": "Managed"
402+
"managed": "Managed",
403+
"hoverContent": {
404+
"totalResources": "Total Resources",
405+
"managed": "Managed",
406+
"unmanaged": "Unmanaged"
407+
}
403408
},
404409
"VaultHint": {
405410
"title": "Vault",

src/components/Hints/CrossplaneHoverContent.tsx

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/components/Hints/GenericHint.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import cx from 'clsx';
55
import { MultiPercentageBar } from './MultiPercentageBar';
66
import { GenericHintProps } from './types';
77
import { styles } from './Hints';
8+
import { HoverContent } from './HoverContent';
89

910
export const GenericHint: React.FC<GenericHintProps> = ({
1011
enabled = false,
@@ -93,6 +94,19 @@ export const GenericHint: React.FC<GenericHintProps> = ({
9394
hovered &&
9495
!isLoading &&
9596
!error &&
97+
config.calculateHoverData && (
98+
(() => {
99+
const hoverData = config.calculateHoverData(allItems, enabled, t);
100+
return hoverData ? <HoverContent enabled={enabled} {...hoverData} /> : null;
101+
})()
102+
)}
103+
104+
{/* Legacy hover content support */}
105+
{enabled &&
106+
hovered &&
107+
!isLoading &&
108+
!error &&
109+
!config.calculateHoverData &&
96110
config.renderHoverContent &&
97111
config.renderHoverContent(allItems, enabled)}
98112

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import { RadarChart } from '@ui5/webcomponents-react-charts';
3+
import { LegendSection } from './LegendSection';
4+
5+
export interface LegendItem {
6+
label: string;
7+
count: number;
8+
color: string;
9+
}
10+
11+
export interface RadarDataPoint {
12+
[key: string]: string | number;
13+
}
14+
15+
export interface RadarMeasure {
16+
accessor: string;
17+
color: string;
18+
hideDataLabel?: boolean;
19+
label: string;
20+
}
21+
22+
export interface RadarDimension {
23+
accessor: string;
24+
}
25+
26+
export interface HoverContentProps {
27+
enabled: boolean;
28+
totalCount: number;
29+
totalLabel: string;
30+
legendItems: LegendItem[];
31+
radarDataset: RadarDataPoint[];
32+
radarDimensions: RadarDimension[];
33+
radarMeasures: RadarMeasure[];
34+
}
35+
36+
export const HoverContent: React.FC<HoverContentProps> = ({
37+
enabled,
38+
totalCount,
39+
totalLabel,
40+
legendItems,
41+
radarDataset,
42+
radarDimensions,
43+
radarMeasures,
44+
}) => {
45+
if (!enabled || radarDataset.length === 0) {
46+
return null;
47+
}
48+
49+
return (
50+
<div
51+
style={{
52+
width: '100%',
53+
display: 'flex',
54+
flexDirection: 'column',
55+
alignItems: 'center',
56+
margin: '1rem 0',
57+
overflow: 'visible',
58+
}}
59+
>
60+
<LegendSection
61+
title={`${totalCount} ${totalLabel}`}
62+
items={legendItems}
63+
/>
64+
<div
65+
style={{
66+
width: '100%',
67+
height: 300,
68+
display: 'flex',
69+
justifyContent: 'center',
70+
alignItems: 'center',
71+
}}
72+
>
73+
<RadarChart
74+
dataset={radarDataset}
75+
dimensions={radarDimensions}
76+
measures={radarMeasures}
77+
style={{ width: '100%', height: '100%', minWidth: 280, minHeight: 280 }}
78+
noLegend={true}
79+
/>
80+
</div>
81+
</div>
82+
);
83+
};

src/components/Hints/calculations.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ManagedResourceItem, Condition } from '../../lib/shared/types';
22
import { APIError } from '../../lib/api/error';
3-
import { HintSegmentCalculator, HintState } from './types';
3+
import { HintSegmentCalculator, HintState, HoverDataCalculator } from './types';
4+
import { HoverContentProps } from './HoverContent';
45

56
/**
67
* Common colors used across all hints
@@ -313,3 +314,132 @@ export const calculateCrossplaneHoverData = (allItems: ManagedResourceItem[]): C
313314
},
314315
};
315316
};
317+
318+
/**
319+
* Calculate hover data for Crossplane using the generic HoverContent structure
320+
* Shows healthy resources (the positive segment)
321+
*/
322+
export const calculateCrossplaneHoverDataGeneric: HoverDataCalculator = (
323+
allItems: ManagedResourceItem[],
324+
enabled: boolean,
325+
t: (key: string) => string,
326+
): Omit<HoverContentProps, 'enabled'> | null => {
327+
if (!enabled || allItems.length === 0) {
328+
return null;
329+
}
330+
331+
const { resourceTypeStats, overallStats } = calculateCrossplaneHoverData(allItems);
332+
333+
// Get the segments from the bar chart calculation to ensure color consistency
334+
const segmentData = calculateCrossplaneSegments(allItems, false, undefined, enabled, t);
335+
336+
const legendItems = segmentData.segments.map(segment => ({
337+
label: segment.label,
338+
count: segment.label === t('common.healthy') ? overallStats.healthy :
339+
segment.label === t('common.creating') ? overallStats.creating :
340+
overallStats.unhealthy,
341+
color: segment.color,
342+
}));
343+
344+
// Focus on healthy percentage in radar chart (the positive aspect)
345+
const radarDataset = resourceTypeStats.map((stats) => ({
346+
type: stats.type,
347+
healthy: stats.healthyPercentage,
348+
}));
349+
350+
// Use the color of the healthy segment (first segment in the bar chart)
351+
const healthyColor = segmentData.segments.find(s => s.label === t('common.healthy'))?.color || HINT_COLORS.healthy;
352+
353+
return {
354+
totalCount: overallStats.total,
355+
totalLabel: t('Hints.CrossplaneHint.hoverContent.totalResources'),
356+
legendItems,
357+
radarDataset,
358+
radarDimensions: [{ accessor: 'type' }],
359+
radarMeasures: [
360+
{
361+
accessor: 'healthy',
362+
color: healthyColor,
363+
hideDataLabel: true,
364+
label: t('Hints.CrossplaneHint.hoverContent.healthy') + ' (%)',
365+
},
366+
],
367+
};
368+
};
369+
370+
/**
371+
* Calculate hover data for GitOps showing resource type management coverage
372+
* Shows managed resources (the positive segment)
373+
*/
374+
export const calculateGitOpsHoverDataGeneric: HoverDataCalculator = (
375+
allItems: ManagedResourceItem[],
376+
enabled: boolean,
377+
t: (key: string) => string,
378+
): Omit<HoverContentProps, 'enabled'> | null => {
379+
if (!enabled || allItems.length === 0) {
380+
return null;
381+
}
382+
383+
// Group by resource type and calculate flux management coverage
384+
const typeStats: Record<string, { total: number; managed: number }> = {};
385+
let totalManaged = 0;
386+
387+
allItems.forEach((item: ManagedResourceItem) => {
388+
const type = item.kind || 'Unknown';
389+
390+
if (!typeStats[type]) {
391+
typeStats[type] = { total: 0, managed: 0 };
392+
}
393+
394+
typeStats[type].total++;
395+
396+
// Check if the resource is managed by Flux
397+
if (
398+
item?.metadata?.labels &&
399+
Object.prototype.hasOwnProperty.call(item.metadata.labels, 'kustomize.toolkit.fluxcd.io/name')
400+
) {
401+
typeStats[type].managed++;
402+
totalManaged++;
403+
}
404+
});
405+
406+
const totalUnmanaged = allItems.length - totalManaged;
407+
408+
// Get the segments from the bar chart calculation to ensure color consistency
409+
const segmentData = calculateGitOpsSegments(allItems, false, undefined, enabled, t);
410+
411+
const legendItems = segmentData.segments.map(segment => ({
412+
label: segment.label,
413+
count: segment.label === t('common.progress') ? totalManaged : totalUnmanaged,
414+
color: segment.color,
415+
}));
416+
417+
// Focus on managed percentage in radar chart (the positive aspect)
418+
const radarDataset = Object.keys(typeStats).map((type) => {
419+
const stats = typeStats[type];
420+
const managedPercentage = Math.round((stats.managed / stats.total) * 100);
421+
return {
422+
type,
423+
managed: managedPercentage,
424+
};
425+
});
426+
427+
// Use the color of the progress/managed segment (first segment in the bar chart)
428+
const managedColor = segmentData.segments.find(s => s.label === t('common.progress'))?.color || HINT_COLORS.managed;
429+
430+
return {
431+
totalCount: allItems.length,
432+
totalLabel: t('Hints.GitOpsHint.hoverContent.totalResources'),
433+
legendItems,
434+
radarDataset,
435+
radarDimensions: [{ accessor: 'type' }],
436+
radarMeasures: [
437+
{
438+
accessor: 'managed',
439+
color: managedColor,
440+
hideDataLabel: true,
441+
label: t('Hints.GitOpsHint.hoverContent.managed') + ' (%)',
442+
},
443+
],
444+
};
445+
};

src/components/Hints/hintConfigs.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import React from 'react';
21
import { useTranslation } from 'react-i18next';
32
import { HintConfig } from './types';
4-
import { calculateCrossplaneSegments, calculateGitOpsSegments, calculateVaultSegments } from './calculations';
5-
import { CrossplaneHoverContent } from './CrossplaneHoverContent';
3+
import {
4+
calculateCrossplaneSegments,
5+
calculateGitOpsSegments,
6+
calculateVaultSegments,
7+
calculateCrossplaneHoverDataGeneric,
8+
calculateGitOpsHoverDataGeneric
9+
} from './calculations';
610

711
export const useCrossplaneHintConfig = (): HintConfig => {
812
const { t } = useTranslation();
@@ -15,9 +19,8 @@ export const useCrossplaneHintConfig = (): HintConfig => {
1519
scrollTarget: '.crossplane-table-element',
1620
calculateSegments: (allItems, isLoading, error, enabled) =>
1721
calculateCrossplaneSegments(allItems, isLoading, error, enabled, t),
18-
renderHoverContent: (allItems, enabled) => {
19-
return React.createElement(CrossplaneHoverContent, { allItems, enabled });
20-
},
22+
calculateHoverData: (allItems, enabled) =>
23+
calculateCrossplaneHoverDataGeneric(allItems, enabled, t),
2124
};
2225
};
2326

@@ -32,6 +35,8 @@ export const useGitOpsHintConfig = (): HintConfig => {
3235
scrollTarget: '.cp-page-section-gitops',
3336
calculateSegments: (allItems, isLoading, error, enabled) =>
3437
calculateGitOpsSegments(allItems, isLoading, error, enabled, t),
38+
calculateHoverData: (allItems, enabled) =>
39+
calculateGitOpsHoverDataGeneric(allItems, enabled, t),
3540
};
3641
};
3742

0 commit comments

Comments
 (0)