Skip to content

Commit 408439b

Browse files
fixes and improvements
1 parent 8b638e4 commit 408439b

File tree

9 files changed

+357
-89
lines changed

9 files changed

+357
-89
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@
294294
"Threshold": "Threshold",
295295
"View metric in query browser": "View metric in query browser",
296296
"No recording rules found": "No recording rules found",
297+
"Mode": "Mode",
297298
"No results found": "No results found",
298299
"Clear or reset filters and try again.": "Clear or reset filters and try again.",
299300
"Check for errors in health dashboard. Status endpoint is returning: {{statusError}}": "Check for errors in health dashboard. Status endpoint is returning: {{statusError}}",

web/src/components/health/alert-row.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { useTranslation } from 'react-i18next';
66
import { formatActiveSince } from '../../utils/datetime';
77
import { valueFormat } from '../../utils/format';
88
import { HealthColorSquare } from './health-color-square';
9-
import { AlertWithRuleName, getAlertFilteredLabels, getAlertLink, getTrafficLink } from './health-helper';
9+
import {
10+
AlertWithRuleName,
11+
getAlertFilteredLabels,
12+
getAlertLink,
13+
getSeverityColor,
14+
getTrafficLink
15+
} from './health-helper';
1016

1117
export interface AlertRowProps {
1218
kind: string;
@@ -39,7 +45,9 @@ export const AlertRow: React.FC<AlertRowProps> = ({ kind, resourceName, alert, w
3945
</Td>
4046
)}
4147
<Td noPadding={!wide}>{alert.state}</Td>
42-
<Td>{alert.labels.severity}</Td>
48+
<Td>
49+
<Label color={getSeverityColor(alert.labels.severity)}>{alert.labels.severity}</Label>
50+
</Td>
4351
{alert.activeAt && <Td>{formatActiveSince(t, alert.activeAt)}</Td>}
4452
<Td>
4553
{labels.length === 0
@@ -51,9 +59,10 @@ export const AlertRow: React.FC<AlertRowProps> = ({ kind, resourceName, alert, w
5159
))}
5260
</Td>
5361
<Td>
54-
{valueFormat(alert.value as number, 2)}
55-
{alert.metadata.threshold && ' > ' + alert.metadata.threshold + ' ' + alert.metadata.unit}
62+
{valueFormat(alert.value as number, 2)} {alert.metadata.unit}
63+
{!wide && alert.metadata.threshold && ' > ' + alert.metadata.threshold}
5664
</Td>
65+
{wide && <Td>{alert.metadata.threshold ? '> ' + alert.metadata.threshold : ''}</Td>}
5766
{wide && <Td>{alert.annotations['description']}</Td>}
5867
<Td noPadding>
5968
<ActionsColumn

web/src/components/health/health-drawer-container.tsx

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import { RecordingRuleCard } from './recording-rule-card';
2424
import { RecordingRuleDetails } from './recording-rule-details';
2525
import { RuleDetails } from './rule-details';
2626

27+
// Type guard to differentiate between ByResource and RecordingRulesByResource
28+
const isAlertResource = (item: ByResource | RecordingRulesByResource): item is ByResource => {
29+
// ByResource has critical.firing, RecordingRulesByResource has critical as array
30+
return 'firing' in item.critical;
31+
};
32+
2733
export interface HealthDrawerContainerProps {
2834
title: string;
2935
stats: ByResource[];
@@ -40,54 +46,37 @@ export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({
4046
isDark
4147
}) => {
4248
const { t } = useTranslation('plugin__netobserv-plugin');
43-
const [selectedResource, setSelectedResource] = React.useState<ByResource | undefined>(undefined);
44-
const [selectedRecordingResource, setSelectedRecordingResource] = React.useState<RecordingRulesByResource>();
49+
const [selectedItem, setSelectedItem] = React.useState<ByResource | RecordingRulesByResource | undefined>(undefined);
4550
const drawerRef = React.useRef<HTMLDivElement>(null);
4651

4752
const onExpand = () => {
4853
drawerRef.current && drawerRef.current.focus();
4954
};
5055

51-
// When selecting a violation, deselect recording rule
52-
const handleSelectResource = (r?: ByResource) => {
53-
setSelectedResource(r);
54-
if (r) {
55-
setSelectedRecordingResource(undefined);
56-
}
57-
};
58-
59-
// When selecting a recording rule, deselect violation
60-
const handleSelectRecordingResource = (r: RecordingRulesByResource | undefined) => {
61-
setSelectedRecordingResource(r);
62-
if (r) {
63-
setSelectedResource(undefined);
64-
}
56+
const handleSelectItem = (item?: ByResource | RecordingRulesByResource) => {
57+
setSelectedItem(item);
6558
};
6659

6760
React.useEffect(() => {
68-
if (selectedResource) {
69-
const fromStats = stats.find(s => s.name === selectedResource.name);
70-
if (fromStats !== selectedResource) {
71-
setSelectedResource(fromStats);
61+
if (selectedItem) {
62+
if (isAlertResource(selectedItem)) {
63+
const fromStats = stats.find(s => s.name === selectedItem.name);
64+
if (fromStats !== selectedItem) {
65+
setSelectedItem(fromStats);
66+
}
67+
} else if (recordingRulesStats) {
68+
const fromStats = recordingRulesStats.find(s => s.name === selectedItem.name);
69+
if (fromStats !== selectedItem) {
70+
setSelectedItem(fromStats);
71+
}
7272
}
7373
}
74-
// we want to update selectedResource when stats changes, no more
74+
// we want to update selectedItem when stats or recordingRulesStats changes, no more
7575
// eslint-disable-next-line react-hooks/exhaustive-deps
76-
}, [stats]);
77-
78-
React.useEffect(() => {
79-
if (selectedRecordingResource && recordingRulesStats) {
80-
const fromStats = recordingRulesStats.find(s => s.name === selectedRecordingResource.name);
81-
if (fromStats !== selectedRecordingResource) {
82-
setSelectedRecordingResource(fromStats);
83-
}
84-
}
85-
// we want to update selectedRecordingResource when recordingRulesStats changes, no more
86-
// eslint-disable-next-line react-hooks/exhaustive-deps
87-
}, [recordingRulesStats]);
76+
}, [stats, recordingRulesStats]);
8877

8978
const hasRecordingRules = recordingRulesStats && recordingRulesStats.length > 0;
90-
const isExpanded = selectedResource !== undefined || selectedRecordingResource !== undefined;
79+
const isExpanded = selectedItem !== undefined;
9180

9281
// Sort alerts by score (best score = lowest value)
9382
const sortedAlerts = React.useMemo(() => _.orderBy(stats, r => r.score, 'asc'), [stats]);
@@ -123,26 +112,16 @@ export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({
123112
>
124113
<DrawerHead>
125114
<span tabIndex={isExpanded ? 0 : -1} ref={drawerRef}>
126-
{selectedResource !== undefined && (
127-
<>
128-
<ResourceLink inline={true} kind={kind} name={selectedResource.name} />
129-
</>
130-
)}
131-
{selectedRecordingResource !== undefined && (
132-
<>
133-
<ResourceLink inline={true} kind={kind} name={selectedRecordingResource.name} />
134-
</>
135-
)}
115+
{selectedItem && <ResourceLink inline={true} kind={kind} name={selectedItem.name} />}
136116
</span>
137117
</DrawerHead>
138-
{selectedResource && (
118+
{selectedItem && (
139119
<div className="health-gallery-drawer-content">
140-
<RuleDetails kind={kind} info={selectedResource} wide={false} />
141-
</div>
142-
)}
143-
{selectedRecordingResource && (
144-
<div className="health-gallery-drawer-content">
145-
<RecordingRuleDetails kind={kind} info={selectedRecordingResource} wide={false} />
120+
{isAlertResource(selectedItem) ? (
121+
<RuleDetails kind={kind} info={selectedItem} wide={false} />
122+
) : (
123+
<RecordingRuleDetails kind={kind} info={selectedItem} wide={false} />
124+
)}
146125
</div>
147126
)}
148127
</DrawerPanelContent>
@@ -165,9 +144,9 @@ export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({
165144
kind={kind}
166145
isDark={isDark}
167146
stats={r}
168-
isSelected={r.name === selectedResource?.name}
147+
isSelected={r.name === selectedItem?.name}
169148
onClick={() => {
170-
handleSelectResource(r.name !== selectedResource?.name ? r : undefined);
149+
handleSelectItem(r.name !== selectedItem?.name ? r : undefined);
171150
}}
172151
/>
173152
))}
@@ -177,9 +156,9 @@ export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({
177156
kind={kind}
178157
isDark={isDark}
179158
stats={r}
180-
isSelected={r.name === selectedRecordingResource?.name}
159+
isSelected={r.name === selectedItem?.name}
181160
onClick={() => {
182-
handleSelectRecordingResource(r.name !== selectedRecordingResource?.name ? r : undefined);
161+
handleSelectItem(r.name !== selectedItem?.name ? r : undefined);
183162
}}
184163
/>
185164
))}

web/src/components/health/health-global.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import { useTranslation } from 'react-i18next';
1515
import { HealthCard } from './health-card';
1616
import { ByResource, getAllAlerts, RecordingRulesByResource } from './health-helper';
1717
import { RecordingRuleCard } from './recording-rule-card';
18-
import { RecordingRuleDetails } from './recording-rule-details';
19-
import { RuleDetails } from './rule-details';
18+
import { UnifiedRuleDetails } from './unified-rule-details';
2019

2120
export interface HealthGlobalProps {
2221
info: ByResource;
@@ -56,12 +55,11 @@ export const HealthGlobal: React.FC<HealthGlobalProps> = ({ info, recordingRules
5655
)}
5756
</GridItem>
5857
<GridItem span={9}>
59-
{allAlerts.length > 0 && <RuleDetails kind={'Global'} info={info} wide={true} />}
60-
{hasRecordingRules && (
61-
<div style={{ marginTop: allAlerts.length > 0 ? '1rem' : '0' }}>
62-
<RecordingRuleDetails kind={'Global'} info={recordingRules} wide={true} />
63-
</div>
64-
)}
58+
<UnifiedRuleDetails
59+
kind={'Global'}
60+
alertInfo={allAlerts.length > 0 ? info : undefined}
61+
recordingRuleInfo={hasRecordingRules ? recordingRules : undefined}
62+
/>
6563
</GridItem>
6664
</Grid>
6765
)}

web/src/components/health/health-helper.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type RecordingRulesByResource = {
1919
critical: RecordingRuleItem[];
2020
warning: RecordingRuleItem[];
2121
other: RecordingRuleItem[];
22+
score: number;
2223
};
2324

2425
export type RecordingRuleMetricValue = {
@@ -213,14 +214,24 @@ const processRecordingRules = (
213214
name: name,
214215
critical: items.filter(i => i.severity === 'critical'),
215216
warning: items.filter(i => i.severity === 'warning'),
216-
other: items.filter(i => i.severity === 'info')
217+
other: items.filter(i => i.severity === 'info'),
218+
score: 0 // Will be computed below
217219
};
218220
};
219221

222+
const global = buildResourceGroup('', globalItems);
223+
const byNamespace = Object.keys(namespaceGroups).map(ns => buildResourceGroup(ns, namespaceGroups[ns]));
224+
const byNode = Object.keys(nodeGroups).map(n => buildResourceGroup(n, nodeGroups[n]));
225+
226+
// Compute scores for all resource groups
227+
[global, ...byNamespace, ...byNode].forEach(r => {
228+
r.score = computeRecordingRulesScore(r);
229+
});
230+
220231
return {
221-
global: buildResourceGroup('', globalItems),
222-
byNamespace: Object.keys(namespaceGroups).map(ns => buildResourceGroup(ns, namespaceGroups[ns])),
223-
byNode: Object.keys(nodeGroups).map(n => buildResourceGroup(n, nodeGroups[n]))
232+
global,
233+
byNamespace,
234+
byNode
224235
};
225236
};
226237

@@ -488,8 +499,58 @@ export const computeScore = (r: ByResource): number => {
488499
return sum / sumWeights;
489500
};
490501

502+
// Score [0,10]; higher is better
503+
export const computeRecordingRulesScore = (r: RecordingRulesByResource): number => {
504+
const allRules = [...r.critical, ...r.warning, ...r.other];
505+
506+
if (allRules.length === 0) {
507+
return 10; // Perfect score if no rules
508+
}
509+
510+
const allScores: ScoreDetail[] = allRules.map(rule => {
511+
// Determine weight based on severity
512+
let weight = minorWeight;
513+
if (rule.severity === 'critical') {
514+
weight = criticalWeight;
515+
} else if (rule.severity === 'warning') {
516+
weight = warningWeight;
517+
}
518+
519+
// Calculate raw score based on value vs threshold
520+
let rawScore = 10; // Default to perfect if no threshold
521+
if (rule.threshold) {
522+
const thresholdValue = parseFloat(rule.threshold);
523+
if (!isNaN(thresholdValue) && thresholdValue > 0) {
524+
// Create a compatible object to use the same computeExcessRatio function as alerts
525+
// Use same default upperBound as alerts (100)
526+
const mockAlert = {
527+
value: rule.value,
528+
metadata: {
529+
thresholdF: thresholdValue,
530+
upperBoundF: 100
531+
}
532+
} as AlertWithRuleName;
533+
534+
const excessRatio = computeExcessRatio(mockAlert);
535+
rawScore = 10 * (1 - excessRatio);
536+
}
537+
}
538+
539+
return { rawScore, weight };
540+
});
541+
542+
const sum = allScores.map(s => s.rawScore * s.weight).reduce((a, b) => a + b, 0);
543+
const sumWeights = allScores.map(s => s.weight).reduce((a, b) => a + b, 0);
544+
545+
if (sumWeights === 0) {
546+
return 10;
547+
}
548+
549+
return sum / sumWeights;
550+
};
551+
491552
// Score [0,1]; lower is better
492-
const computeExcessRatio = (a: AlertWithRuleName): number => {
553+
export const computeExcessRatio = (a: AlertWithRuleName): number => {
493554
// Assuming the alert value is a [0-n] percentage. Needs update if more use cases come up.
494555
const threshold = a.metadata.thresholdF / 2;
495556
const upper = a.metadata.upperBoundF;
@@ -510,6 +571,25 @@ export const computeAlertScore = (a: AlertWithRuleName): ScoreDetail => {
510571
};
511572
};
512573

574+
// Mapping of severity levels to PatternFly Label colors
575+
// critical -> red (danger)
576+
// warning -> orange (warning)
577+
// info -> blue (info)
578+
const SEVERITY_LABEL_COLORS = {
579+
critical: 'red',
580+
warning: 'orange',
581+
info: 'blue'
582+
} as const;
583+
584+
export const getSeverityColor = (
585+
severity: string | undefined
586+
): 'red' | 'orange' | 'blue' | 'grey' | 'purple' | 'cyan' | 'green' | 'gold' => {
587+
if (severity && severity in SEVERITY_LABEL_COLORS) {
588+
return SEVERITY_LABEL_COLORS[severity as keyof typeof SEVERITY_LABEL_COLORS];
589+
}
590+
return 'blue'; // default for info/undefined
591+
};
592+
513593
export const isSilenced = (silence: SilenceMatcher[], labels: PrometheusLabels): boolean => {
514594
for (const matcher of silence) {
515595
if (!(matcher.name in labels)) {

web/src/components/health/recording-rule-card.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { BellIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons';
1515
import * as React from 'react';
1616
import { useTranslation } from 'react-i18next';
17+
import { valueFormat } from '../../utils/format';
1718
import { RecordingRulesByResource } from './health-helper';
1819

1920
export interface RecordingRuleCardProps {
@@ -43,8 +44,6 @@ export const RecordingRuleCard: React.FC<RecordingRuleCardProps> = ({ stats, kin
4344
classes.push('dark');
4445
}
4546

46-
const totalRules = stats.critical.length + stats.warning.length + stats.other.length;
47-
4847
return (
4948
<Card className={classes.join(' ')} isClickable={onClick !== undefined} isClicked={isSelected}>
5049
<CardHeader
@@ -89,7 +88,7 @@ export const RecordingRuleCard: React.FC<RecordingRuleCardProps> = ({ stats, kin
8988
</FlexItem>
9089
<FlexItem>
9190
<TextContent>
92-
<Text component={TextVariants.h1}>{totalRules}</Text>
91+
<Text component={TextVariants.h1}>{valueFormat(stats.score, 1)}</Text>
9392
</TextContent>
9493
</FlexItem>
9594
</Flex>

0 commit comments

Comments
 (0)