Skip to content

Commit 146de6b

Browse files
authored
feat: add hint section to MCP overview page (#251)
1 parent 0f1008c commit 146de6b

23 files changed

+2274
-3
lines changed

package-lock.json

Lines changed: 296 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@ui5/webcomponents-fiori": "^2.7.2",
4141
"@ui5/webcomponents-icons": "^2.7.2",
4242
"@ui5/webcomponents-react": "^2.7.2",
43+
"@ui5/webcomponents-react-charts": "^2.13.1",
4344
"@xyflow/react": "^12.8.2",
4445
"clsx": "^2.1.1",
4546
"dagre": "^0.8.5",

public/locales/en.json

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
"defaultNamespaceInfo": "Leave empty to use <span>default</span> namespace",
173173
"serviceAccoutsGuide": "You can also use our <link1>Service Account Guide</link1> for more information."
174174
},
175+
175176
"ProjectsPage": {
176177
"header": "Your instances of <span>ManagedControlPlane</span>",
177178
"projectHeader": "Project:"
@@ -185,6 +186,7 @@
185186
"McpPage": {
186187
"accessError": "Managed Control Plane does not have access information yet",
187188
"componentsTitle": "Components",
189+
"overviewTitle": "Overview",
188190
"crossplaneTitle": "Crossplane",
189191
"gitOpsTitle": "GitOps",
190192
"landscapersTitle": "Landscapers",
@@ -334,7 +336,12 @@
334336
"synced": "Synced",
335337
"healthy": "Healthy",
336338
"installed": "Installed",
337-
"none": "None"
339+
"none": "None",
340+
"creating": "Creating",
341+
"unhealthy": "Unhealthy",
342+
"progress": "Managed",
343+
"remaining": "Remaining",
344+
"active": "Active"
338345
},
339346
"errors": {
340347
"installError": "Install error",
@@ -366,5 +373,52 @@
366373
"selectedComponents": "Selected Components",
367374
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.",
368375
"cannotLoad": "Cannot load components list"
376+
},
377+
"Hints": {
378+
"CrossplaneHint": {
379+
"title": "Crossplane",
380+
"subtitle": "Managed Resources Readiness",
381+
"activeStatus": "Active v",
382+
"progressAvailable": "% Available",
383+
"noResources": "No Resources",
384+
"inactive": "Inactive",
385+
"activate": "Activate",
386+
"healthy": "Healthy",
387+
"hoverContent": {
388+
"totalResources": "Total Resources",
389+
"healthy": "Healthy",
390+
"creating": "Creating",
391+
"failing": "Failing"
392+
}
393+
},
394+
"GitOpsHint": {
395+
"title": "Flux",
396+
"subtitle": "GitOps Progress",
397+
"activeStatus": "Active v",
398+
"progressAvailable": "% Available",
399+
"noResources": "No Resources",
400+
"inactive": "Inactive",
401+
"activate": "Activate",
402+
"managed": "Managed",
403+
"hoverContent": {
404+
"totalResources": "Total Resources",
405+
"managed": "Managed",
406+
"unmanaged": "Unmanaged"
407+
}
408+
},
409+
"VaultHint": {
410+
"title": "Vault",
411+
"subtitle": "Rotating Secrets Progress",
412+
"activeStatus": "Active v",
413+
"progressAvailable": "% Available",
414+
"noResources": "No Resources",
415+
"inactive": "Coming soon...",
416+
"activate": "Activate"
417+
},
418+
"common": {
419+
"loading": "Loading...",
420+
"errorLoadingResources": "Error loading resources",
421+
"activate": "Activate"
422+
}
369423
}
370424
}

public/vault.png

24.9 KB
Loading

src/components/Graphs/useGraph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function useGraph(colorBy: ColorBy, onYamlClick: (item: ManagedResourceIt
110110
const status = statusCond?.status === 'True' ? 'OK' : 'ERROR';
111111

112112
let fluxName: string | undefined;
113-
const labelsMap = (item.metadata as { labels?: Record<string, string> }).labels;
113+
const labelsMap = (item.metadata as unknown as { labels?: Record<string, string> }).labels;
114114
if (labelsMap) {
115115
const key = Object.keys(labelsMap).find((k) => k.endsWith('/name'));
116116
if (key) fluxName = labelsMap[key];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* Card Hover Content Styles */
2+
.hoverContent {
3+
width: 100%;
4+
display: flex;
5+
flex-direction: column;
6+
align-items: center;
7+
margin: 1rem 0;
8+
overflow: visible;
9+
}
10+
11+
.chartContainer {
12+
width: 100%;
13+
height: 300px;
14+
display: flex;
15+
justify-content: center;
16+
align-items: center;
17+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, { useMemo } from 'react';
2+
import { RadarChart } from '@ui5/webcomponents-react-charts';
3+
import { LegendSection } from '../LegendSection/LegendSection';
4+
import { styles } from '../HintsCardsRow';
5+
import styles2 from './CardHoverContent.module.css';
6+
import cx from 'clsx';
7+
8+
export interface LegendItem {
9+
label: string;
10+
count: number;
11+
color: string;
12+
}
13+
14+
export interface RadarDataPoint {
15+
[key: string]: string | number;
16+
}
17+
18+
export interface RadarMeasure {
19+
accessor: string;
20+
color: string;
21+
hideDataLabel?: boolean;
22+
label: string;
23+
}
24+
25+
export interface RadarDimension {
26+
accessor: string;
27+
}
28+
29+
export interface HoverContentProps {
30+
enabled: boolean;
31+
totalCount: number;
32+
totalLabel: string;
33+
legendItems: LegendItem[];
34+
radarDataset: RadarDataPoint[];
35+
radarDimensions: RadarDimension[];
36+
radarMeasures: RadarMeasure[];
37+
isLoading?: boolean;
38+
}
39+
40+
// Helper function to truncate labels to max 13 characters
41+
const truncateLabel = (label: string, maxLength: number = 13): string => {
42+
if (label.length <= maxLength) {
43+
return label;
44+
}
45+
return label.substring(0, maxLength) + '...';
46+
};
47+
48+
export const HoverContent: React.FC<HoverContentProps> = ({
49+
enabled,
50+
totalCount,
51+
totalLabel,
52+
legendItems,
53+
radarDataset,
54+
radarDimensions,
55+
radarMeasures,
56+
isLoading = false,
57+
}) => {
58+
// Process the dataset to truncate labels
59+
const processedDataset = useMemo(() => {
60+
return radarDataset.map((dataPoint) => {
61+
const processedDataPoint = { ...dataPoint };
62+
63+
// Truncate labels for each dimension accessor
64+
radarDimensions.forEach((dimension) => {
65+
const value = dataPoint[dimension.accessor];
66+
if (typeof value === 'string') {
67+
processedDataPoint[dimension.accessor] = truncateLabel(value);
68+
}
69+
});
70+
71+
return processedDataPoint;
72+
});
73+
}, [radarDataset, radarDimensions]);
74+
75+
if (!enabled) {
76+
return null;
77+
}
78+
79+
return (
80+
<div className={cx(styles.hoverContent, styles2.hoverContent)}>
81+
<LegendSection title={`${totalCount} ${totalLabel}`} items={legendItems} />
82+
<div className={styles2.chartContainer}>
83+
{isLoading || radarDataset.length === 0 ? (
84+
<div className={cx(styles.hoverContentLoading)}>
85+
<RadarChart
86+
dataset={[]}
87+
dimensions={[
88+
{
89+
accessor: 'name',
90+
formatter: (value: string | number) => String(value || ''),
91+
},
92+
]}
93+
measures={[
94+
{
95+
accessor: 'users',
96+
formatter: (value: string | number) => String(value || ''),
97+
label: 'Users',
98+
},
99+
{
100+
accessor: 'sessions',
101+
formatter: (value: string | number) => String(value || ''),
102+
hideDataLabel: true,
103+
label: 'Active Sessions',
104+
},
105+
{
106+
accessor: 'volume',
107+
label: 'Vol.',
108+
},
109+
]}
110+
style={{ width: '100%', height: '100%', minWidth: 280, minHeight: 280 }}
111+
noLegend={true}
112+
onClick={() => {}}
113+
onDataPointClick={() => {}}
114+
onLegendClick={() => {}}
115+
/>
116+
</div>
117+
) : (
118+
<RadarChart
119+
dataset={processedDataset}
120+
dimensions={radarDimensions}
121+
measures={radarMeasures}
122+
style={{ width: '100%', height: '100%', minWidth: 280, minHeight: 280 }}
123+
noLegend={true}
124+
/>
125+
)}
126+
</div>
127+
</div>
128+
);
129+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ManagedResourceItem, Condition } from '../../../lib/shared/types';
2+
3+
export interface ResourceTypeStats {
4+
type: string;
5+
total: number;
6+
healthy: number;
7+
creating: number;
8+
unhealthy: number;
9+
healthyPercentage: number;
10+
creatingPercentage: number;
11+
unhealthyPercentage: number;
12+
}
13+
14+
export interface OverallStats {
15+
total: number;
16+
healthy: number;
17+
creating: number;
18+
unhealthy: number;
19+
}
20+
21+
export interface CrossplaneHoverData {
22+
resourceTypeStats: ResourceTypeStats[];
23+
overallStats: OverallStats;
24+
}
25+
26+
/**
27+
* Calculate comprehensive statistics for Crossplane hover content
28+
*/
29+
export const calculateCrossplaneHoverData = (allItems: ManagedResourceItem[]): CrossplaneHoverData => {
30+
const typeStats: Record<string, { total: number; healthy: number; creating: number; unhealthy: number }> = {};
31+
let totalHealthy = 0;
32+
let totalCreating = 0;
33+
let totalUnhealthy = 0;
34+
35+
allItems.forEach((item: ManagedResourceItem) => {
36+
const type = item.kind || 'Unknown';
37+
38+
if (!typeStats[type]) {
39+
typeStats[type] = { total: 0, healthy: 0, creating: 0, unhealthy: 0 };
40+
}
41+
42+
typeStats[type].total++;
43+
44+
const conditions = item.status?.conditions || [];
45+
const ready = conditions.find((c: Condition) => c.type === 'Ready' && c.status === 'True');
46+
const synced = conditions.find((c: Condition) => c.type === 'Synced' && c.status === 'True');
47+
48+
if (ready && synced) {
49+
typeStats[type].healthy++;
50+
totalHealthy++;
51+
} else if (synced && !ready) {
52+
// Resource is synced but not ready - it's creating
53+
typeStats[type].creating++;
54+
totalCreating++;
55+
} else {
56+
// Resource has issues or is not synced
57+
typeStats[type].unhealthy++;
58+
totalUnhealthy++;
59+
}
60+
});
61+
62+
const resourceTypeStats: ResourceTypeStats[] = Object.keys(typeStats).map((type) => {
63+
const stats = typeStats[type];
64+
return {
65+
type,
66+
total: stats.total,
67+
healthy: stats.healthy,
68+
creating: stats.creating,
69+
unhealthy: stats.unhealthy,
70+
healthyPercentage: Math.round((stats.healthy / stats.total) * 100),
71+
creatingPercentage: Math.round((stats.creating / stats.total) * 100),
72+
unhealthyPercentage: Math.round((stats.unhealthy / stats.total) * 100),
73+
};
74+
});
75+
76+
return {
77+
resourceTypeStats,
78+
overallStats: {
79+
total: allItems.length,
80+
healthy: totalHealthy,
81+
creating: totalCreating,
82+
unhealthy: totalUnhealthy,
83+
},
84+
};
85+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.container {
2+
position: relative;
3+
width: 100%;
4+
}
5+
6+
.avatar {
7+
width: 50px;
8+
height: 50px;
9+
border-radius: 50%;
10+
background: transparent;
11+
object-fit: cover;
12+
}
13+
14+
.contentContainer {
15+
display: flex;
16+
flex-direction: column;
17+
align-items: center;
18+
padding: 0.5rem 0;
19+
}
20+
21+
.progressBarContainer {
22+
display: flex;
23+
gap: 8px;
24+
width: 100%;
25+
max-width: 500px;
26+
padding: 0 0.5rem;
27+
}
28+
29+
.progressBar {
30+
width: 100%;
31+
}
32+
33+
.activateButton {
34+
position: absolute;
35+
top: 16px;
36+
right: 16px;
37+
z-index: 2;
38+
pointer-events: auto;
39+
}
40+
41+
.activateButtonClickable {
42+
cursor: pointer;
43+
}
44+
45+
.activateButtonDefault {
46+
cursor: default;
47+
}

0 commit comments

Comments
 (0)