Skip to content

Commit f42551f

Browse files
committed
fix: review
1 parent e1fc54c commit f42551f

File tree

8 files changed

+182
-108
lines changed

8 files changed

+182
-108
lines changed

src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
border: unset;
2323
}
24-
&__doughtnut {
24+
&__doughnut {
2525
margin-top: auto;
2626
}
2727
&__cards {

src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx

Lines changed: 82 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {valueIsDefined} from '../../../utils';
88
import {formatNumber} from '../../../utils/dataFormatters/dataFormatters';
99
import i18n from '../i18n';
1010

11-
import {ClusterMetricsCard, ClusterMetricsCardSkeleton} from './components/ClusterMetricsCard';
11+
import {ClusterDashboardSkeleton, ClusterMetricsCard} from './components/ClusterMetricsCard';
1212
import {ClusterMetricsCores} from './components/ClusterMetricsCores';
1313
import {ClusterMetricsMemory} from './components/ClusterMetricsMemory';
1414
import {ClusterMetricsStorage} from './components/ClusterMetricsStorage';
@@ -43,95 +43,97 @@ interface ClusterDashboardProps {
4343
loading?: boolean;
4444
}
4545

46-
export function ClusterDashboard({cluster, groupStats = {}, loading}: ClusterDashboardProps) {
47-
const getDoughnuts = () => {
48-
if (loading) {
49-
return Array.from('123').map((el) => <ClusterMetricsCardSkeleton key={el} />);
50-
}
51-
const metricsCards = [];
52-
if (isClusterInfoV2(cluster)) {
53-
const {CoresUsed, NumberOfCpus} = cluster;
54-
if (valueIsDefined(CoresUsed) && valueIsDefined(NumberOfCpus)) {
55-
metricsCards.push(
56-
<ClusterMetricsCores value={CoresUsed} capacity={NumberOfCpus} key="cores" />,
57-
);
58-
}
59-
}
60-
const {StorageTotal, StorageUsed} = cluster;
61-
if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) {
62-
metricsCards.push(
63-
<ClusterMetricsStorage key="storage" value={StorageUsed} capacity={StorageTotal} />,
64-
);
65-
}
66-
const {MemoryTotal, MemoryUsed} = cluster;
67-
if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) {
46+
export function ClusterDashboard(props: ClusterDashboardProps) {
47+
return (
48+
<div className={b()}>
49+
<Flex gap={4} wrap>
50+
<Flex gap={4} wrap="nowrap">
51+
<ClusterDoughnuts {...props} />
52+
</Flex>
53+
<div className={b('cards-container')}>
54+
<ClusterDashboardCards {...props} />
55+
</div>
56+
</Flex>
57+
</div>
58+
);
59+
}
60+
61+
function ClusterDoughnuts({cluster, loading}: ClusterDashboardProps) {
62+
if (loading) {
63+
return <ClusterDashboardSkeleton />;
64+
}
65+
const metricsCards = [];
66+
if (isClusterInfoV2(cluster)) {
67+
const {CoresUsed, NumberOfCpus} = cluster;
68+
if (valueIsDefined(CoresUsed) && valueIsDefined(NumberOfCpus)) {
6869
metricsCards.push(
69-
<ClusterMetricsMemory key="memory" value={MemoryUsed} capacity={MemoryTotal} />,
70+
<ClusterMetricsCores value={CoresUsed} capacity={NumberOfCpus} key="cores" />,
7071
);
7172
}
72-
return metricsCards;
73-
};
73+
}
74+
const {StorageTotal, StorageUsed} = cluster;
75+
if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) {
76+
metricsCards.push(
77+
<ClusterMetricsStorage key="storage" value={StorageUsed} capacity={StorageTotal} />,
78+
);
79+
}
80+
const {MemoryTotal, MemoryUsed} = cluster;
81+
if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) {
82+
metricsCards.push(
83+
<ClusterMetricsMemory key="memory" value={MemoryUsed} capacity={MemoryTotal} />,
84+
);
85+
}
86+
return metricsCards;
87+
}
7488

75-
const getCards = () => {
76-
if (loading) {
77-
return null;
78-
}
79-
const cards = [];
89+
function ClusterDashboardCards({cluster, groupStats = {}, loading}: ClusterDashboardProps) {
90+
if (loading) {
91+
return null;
92+
}
93+
const cards = [];
94+
95+
const nodesRoles = getNodesRolesInfo(cluster);
96+
cards.push(
97+
<ClusterMetricsCard size="l" key={'roles'} title={i18n('label_nodes')}>
98+
<Flex gap={2} direction="column">
99+
<Amount value={cluster?.NodesAlive} />
100+
101+
{nodesRoles?.length ? <Tags tags={nodesRoles} gap={3} /> : null}
102+
</Flex>
103+
</ClusterMetricsCard>,
104+
);
80105

81-
const nodesRoles = getNodesRolesInfo(cluster);
106+
if (Object.keys(groupStats).length) {
107+
const tags = getStorageGroupStats(groupStats);
108+
const total = getTotalStorageGroupsUsed(groupStats);
82109
cards.push(
83-
<ClusterMetricsCard size="l" key={'roles'} title={i18n('label_nodes')}>
110+
<ClusterMetricsCard size="l" key={'groups'} title={i18n('label_storage-groups')}>
84111
<Flex gap={2} direction="column">
85-
<Amount value={cluster?.NodesAlive} />
86-
87-
{nodesRoles?.length ? <Tags tags={nodesRoles} gap={3} /> : null}
112+
<Amount value={total} />
113+
<Tags tags={tags} gap={3} />
88114
</Flex>
89115
</ClusterMetricsCard>,
90116
);
117+
}
91118

92-
if (Object.keys(groupStats).length) {
93-
const tags = getStorageGroupStats(groupStats);
94-
const total = getTotalStorageGroupsUsed(groupStats);
95-
cards.push(
96-
<ClusterMetricsCard size="l" key={'groups'} title={i18n('label_storage-groups')}>
97-
<Flex gap={2} direction="column">
98-
<Amount value={total} />
99-
<Tags tags={tags} gap={3} />
100-
</Flex>
101-
</ClusterMetricsCard>,
102-
);
103-
}
104-
105-
const dataCenters = getDCInfo(cluster);
106-
if (dataCenters?.length) {
107-
cards.push(
108-
<ClusterMetricsCard size="l" key={'hosts'} title={i18n('label_hosts')}>
109-
<Flex gap={2} direction="column">
110-
<Amount value={cluster?.Hosts} />
111-
<Tags tags={dataCenters} gap={3} />
112-
</Flex>
113-
</ClusterMetricsCard>,
114-
);
115-
}
116-
117-
if (cluster.Tenants) {
118-
cards.push(
119-
<ClusterMetricsCard size="l" key="tenants" title={i18n('label_databases')}>
120-
<Amount value={cluster?.Tenants} />
121-
</ClusterMetricsCard>,
122-
);
123-
}
124-
return cards;
125-
};
126-
127-
return (
128-
<div className={b()}>
129-
<Flex gap={4} wrap>
130-
<Flex gap={4} wrap="nowrap">
131-
{getDoughnuts()}
119+
const dataCenters = getDCInfo(cluster);
120+
if (dataCenters?.length) {
121+
cards.push(
122+
<ClusterMetricsCard size="l" key={'hosts'} title={i18n('label_hosts')}>
123+
<Flex gap={2} direction="column">
124+
<Amount value={cluster?.Hosts} />
125+
<Tags tags={dataCenters} gap={3} />
132126
</Flex>
133-
<div className={b('cards-container')}>{getCards()}</div>
134-
</Flex>
135-
</div>
136-
);
127+
</ClusterMetricsCard>,
128+
);
129+
}
130+
131+
if (cluster.Tenants) {
132+
cards.push(
133+
<ClusterMetricsCard size="l" key="tenants" title={i18n('label_databases')}>
134+
<Amount value={cluster?.Tenants} />
135+
</ClusterMetricsCard>,
136+
);
137+
}
138+
return cards;
137139
}

src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
13
import {Text} from '@gravity-ui/uikit';
24

35
import type {DiagnosticCardProps} from '../../../../components/DiagnosticCard/DiagnosticCard';
@@ -45,17 +47,27 @@ export function ClusterMetricsCardDoughnut({
4547
}: ClusterMetricsDougnutCardProps) {
4648
return (
4749
<ClusterMetricsCard title={title} size={size}>
48-
<DoughnutMetrics {...rest} className={b('doughtnut')}>
50+
<DoughnutMetrics {...rest} className={b('doughnut')}>
4951
{children}
5052
</DoughnutMetrics>
5153
</ClusterMetricsCard>
5254
);
5355
}
5456

55-
export function ClusterMetricsCardSkeleton() {
57+
function ClusterMetricsCardSkeleton() {
5658
return (
5759
<ClusterMetricsCard className={b('skeleton-wrapper')}>
5860
<Skeleton className={b('skeleton')} />
5961
</ClusterMetricsCard>
6062
);
6163
}
64+
65+
export function ClusterDashboardSkeleton() {
66+
return (
67+
<React.Fragment>
68+
<ClusterMetricsCardSkeleton />
69+
<ClusterMetricsCardSkeleton />
70+
<ClusterMetricsCardSkeleton />
71+
</React.Fragment>
72+
);
73+
}
Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
1+
import {unbreakableGap} from '../../utils';
12
import {formatBytes} from '../formatBytes';
23

34
describe('formatBytes', () => {
45
it('should work with only value', () => {
5-
expect(formatBytes({value: 100})).toBe('100\xa0B');
6-
expect(formatBytes({value: 100_000})).toBe('100\xa0KB');
7-
expect(formatBytes({value: 100_000_000})).toBe('100\xa0MB');
8-
expect(formatBytes({value: 100_000_000_000})).toBe('100\xa0GB');
9-
expect(formatBytes({value: 100_000_000_000_000})).toBe('100\xa0TB');
6+
expect(formatBytes({value: 100})).toBe(`100${unbreakableGap}B`);
7+
expect(formatBytes({value: 100_000})).toBe(`100${unbreakableGap}KB`);
8+
expect(formatBytes({value: 100_000_000})).toBe(`100${unbreakableGap}MB`);
9+
expect(formatBytes({value: 100_000_000_000})).toBe(`100${unbreakableGap}GB`);
10+
expect(formatBytes({value: 100_000_000_000_000})).toBe(`100${unbreakableGap}TB`);
1011
});
1112
it('should convert to size', () => {
12-
expect(formatBytes({value: 100_000, size: 'b'})).toBe('100\xa0000\xa0B');
13-
expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe('100\xa0000\xa0GB');
13+
expect(formatBytes({value: 100_000, size: 'b'})).toBe(
14+
`100${unbreakableGap}000${unbreakableGap}B`,
15+
);
16+
expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe(
17+
`100${unbreakableGap}000${unbreakableGap}GB`,
18+
);
1419
});
1520
it('should convert without labels', () => {
16-
expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe('100\xa0000');
21+
expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe(
22+
`100${unbreakableGap}000`,
23+
);
1724
expect(formatBytes({value: 100_000_000_000_000, size: 'gb', withSizeLabel: false})).toBe(
18-
'100\xa0000',
25+
`100${unbreakableGap}000`,
1926
);
2027
});
2128
it('should convert to speed', () => {
22-
expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe('100\xa0KB/s');
29+
expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe(
30+
`100${unbreakableGap}KB/s`,
31+
);
2332
expect(formatBytes({value: 100_000, size: 'b', withSpeedLabel: true})).toBe(
24-
'100\xa0000\xa0B/s',
33+
`100${unbreakableGap}000${unbreakableGap}B/s`,
2534
);
2635
});
2736
it('should return fixed amount of significant digits', () => {
28-
expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual('99\xa0000\xa0B');
29-
expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual('100\xa0KB');
37+
expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual(
38+
`99${unbreakableGap}000${unbreakableGap}B`,
39+
);
40+
expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual(
41+
`100${unbreakableGap}KB`,
42+
);
3043
expect(formatBytes({value: 99_000_000_000_000, significantDigits: 2})).toEqual(
31-
'99\xa0000\xa0GB',
44+
`99${unbreakableGap}000${unbreakableGap}GB`,
3245
);
3346
expect(formatBytes({value: 100_000_000_000_000, significantDigits: 2})).toEqual(
34-
'100\xa0TB',
47+
`100${unbreakableGap}TB`,
3548
);
3649
});
3750
it('should return empty string on invalid data', () => {
@@ -42,12 +55,12 @@ describe('formatBytes', () => {
4255
expect(formatBytes({value: '123qwe'})).toEqual('');
4356
});
4457
it('should work with precision', () => {
45-
expect(formatBytes({value: 123.123, precision: 2})).toBe('123\xa0B');
46-
expect(formatBytes({value: 12.123, precision: 2})).toBe('12\xa0B');
47-
expect(formatBytes({value: 1.123, precision: 2})).toBe('1.1\xa0B');
48-
expect(formatBytes({value: 0.123, precision: 2})).toBe('0.12\xa0B');
49-
expect(formatBytes({value: 0.012, precision: 2})).toBe('0.01\xa0B');
50-
expect(formatBytes({value: 0.001, precision: 2})).toBe('0\xa0B');
51-
expect(formatBytes({value: 0, precision: 2})).toBe('0\xa0B');
58+
expect(formatBytes({value: 123.123, precision: 2})).toBe(`123${unbreakableGap}B`);
59+
expect(formatBytes({value: 12.123, precision: 2})).toBe(`12${unbreakableGap}B`);
60+
expect(formatBytes({value: 1.123, precision: 2})).toBe(`1.1${unbreakableGap}B`);
61+
expect(formatBytes({value: 0.123, precision: 2})).toBe(`0.12${unbreakableGap}B`);
62+
expect(formatBytes({value: 0.012, precision: 2})).toBe(`0.01${unbreakableGap}B`);
63+
expect(formatBytes({value: 0.001, precision: 2})).toBe(`0${unbreakableGap}B`);
64+
expect(formatBytes({value: 0, precision: 2})).toBe(`0${unbreakableGap}B`);
5265
});
5366
});

src/utils/bytesParsers/formatBytes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE} from '../constants';
22
import {formatNumber, roundToPrecision} from '../dataFormatters/dataFormatters';
3-
import {isNumeric} from '../utils';
3+
import {isNumeric, unbreakableGap} from '../utils';
44

55
import i18n from './i18n';
66

@@ -84,7 +84,7 @@ const formatToSize = ({value, size = 'mb', precision = 0}: FormatToSizeArgs) =>
8484
return formatNumber(result);
8585
};
8686

87-
const addSizeLabel = (result: string, size: BytesSizes, delimiter = '\xa0') => {
87+
const addSizeLabel = (result: string, size: BytesSizes, delimiter = unbreakableGap) => {
8888
return result + delimiter + sizes[size].label;
8989
};
9090

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {unbreakableGap} from '../../utils';
2+
import {formatStorageValues} from '../dataFormatters';
3+
4+
describe('formatStorageValues', () => {
5+
it('should return ["", ""] when both value and total are undefined', () => {
6+
const result = formatStorageValues();
7+
expect(result).toEqual(['', '']);
8+
});
9+
10+
it('should format value correctly when total is undefined', () => {
11+
const result = formatStorageValues(1024);
12+
expect(result).toEqual([`1${unbreakableGap}KB`, '']);
13+
});
14+
15+
it('should format total correctly when value is undefined', () => {
16+
const result = formatStorageValues(undefined, 2048);
17+
expect(result).toEqual(['', `2${unbreakableGap}KB`]);
18+
});
19+
20+
it('should format both value and total correctly', () => {
21+
const result = formatStorageValues(1024, 2048);
22+
expect(result).toEqual(['1', `2${unbreakableGap}KB`]);
23+
});
24+
25+
it('should handle small value compared to total and increase precision', () => {
26+
const result = formatStorageValues(1, 1024);
27+
expect(result).toEqual(['0.001', `1${unbreakableGap}KB`]);
28+
});
29+
30+
it('should return ["0", formattedTotal] when value is 0', () => {
31+
const result = formatStorageValues(0, 2048);
32+
expect(result).toEqual(['0', `2${unbreakableGap}KB`]);
33+
});
34+
35+
it('should use provided size and delimiter', () => {
36+
const result = formatStorageValues(5120, 10240, 'mb', '/');
37+
expect(result).toEqual(['0.01', '0/MB']);
38+
});
39+
40+
it('should handle non-numeric total gracefully', () => {
41+
const result = formatStorageValues(2048, 'Not a number' as any);
42+
expect(result).toEqual([`2${unbreakableGap}KB`, '']);
43+
});
44+
});

src/utils/numeral.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import numeral from 'numeral';
22
import 'numeral/locales'; // Without this numeral will throw an error when using not 'en' locale
33

44
import {Lang, i18n} from './i18n';
5+
import {unbreakableGap} from './utils';
56

67
// Set space delimiter for all locales possible in project
78
Object.values(Lang).forEach((value) => {
89
if (numeral.locales[value]) {
9-
numeral.locales[value].delimiters.thousands = '\xa0';
10+
numeral.locales[value].delimiters.thousands = unbreakableGap;
1011
}
1112
});
1213

src/utils/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ export function isNumeric(value?: unknown): value is number | string {
104104
export function toExponential(value: number, precision?: number) {
105105
return Number(value).toExponential(precision);
106106
}
107+
108+
export const unbreakableGap = '\xa0';

0 commit comments

Comments
 (0)