Skip to content

Commit 16c43ca

Browse files
committed
Health overview dashboard card
Signed-off-by: Arun Kumar Mohan <[email protected]>
1 parent 782be0e commit 16c43ca

File tree

4 files changed

+381
-0
lines changed

4 files changed

+381
-0
lines changed

locales/en/plugin__odf-console.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
"Action needed": "Action needed",
189189
"Actions": "Actions",
190190
"Active health checks": "Active health checks",
191+
"Active issues": "Active issues",
191192
"Actively monitoring, waiting for trigger": "Actively monitoring, waiting for trigger",
192193
"Activity": "Activity",
193194
"Activity description": "Activity description",
@@ -1023,6 +1024,9 @@
10231024
"Headers that can be sent by the client in the request.": "Headers that can be sent by the client in the request.",
10241025
"Headers that should be exposed to the browser.": "Headers that should be exposed to the browser.",
10251026
"Health": "Health",
1027+
"Health check metrics are being collected. This may take a few moments.": "Health check metrics are being collected. This may take a few moments.",
1028+
"Health Score": "Health Score",
1029+
"Health Score Over Time": "Health Score Over Time",
10261030
"Healthy": "Healthy",
10271031
"Help": "Help",
10281032
"Hide message": "Hide message",
@@ -1119,6 +1123,7 @@
11191123
"Labels help you organize and select resources. Adding labels below will let you query for objects that have similar, overlapping or dissimilar labels.": "Labels help you organize and select resources. Adding labels below will let you query for objects that have similar, overlapping or dissimilar labels.",
11201124
"Labels must start and end with an alphanumeric character, can consist of lower-case letters, numbers, dots (.), hyphens (-), forward slash (/), underscore(_) and equal to (=)": "Labels must start and end with an alphanumeric character, can consist of lower-case letters, numbers, dots (.), hyphens (-), forward slash (/), underscore(_) and equal to (=)",
11211125
"LargeScale": "LargeScale",
1126+
"Last 24 hours": "Last 24 hours",
11221127
"Last 24 hours ({{count}})_one": "Last 24 hours ({{count}})",
11231128
"Last 24 hours ({{count}})_other": "Last 24 hours ({{count}})",
11241129
"Last available: ": "Last available: ",
@@ -1276,10 +1281,12 @@
12761281
"No assigned disaster recovery policy found": "No assigned disaster recovery policy found",
12771282
"No buckets found": "No buckets found",
12781283
"No capacity data available.": "No capacity data available.",
1284+
"No chart data available": "No chart data available",
12791285
"No clusters selected": "No clusters selected",
12801286
"No common storage class found for the selected managed clusters. To create a DR policy, a common storage class must exist, if not configured already, provision a common storage class and try again.": "No common storage class found for the selected managed clusters. To create a DR policy, a common storage class must exist, if not configured already, provision a common storage class and try again.",
12811287
"no compression": "no compression",
12821288
"No conditions found": "No conditions found",
1289+
"No data": "No data",
12831290
"No data available": "No data available",
12841291
"No datapoints found.": "No datapoints found.",
12851292
"No disaster recovery policies yet": "No disaster recovery policies yet",
@@ -2042,6 +2049,7 @@
20422049
"Type: {{selectedService}}": "Type: {{selectedService}}",
20432050
"Type: {{type}}": "Type: {{type}}",
20442051
"Typeahead single select": "Typeahead single select",
2052+
"Unable to retrieve health check data.": "Unable to retrieve health check data.",
20452053
"Unable to unsilence the selected alerts": "Unable to unsilence the selected alerts",
20462054
"Unavailable": "Unavailable",
20472055
"Unblocked": "Unblocked",
@@ -2137,6 +2145,7 @@
21372145
"View documentation": "View documentation",
21382146
"View external systems": "View external systems",
21392147
"View failed objects": "View failed objects",
2148+
"View health checks": "View health checks",
21402149
"View more": "View more",
21412150
"View namespaces": "View namespaces",
21422151
"View policies": "View policies",
@@ -2166,6 +2175,7 @@
21662175
"Volumes & Kubernetes resources are syncing slower than usual": "Volumes & Kubernetes resources are syncing slower than usual",
21672176
"Volumes are syncing slower than usual": "Volumes are syncing slower than usual",
21682177
"VolumeSnapshot classes": "VolumeSnapshot classes",
2178+
"Waiting for health checks": "Waiting for health checks",
21692179
"Warning": "Warning",
21702180
"We could not retrieve any information about the managed cluster {{clusterName}}": "We could not retrieve any information about the managed cluster {{clusterName}}",
21712181
"weeks": "weeks",
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import * as React from 'react';
2+
import {
3+
useCustomPrometheusPoll,
4+
usePrometheusBasePath,
5+
} from '@odf/shared/hooks/custom-prometheus-poll';
6+
import useRefWidth from '@odf/shared/hooks/ref-width';
7+
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
8+
// import { humanizeNumber } from '@odf/shared/utils';
9+
import {
10+
Chart,
11+
ChartArea,
12+
ChartAxis,
13+
ChartGroup,
14+
ChartThemeColor,
15+
ChartVoronoiContainer,
16+
} from '@patternfly/react-charts';
17+
import {
18+
Card,
19+
CardBody,
20+
CardHeader,
21+
CardTitle,
22+
Grid,
23+
GridItem,
24+
Button,
25+
EmptyState,
26+
EmptyStateVariant,
27+
EmptyStateIcon,
28+
EmptyStateBody,
29+
EmptyStateHeader,
30+
} from '@patternfly/react-core';
31+
import { ArrowRightIcon, SpaceShuttleIcon } from '@patternfly/react-icons';
32+
import {
33+
useHealthAlerts,
34+
useSilencedAlerts,
35+
filterOutSilencedAlerts,
36+
} from '../../HealthOverview/hooks';
37+
import './health-overview-card.scss';
38+
39+
// Define your Prometheus query
40+
const HEALTH_SCORE_QUERY = 'ocs_health_score';
41+
42+
type HealthDataPoint = {
43+
x: number;
44+
y: number;
45+
name: string;
46+
};
47+
48+
export const HealthOverviewCard: React.FC = () => {
49+
const { t } = useCustomTranslation();
50+
const [containerRef] = useRefWidth();
51+
const [chartRef, chartWidth] = useRefWidth();
52+
const basePath = usePrometheusBasePath();
53+
54+
// Fetch health score metric over time (range query)
55+
const [healthScoreData, healthScoreError] = useCustomPrometheusPoll({
56+
query: HEALTH_SCORE_QUERY,
57+
endpoint: 'api/v1/query_range' as any,
58+
basePath,
59+
samples: 20,
60+
timespan: 3600000, // 1 hour
61+
});
62+
63+
// Fetch alerts for counting active issues by severity
64+
const [healthAlerts] = useHealthAlerts();
65+
const { silences } = useSilencedAlerts();
66+
67+
// Filter out silenced alerts and count by severity
68+
const { criticalCount, moderateCount, minorCount } = React.useMemo(() => {
69+
// Get active alerts (exclude silenced ones)
70+
const activeAlerts = filterOutSilencedAlerts(healthAlerts, silences);
71+
72+
// Only count firing alerts (not resolved ones)
73+
const firingAlerts = activeAlerts.filter(
74+
(alert) => alert.state === 'firing'
75+
);
76+
77+
// Count by severity
78+
const critical = firingAlerts.filter(
79+
(alert) => alert.severity === 'critical'
80+
).length;
81+
const moderate = firingAlerts.filter(
82+
(alert) => alert.severity === 'warning'
83+
).length;
84+
const minor = firingAlerts.filter(
85+
(alert) => alert.severity === 'info'
86+
).length;
87+
88+
return {
89+
criticalCount: critical,
90+
moderateCount: moderate,
91+
minorCount: minor,
92+
};
93+
}, [healthAlerts, silences]);
94+
95+
// Process health score data for chart
96+
const chartData: HealthDataPoint[] = React.useMemo(() => {
97+
if (!healthScoreData?.data?.result?.[0]?.values) return [];
98+
99+
return healthScoreData.data.result[0].values.map((value) => {
100+
const [timestamp, scoreValue] = value;
101+
const score = parseFloat(scoreValue);
102+
const date = new Date(timestamp * 1000);
103+
104+
return {
105+
x: timestamp * 1000, // Use milliseconds for x-axis
106+
y: score,
107+
name: `${score.toFixed(1)}% at ${date.toLocaleTimeString()}`,
108+
};
109+
});
110+
}, [healthScoreData]);
111+
112+
// Get current health score (latest value)
113+
const currentHealthScore = React.useMemo(() => {
114+
if (chartData.length === 0) return null;
115+
return chartData[chartData.length - 1].y;
116+
}, [chartData]);
117+
118+
// Use real issue counts from alerts
119+
const criticalIssues = criticalCount;
120+
const moderateIssues = moderateCount;
121+
const minorIssues = minorCount;
122+
123+
const isLoading = !healthScoreData && !healthScoreError;
124+
const hasNoData = chartData.length === 0;
125+
const showEmptyState = isLoading || healthScoreError || hasNoData;
126+
127+
if (showEmptyState) {
128+
return (
129+
<Card isFlat>
130+
<CardHeader>
131+
<CardTitle>{t('ODF infrastructure health')}</CardTitle>
132+
</CardHeader>
133+
<CardBody>
134+
<EmptyState variant={EmptyStateVariant.lg}>
135+
<EmptyStateHeader
136+
titleText={<>{t('Waiting for health checks')}</>}
137+
icon={<EmptyStateIcon icon={SpaceShuttleIcon} />}
138+
headingLevel="h4"
139+
/>
140+
<EmptyStateBody>
141+
{healthScoreError
142+
? t('Unable to retrieve health check data.')
143+
: t(
144+
'Health check metrics are being collected. This may take a few moments.'
145+
)}
146+
</EmptyStateBody>
147+
</EmptyState>
148+
</CardBody>
149+
</Card>
150+
);
151+
}
152+
153+
return (
154+
<Card isFlat className="odf-infrastructure-health-card" ref={containerRef}>
155+
<CardHeader>
156+
<CardTitle>{t('ODF infrastructure health')}</CardTitle>
157+
</CardHeader>
158+
<CardBody>
159+
<Grid hasGutter>
160+
{/* Health Score and Chart Row */}
161+
<GridItem md={4} sm={12}>
162+
<div className="odf-infrastructure-health-card__score">
163+
{currentHealthScore !== null ? (
164+
<>
165+
<div className="odf-infrastructure-health-card__score-value">
166+
{Math.round(currentHealthScore)}%
167+
</div>
168+
<div className="odf-infrastructure-health-card__score-label">
169+
{t('Health Score')}
170+
</div>
171+
</>
172+
) : (
173+
<div className="pf-v5-u-color-200">{t('No data')}</div>
174+
)}
175+
</div>
176+
</GridItem>
177+
178+
<GridItem md={8} sm={12} ref={chartRef}>
179+
{chartData.length > 0 ? (
180+
<div className="odf-infrastructure-health-card__chart">
181+
<div className="odf-infrastructure-health-card__chart-label">
182+
{t('Last 24 hours')}
183+
</div>
184+
<Chart
185+
ariaTitle={t('Health Score Over Time')}
186+
containerComponent={
187+
<ChartVoronoiContainer
188+
labels={({ datum }) => datum.name}
189+
constrainToVisibleArea
190+
/>
191+
}
192+
height={180}
193+
width={chartWidth > 0 ? chartWidth - 40 : 600}
194+
padding={{ top: 20, bottom: 40, left: 10, right: 10 }}
195+
domain={{ y: [0, 100] }}
196+
scale={{ x: 'time', y: 'linear' }}
197+
themeColor={ChartThemeColor.blue}
198+
>
199+
<ChartAxis
200+
tickFormat={(t) => {
201+
const date = new Date(t);
202+
return date.toLocaleTimeString([], {
203+
hour: '2-digit',
204+
minute: '2-digit',
205+
hour12: true,
206+
});
207+
}}
208+
fixLabelOverlap
209+
style={{
210+
axis: { stroke: 'transparent' },
211+
tickLabels: {
212+
fontSize: 10,
213+
fill: 'var(--pf-v5-global--Color--200)',
214+
},
215+
}}
216+
/>
217+
<ChartGroup>
218+
<ChartArea
219+
data={chartData}
220+
interpolation="monotoneX"
221+
style={{
222+
data: {
223+
fill: 'var(--pf-v5-chart-color-blue-300)',
224+
fillOpacity: 0.3,
225+
stroke: 'var(--pf-v5-chart-color-blue-400)',
226+
strokeWidth: 2,
227+
},
228+
}}
229+
/>
230+
</ChartGroup>
231+
</Chart>
232+
</div>
233+
) : (
234+
<div className="pf-v5-u-color-200 pf-v5-u-text-align-center">
235+
{t('No chart data available')}
236+
</div>
237+
)}
238+
</GridItem>
239+
240+
{/* Active Issues Section */}
241+
<GridItem md={12}>
242+
<div className="odf-infrastructure-health-card__section-title">
243+
{t('Active issues')}
244+
</div>
245+
</GridItem>
246+
247+
<GridItem md={4} sm={4}>
248+
<div className="odf-infrastructure-health-card__issue-count">
249+
{criticalIssues}
250+
</div>
251+
<div className="odf-infrastructure-health-card__issue-label">
252+
{t('Critical')}
253+
</div>
254+
</GridItem>
255+
256+
<GridItem md={4} sm={4}>
257+
<div className="odf-infrastructure-health-card__issue-count">
258+
{moderateIssues}
259+
</div>
260+
<div className="odf-infrastructure-health-card__issue-label">
261+
{t('Moderate')}
262+
</div>
263+
</GridItem>
264+
265+
<GridItem md={4} sm={4}>
266+
<div className="odf-infrastructure-health-card__issue-count">
267+
{minorIssues}
268+
</div>
269+
<div className="odf-infrastructure-health-card__issue-label">
270+
{t('Minor')}
271+
</div>
272+
</GridItem>
273+
274+
{/* Action Link */}
275+
<GridItem md={12}>
276+
<Button
277+
variant="link"
278+
icon={<ArrowRightIcon />}
279+
iconPosition="end"
280+
isInline
281+
component="a"
282+
href="/odf/overview/health"
283+
>
284+
{t('View health checks')}
285+
</Button>
286+
</GridItem>
287+
</Grid>
288+
</CardBody>
289+
</Card>
290+
);
291+
};
292+
293+
/*
294+
import * as React from 'react';
295+
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core';
296+
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
297+
298+
export const HealthOverviewCard: React.FC = () => {
299+
const { t } = useCustomTranslation();
300+
301+
return (
302+
<Card className="odfDashboard-card--height">
303+
<CardHeader>
304+
<CardTitle>{t('ODF infrastructure health')}</CardTitle>
305+
</CardHeader>
306+
<CardBody> </CardBody>
307+
</Card>
308+
);
309+
};
310+
*/

0 commit comments

Comments
 (0)