Skip to content

Commit 4991b24

Browse files
committed
"merge" heatmap and alerts table
- Remove the heatmap component - Instead, show color squares per entry in the alerts table - Show gradient in tooltip, with score details - Add marker line on gradient
1 parent ed8c141 commit 4991b24

File tree

11 files changed

+155
-335
lines changed

11 files changed

+155
-335
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,18 +246,11 @@
246246
"minor issues": "minor issues",
247247
"pending issues": "pending issues",
248248
"silenced issues": "silenced issues",
249+
"Score": "Score",
250+
"Weight": "Weight",
249251
"View in Network Traffic": "View in Network Traffic",
250252
"No violations found": "No violations found",
251253
"Global rule violations": "Global rule violations",
252-
"Rule {{ruleName}}: no alert": "Rule {{ruleName}}: no alert",
253-
"The heatmap represents every issues related to a resource, using a color scale that depends on the severity, state and value amplitude.": "The heatmap represents every issues related to a resource, using a color scale that depends on the severity, state and value amplitude.",
254-
"Heatmap": "Heatmap",
255-
"Click on a cell to show the details.": "Click on a cell to show the details.",
256-
"Gradient per severity: ": "Gradient per severity: ",
257-
"info": "info",
258-
"warning": "warning",
259-
"critical": "critical",
260-
"No alert for this rule": "No alert for this rule",
261254
"No rules found, health cannot be determined": "No rules found, health cannot be determined",
262255
"Check alert definitions in FlowCollector \"spec.processor.metrics.alertGroups\" and \"spec.processor.metrics.disableAlerts\".": "Check alert definitions in FlowCollector \"spec.processor.metrics.alertGroups\" and \"spec.processor.metrics.disableAlerts\".",
263256
"Make sure that Prometheus and AlertManager are running.": "Make sure that Prometheus and AlertManager are running.",

web/src/components/health/__tests__/helper.spec.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('health helpers', () => {
3838
const alert = mockAlert('test', 'info', 'pending', 10, 10);
3939
const score = computeAlertScore(alert);
4040
expect(score.rawScore).toBeCloseTo(9.47, 2);
41-
expect(score.weight).toEqual(0.12);
41+
expect(score.weight).toEqual(0.075);
4242
});
4343

4444
it('should compute full score', () => {
@@ -48,29 +48,29 @@ describe('health helpers', () => {
4848
critical: { firing: [], pending: [], silenced: [], inactive: [] },
4949
warning: { firing: [], pending: [], silenced: [], inactive: [] },
5050
other: { firing: [], pending: [], silenced: [], inactive: [] },
51-
score: { total: 0, details: [] }
51+
score: 0
5252
};
53-
expect(computeScore(r).total).toEqual(10);
53+
expect(computeScore(r)).toEqual(10);
5454

5555
// Add 3 inactive alerts => still max score
5656
r.critical.inactive.push('test-critical');
5757
r.warning.inactive.push('test-warning');
5858
r.other.inactive.push('test-info');
59-
expect(computeScore(r).total).toEqual(10);
59+
expect(computeScore(r)).toEqual(10);
6060

6161
// Turn the inactive info into pending => slightly decreasing score
6262
r.other.inactive = [];
6363
r.other.pending = [mockAlert('test-info', 'info', 'pending', 10, 20)];
64-
expect(computeScore(r).total).toBeCloseTo(9.9, 1);
64+
expect(computeScore(r)).toBeCloseTo(9.9, 1);
6565

6666
// Turn the inactive warning into firing => more decreasing score
6767
r.warning.inactive = [];
6868
r.warning.firing = [mockAlert('test-warning', 'warning', 'firing', 10, 40)];
69-
expect(computeScore(r).total).toBeCloseTo(8.5, 1);
69+
expect(computeScore(r)).toBeCloseTo(8.8, 1);
7070

7171
// Turn the inactive critical into firing => more decrease
7272
r.critical.inactive = [];
7373
r.critical.firing = [mockAlert('test-critical', 'critical', 'firing', 10, 40)];
74-
expect(computeScore(r).total).toBeCloseTo(6.5, 1);
74+
expect(computeScore(r)).toBeCloseTo(6.4, 1);
7575
});
7676
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const HealthCard: React.FC<HealthCardProps> = ({ stats, kind, isDark, isS
9696
</FlexItem>
9797
<FlexItem>
9898
<TextContent>
99-
<Text component={TextVariants.h1}>{valueFormat(stats.score.total)}</Text>
99+
<Text component={TextVariants.h1}>{valueFormat(stats.score)}</Text>
100100
</TextContent>
101101
</FlexItem>
102102
</Flex>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.cell {
2+
border-radius: 5px;
3+
width: 1rem;
4+
height: 1rem;
5+
display: inline-block;
6+
vertical-align: middle;
7+
margin-right: 5px;
8+
}
9+
10+
.gradient {
11+
border-radius: 2px;
12+
width: 5rem;
13+
height: 5px;
14+
display: inline-block;
15+
margin-left: 5px;
16+
}
17+
18+
.gradient .vertical-mark {
19+
position: relative;
20+
display: block;
21+
height: 100%;
22+
border-right: solid 1px black;
23+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { valueFormat } from '../../utils/format';
4+
import { AlertWithRuleName, computeAlertScore, computeExcessRatioStatusWeighted } from './helper';
5+
6+
import { Tooltip } from '@patternfly/react-core';
7+
import './health-color-square.css';
8+
9+
export interface HealthColorSquareProps {
10+
alert: AlertWithRuleName;
11+
}
12+
13+
// rgb in [0,255] bounds
14+
type Color = { r: number; g: number; b: number };
15+
type ColorMap = Color[];
16+
17+
const criticalColorMap: ColorMap = [
18+
{ r: 250, g: 234, b: 232 },
19+
{ r: 163, g: 0, b: 0 },
20+
{ r: 44, g: 0, b: 0 },
21+
{ r: 20, g: 0, b: 20 }
22+
];
23+
24+
const warningColorMap: ColorMap = [
25+
{ r: 253, g: 247, b: 231 },
26+
{ r: 240, g: 171, b: 0 },
27+
{ r: 236, g: 122, b: 8 },
28+
{ r: 59, g: 31, b: 0 }
29+
];
30+
31+
const infoColorMap: ColorMap = [
32+
{ r: 62, g: 134, b: 53 },
33+
{ r: 228, g: 245, b: 188 },
34+
{ r: 154, g: 216, b: 216 }
35+
];
36+
37+
const getCellColors = (value: number, rangeFrom: number, rangeTo: number, colorMap: ColorMap) => {
38+
const clamped = Math.max(rangeFrom, Math.min(rangeTo, value));
39+
const ratio = (clamped - rangeFrom) / (rangeTo - rangeFrom); // e.g. 0.8 | 0 | 1
40+
const colorRatio = ratio * (colorMap.length - 1); // e.g. (length is 3) 1.6 | 0 | 2
41+
const colorLow = colorMap[Math.floor(colorRatio)]; // e.g. m[1] | m[0] | m[2]
42+
const colorHigh = colorMap[Math.ceil(colorRatio)]; // e.g. m[2] | m[0] | m[2]
43+
const remains = colorRatio - Math.floor(colorRatio); // e.g. 0.6 | 0 | 0
44+
const r = Math.floor((colorHigh.r - colorLow.r) * remains + colorLow.r);
45+
const g = Math.floor((colorHigh.g - colorLow.g) * remains + colorLow.g);
46+
const b = Math.floor((colorHigh.b - colorLow.b) * remains + colorLow.b);
47+
const brightness = 0.21 * r + 0.72 * g + 0.07 * b; // https://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale/
48+
const textColor = brightness > 128 ? 'var(--pf-global--palette--black-1000)' : 'var(--pf-global--palette--black-100)';
49+
return {
50+
color: textColor,
51+
backgroundColor: `rgb(${r},${g},${b})`
52+
};
53+
};
54+
55+
const buildGradientCSS = (colorMap: ColorMap): string => {
56+
const colorStops = colorMap.map(c => `rgb(${c.r},${c.g},${c.b})`);
57+
return 'linear-gradient(to right,' + colorStops.join(',') + ')';
58+
};
59+
60+
export const HealthColorSquare: React.FC<HealthColorSquareProps> = ({ alert }) => {
61+
const { t } = useTranslation('plugin__netobserv-plugin');
62+
63+
let prefix = '';
64+
switch (alert.state) {
65+
case 'pending':
66+
prefix = `[pending] `;
67+
break;
68+
case 'silenced':
69+
prefix = `[silenced] `;
70+
break;
71+
}
72+
const valueInfo =
73+
valueFormat(alert.value as number, 2) +
74+
(alert.metadata?.threshold ? '> ' + alert.metadata.threshold + ' ' + alert.metadata.unit : '');
75+
const tooltip = `${prefix}${alert.annotations['summary']} | ${valueInfo}`;
76+
77+
const colorMap =
78+
alert.labels.severity === 'critical'
79+
? criticalColorMap
80+
: alert.labels.severity === 'warning'
81+
? warningColorMap
82+
: infoColorMap;
83+
84+
const scoreForMap = computeExcessRatioStatusWeighted(alert);
85+
const score = computeAlertScore(alert);
86+
87+
return (
88+
<Tooltip
89+
content={
90+
<>
91+
{t('Score') + ': ' + valueFormat(score.rawScore)}
92+
<br />
93+
{t('Weight') + ': ' + score.weight}
94+
<br />
95+
<div className="gradient" style={{ backgroundImage: buildGradientCSS(colorMap) }}>
96+
<span className="vertical-mark" style={{ width: 100 * scoreForMap + '%' }} />
97+
</div>
98+
</>
99+
}
100+
>
101+
<div className={'cell'} style={getCellColors(scoreForMap, 0, 1, colorMap)} title={tooltip} />
102+
</Tooltip>
103+
);
104+
};

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

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
22
import {
33
Drawer,
4-
DrawerActions,
5-
DrawerCloseButton,
64
DrawerContent,
75
DrawerContentBody,
86
DrawerHead,
@@ -14,15 +12,12 @@ import {
1412
MenuToggleElement,
1513
Text,
1614
TextContent,
17-
TextVariants,
18-
ToggleGroup,
19-
ToggleGroupItem
15+
TextVariants
2016
} from '@patternfly/react-core';
21-
import { EllipsisVIcon, ListIcon, ThIcon } from '@patternfly/react-icons';
17+
import { EllipsisVIcon } from '@patternfly/react-icons';
2218
import * as React from 'react';
2319
import { useTranslation } from 'react-i18next';
2420
import { HealthGallery } from './health-gallery';
25-
import { HealthHeatmap } from './health-heatmap';
2621
import { ByResource } from './helper';
2722
import { RuleDetails } from './rule-details';
2823

@@ -36,7 +31,6 @@ export interface HealthDrawerContainerProps {
3631
export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({ title, stats, kind, isDark }) => {
3732
const { t } = useTranslation('plugin__netobserv-plugin');
3833
const [selectedResource, setSelectedResource] = React.useState<ByResource | undefined>(undefined);
39-
const [selectedPanelView, setSelectedPanelView] = React.useState<'heatmap' | 'table'>('heatmap');
4034
const [isKebabOpen, setKebabOpen] = React.useState(false);
4135
const drawerRef = React.useRef<HTMLDivElement>(null);
4236

@@ -112,31 +106,10 @@ export const HealthDrawerContainer: React.FC<HealthDrawerContainerProps> = ({ ti
112106
</>
113107
)}
114108
</span>
115-
<DrawerActions>
116-
<ToggleGroup aria-label="Heatmap view">
117-
<ToggleGroupItem
118-
icon={<ThIcon />}
119-
buttonId="toggle-group-heatmap"
120-
isSelected={selectedPanelView === 'heatmap'}
121-
onChange={() => setSelectedPanelView('heatmap')}
122-
/>
123-
<ToggleGroupItem
124-
icon={<ListIcon />}
125-
buttonId="toggle-group-table"
126-
isSelected={selectedPanelView === 'table'}
127-
onChange={() => setSelectedPanelView('table')}
128-
/>
129-
</ToggleGroup>
130-
<DrawerCloseButton onClick={() => setSelectedResource(undefined)} />
131-
</DrawerActions>
132109
</DrawerHead>
133110
{selectedResource && (
134111
<div className="health-gallery-drawer-content">
135-
{selectedPanelView === 'heatmap' ? (
136-
<HealthHeatmap info={selectedResource} interactive={true} />
137-
) : (
138-
<RuleDetails info={selectedResource} detailed={false} />
139-
)}
112+
<RuleDetails info={selectedResource} header={false} />
140113
</div>
141114
)}
142115
</DrawerPanelContent>

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { CheckCircleIcon } from '@patternfly/react-icons';
1313
import * as React from 'react';
1414
import { useTranslation } from 'react-i18next';
1515
import { HealthCard } from './health-card';
16-
import { HealthHeatmap } from './health-heatmap';
1716
import { ByResource, getAllAlerts } from './helper';
1817
import { RuleDetails } from './rule-details';
1918

@@ -44,10 +43,7 @@ export const HealthGlobal: React.FC<HealthGlobalProps> = ({ info, isDark }) => {
4443
<HealthCard isDark={isDark} stats={info} isSelected={false} />
4544
</GridItem>
4645
<GridItem span={9}>
47-
<RuleDetails info={info} detailed={true} />
48-
</GridItem>
49-
<GridItem span={3}>
50-
<HealthHeatmap info={info} interactive={false} />
46+
<RuleDetails info={info} header={true} />
5147
</GridItem>
5248
</Grid>
5349
)}

0 commit comments

Comments
 (0)