Skip to content

Commit 7e0429b

Browse files
feat(ClusterInfo): add slo logs link (#1850)
1 parent e99b217 commit 7e0429b

File tree

6 files changed

+209
-124
lines changed

6 files changed

+209
-124
lines changed

src/containers/Cluster/ClusterInfo/ClusterInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {VersionToColorMap} from '../../../types/versions';
1010
import i18n from '../i18n';
1111

1212
import {b} from './shared';
13-
import {getInfo, useClusterLinks} from './utils';
13+
import {useClusterLinks} from './utils/useClusterLinks';
14+
import {getInfo} from './utils/utils';
1415

1516
import './ClusterInfo.scss';
1617

src/containers/Cluster/ClusterInfo/utils.tsx

Lines changed: 0 additions & 123 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {prepareClusterCoresLink, prepareClusterLoggingLinks} from '../useClusterLinks';
2+
3+
describe('prepareClusterCoresLink', () => {
4+
it('It should parse stringified json with cores url', () => {
5+
expect(
6+
prepareClusterCoresLink('{"url":"https://coredumps.com?cluster=my_cluster"}'),
7+
).toEqual('https://coredumps.com?cluster=my_cluster');
8+
});
9+
10+
it('It should return undefined if input is undefined', () => {
11+
expect(prepareClusterCoresLink(undefined)).toEqual(undefined);
12+
});
13+
it('It should return undefined if input is incorrect', () => {
14+
expect(prepareClusterCoresLink('hello')).toEqual(undefined);
15+
});
16+
});
17+
18+
describe('prepareClusterLoggingLinks', () => {
19+
it('It should parse stringified json with logging and slo logs urls', () => {
20+
expect(
21+
prepareClusterLoggingLinks(
22+
'{"url":"https://logging.com/logs?cluster=my_cluster","slo_logs_url":"https://logging.com/slo-logs?cluster=my_cluster"}',
23+
),
24+
).toEqual({
25+
logsUrl: 'https://logging.com/logs?cluster=my_cluster',
26+
sloLogsUrl: 'https://logging.com/slo-logs?cluster=my_cluster',
27+
});
28+
});
29+
it('It should parse stringified json with only logging url', () => {
30+
expect(
31+
prepareClusterLoggingLinks('{"url":"https://logging.com/logs?cluster=my_cluster"}'),
32+
).toEqual({
33+
logsUrl: 'https://logging.com/logs?cluster=my_cluster',
34+
});
35+
});
36+
it('It should parse stringified json with only slo logs url', () => {
37+
expect(
38+
prepareClusterLoggingLinks(
39+
'{"slo_logs_url":"https://logging.com/slo-logs?cluster=my_cluster"}',
40+
),
41+
).toEqual({
42+
sloLogsUrl: 'https://logging.com/slo-logs?cluster=my_cluster',
43+
});
44+
});
45+
it('It should return empty object if input is undefined', () => {
46+
expect(prepareClusterLoggingLinks(undefined)).toEqual({});
47+
});
48+
it('It should return empty object if input is incorrect', () => {
49+
expect(prepareClusterLoggingLinks('hello')).toEqual({});
50+
});
51+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
3+
import {useClusterBaseInfo} from '../../../../store/reducers/cluster/cluster';
4+
import type {ClusterLink} from '../../../../types/additionalProps';
5+
import {parseJson} from '../../../../utils/utils';
6+
import i18n from '../../i18n';
7+
8+
/**
9+
* parses stringified json in format {url: "href"}
10+
*/
11+
export function prepareClusterCoresLink(rawCoresString?: string) {
12+
try {
13+
const linkObject = parseJson(rawCoresString) as unknown;
14+
15+
if (
16+
linkObject &&
17+
typeof linkObject === 'object' &&
18+
'url' in linkObject &&
19+
typeof linkObject.url === 'string'
20+
) {
21+
return linkObject.url;
22+
}
23+
} catch {}
24+
25+
return undefined;
26+
}
27+
28+
/**
29+
* parses stringified json in format {url: "href", slo_logs_url: "href"}
30+
*/
31+
export function prepareClusterLoggingLinks(rawLoggingString?: string) {
32+
try {
33+
const linkObject = parseJson(rawLoggingString) as unknown;
34+
35+
if (linkObject && typeof linkObject === 'object') {
36+
const logsUrl =
37+
'url' in linkObject && typeof linkObject.url === 'string'
38+
? linkObject.url
39+
: undefined;
40+
const sloLogsUrl =
41+
'slo_logs_url' in linkObject && typeof linkObject.slo_logs_url === 'string'
42+
? linkObject.slo_logs_url
43+
: undefined;
44+
return {logsUrl, sloLogsUrl};
45+
}
46+
} catch {}
47+
48+
return {};
49+
}
50+
51+
export function useClusterLinks() {
52+
const {cores, logging} = useClusterBaseInfo();
53+
54+
return React.useMemo(() => {
55+
const result: ClusterLink[] = [];
56+
57+
const coresUrl = prepareClusterCoresLink(cores);
58+
const {logsUrl, sloLogsUrl} = prepareClusterLoggingLinks(logging);
59+
60+
if (coresUrl) {
61+
result.push({
62+
title: i18n('link_cores'),
63+
url: coresUrl,
64+
});
65+
}
66+
67+
if (logsUrl) {
68+
result.push({
69+
title: i18n('link_logging'),
70+
url: logsUrl,
71+
});
72+
}
73+
74+
if (sloLogsUrl) {
75+
result.push({
76+
title: i18n('link_slo-logs'),
77+
url: sloLogsUrl,
78+
});
79+
}
80+
81+
return result;
82+
}, [cores, logging]);
83+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
3+
import {Flex} from '@gravity-ui/uikit';
4+
5+
import {ProgressViewer} from '../../../../components/ProgressViewer/ProgressViewer';
6+
import {Tags} from '../../../../components/Tags';
7+
import {isClusterInfoV2} from '../../../../types/api/cluster';
8+
import type {TClusterInfo} from '../../../../types/api/cluster';
9+
import type {EFlag} from '../../../../types/api/enums';
10+
import type {InfoItem} from '../../../../types/components';
11+
import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
12+
import i18n from '../../i18n';
13+
import {NodesState} from '../components/NodesState/NodesState';
14+
import {b} from '../shared';
15+
16+
const COLORS_PRIORITY: Record<EFlag, number> = {
17+
Green: 5,
18+
Blue: 4,
19+
Yellow: 3,
20+
Orange: 2,
21+
Red: 1,
22+
Grey: 0,
23+
};
24+
25+
const getDCInfo = (cluster: TClusterInfo) => {
26+
if (isClusterInfoV2(cluster) && cluster.MapDataCenters) {
27+
return Object.entries(cluster.MapDataCenters).map(([dc, count]) => (
28+
<React.Fragment key={dc}>
29+
{dc}: {formatNumber(count)}
30+
</React.Fragment>
31+
));
32+
}
33+
return undefined;
34+
};
35+
36+
export const getInfo = (cluster: TClusterInfo, additionalInfo: InfoItem[]) => {
37+
const info: InfoItem[] = [];
38+
39+
if (isClusterInfoV2(cluster) && cluster.MapNodeStates) {
40+
const arrayNodesStates = Object.entries(cluster.MapNodeStates) as [EFlag, number][];
41+
// sort stack to achieve order "green, orange, yellow, red, blue, grey"
42+
arrayNodesStates.sort((a, b) => COLORS_PRIORITY[b[0]] - COLORS_PRIORITY[a[0]]);
43+
const nodesStates = arrayNodesStates.map(([state, count]) => {
44+
return (
45+
<NodesState state={state as EFlag} key={state}>
46+
{formatNumber(count)}
47+
</NodesState>
48+
);
49+
});
50+
info.push({
51+
label: i18n('label_nodes-state'),
52+
value: <Flex gap={2}>{nodesStates}</Flex>,
53+
});
54+
}
55+
56+
const dataCenters = getDCInfo(cluster);
57+
if (dataCenters?.length) {
58+
info.push({
59+
label: i18n('label_dc'),
60+
value: <Tags tags={dataCenters} gap={2} className={b('dc')} />,
61+
});
62+
}
63+
64+
info.push({
65+
label: i18n('label_load'),
66+
value: <ProgressViewer value={cluster?.LoadAverage} capacity={cluster?.NumberOfCpus} />,
67+
});
68+
69+
info.push(...additionalInfo);
70+
71+
return info;
72+
};

src/containers/Cluster/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"links": "Links",
1212
"link_cores": "Coredumps",
1313
"link_logging": "Logging",
14+
"link_slo-logs": "Slo Logs",
1415
"context_cores": "cores",
1516
"title_cpu": "CPU",
1617
"title_storage": "Storage",

0 commit comments

Comments
 (0)