Skip to content

Commit 14650fe

Browse files
feat(Network): add table view
1 parent c71925d commit 14650fe

File tree

25 files changed

+661
-61
lines changed

25 files changed

+661
-61
lines changed

src/components/NodeHostWrapper/NodeHostWrapper.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {PopoverBehavior} from '@gravity-ui/uikit';
22

33
import {getDefaultNodePath} from '../../containers/Node/NodePages';
44
import type {NodeAddress} from '../../types/additionalProps';
5-
import type {TSystemStateInfo} from '../../types/api/nodes';
5+
import type {TNodeInfo, TSystemStateInfo} from '../../types/api/nodes';
66
import {
77
createDeveloperUIInternalPageHref,
88
createDeveloperUILinkWithNodeId,
@@ -13,22 +13,33 @@ import {EntityStatus} from '../EntityStatus/EntityStatus';
1313
import {NodeEndpointsTooltipContent} from '../TooltipsContent';
1414

1515
export type NodeHostData = NodeAddress &
16+
Pick<TNodeInfo, 'ConnectStatus'> &
1617
Pick<TSystemStateInfo, 'SystemState'> & {
1718
NodeId: string | number;
1819
TenantName?: string;
1920
};
2021

22+
export type StatusForIcon = 'SystemState' | 'ConnectStatus';
23+
2124
interface NodeHostWrapperProps {
2225
node: NodeHostData;
2326
getNodeRef?: (node?: NodeAddress) => string | null;
2427
database?: string;
28+
statusForIcon?: StatusForIcon;
2529
}
2630

27-
export const NodeHostWrapper = ({node, getNodeRef, database}: NodeHostWrapperProps) => {
31+
export const NodeHostWrapper = ({
32+
node,
33+
getNodeRef,
34+
database,
35+
statusForIcon,
36+
}: NodeHostWrapperProps) => {
2837
if (!node.Host) {
2938
return <span></span>;
3039
}
3140

41+
const status = statusForIcon === 'ConnectStatus' ? node.ConnectStatus : node.SystemState;
42+
3243
const isNodeAvailable = !isUnavailableNode(node);
3344

3445
let developerUIInternalHref: string | undefined;
@@ -56,12 +67,7 @@ export const NodeHostWrapper = ({node, getNodeRef, database}: NodeHostWrapperPro
5667
behavior={PopoverBehavior.Immediate}
5768
delayClosing={200}
5869
>
59-
<EntityStatus
60-
name={node.Host}
61-
status={node.SystemState}
62-
path={nodePath}
63-
hasClipboardButton
64-
/>
70+
<EntityStatus name={node.Host} status={status} path={nodePath} hasClipboardButton />
6571
</CellWithPopover>
6672
);
6773
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {UNBREAKABLE_GAP} from '../../../utils/utils';
2+
import {prepareClockSkewValue, preparePingTimeValue} from '../utils';
3+
4+
describe('preparePingTimeValue', () => {
5+
it('Should correctly prepare value', () => {
6+
expect(preparePingTimeValue(1)).toEqual(`0${UNBREAKABLE_GAP}ms`);
7+
expect(preparePingTimeValue(100)).toEqual(`0.1${UNBREAKABLE_GAP}ms`);
8+
expect(preparePingTimeValue(5_550)).toEqual(`6${UNBREAKABLE_GAP}ms`);
9+
expect(preparePingTimeValue(100_000)).toEqual(`100${UNBREAKABLE_GAP}ms`);
10+
});
11+
});
12+
13+
describe('prepareClockSkewValue', () => {
14+
it('Should correctly prepare 0 or very low values', () => {
15+
expect(prepareClockSkewValue(0)).toEqual(`0${UNBREAKABLE_GAP}ms`);
16+
expect(prepareClockSkewValue(10)).toEqual(`0${UNBREAKABLE_GAP}ms`);
17+
expect(prepareClockSkewValue(-10)).toEqual(`0${UNBREAKABLE_GAP}ms`);
18+
});
19+
it('Should correctly prepare positive values', () => {
20+
expect(prepareClockSkewValue(100)).toEqual(`+0.1${UNBREAKABLE_GAP}ms`);
21+
expect(prepareClockSkewValue(5_500)).toEqual(`+6${UNBREAKABLE_GAP}ms`);
22+
expect(prepareClockSkewValue(100_000)).toEqual(`+100${UNBREAKABLE_GAP}ms`);
23+
});
24+
25+
it('Should correctly prepare negative values', () => {
26+
expect(prepareClockSkewValue(-100)).toEqual(`-0.1${UNBREAKABLE_GAP}ms`);
27+
expect(prepareClockSkewValue(-5_500)).toEqual(`-6${UNBREAKABLE_GAP}ms`);
28+
expect(prepareClockSkewValue(-100_000)).toEqual(`-100${UNBREAKABLE_GAP}ms`);
29+
});
30+
});

src/components/nodesColumns/columns.tsx

Lines changed: 176 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ import {valueIsDefined} from '../../utils';
88
import {cn} from '../../utils/cn';
99
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
1010
import {
11+
formatPercent,
1112
formatStorageValues,
1213
formatStorageValuesToGb,
1314
} from '../../utils/dataFormatters/dataFormatters';
1415
import {getSpaceUsageSeverity} from '../../utils/storage';
1516
import type {Column} from '../../utils/tableUtils/types';
16-
import {isNumeric} from '../../utils/utils';
17+
import {bytesToSpeed, isNumeric} from '../../utils/utils';
1718
import {CellWithPopover} from '../CellWithPopover/CellWithPopover';
1819
import {MemoryViewer} from '../MemoryViewer/MemoryViewer';
1920
import {NodeHostWrapper} from '../NodeHostWrapper/NodeHostWrapper';
20-
import type {NodeHostData} from '../NodeHostWrapper/NodeHostWrapper';
21+
import type {NodeHostData, StatusForIcon} from '../NodeHostWrapper/NodeHostWrapper';
2122
import {PoolsGraph} from '../PoolsGraph/PoolsGraph';
2223
import {ProgressViewer} from '../ProgressViewer/ProgressViewer';
2324
import {TabletsStatistic} from '../TabletsStatistic';
@@ -27,6 +28,7 @@ import {UsageLabel} from '../UsageLabel/UsageLabel';
2728
import {NODES_COLUMNS_IDS, NODES_COLUMNS_TITLES} from './constants';
2829
import i18n from './i18n';
2930
import type {GetNodesColumnsParams} from './types';
31+
import {prepareClockSkewValue, preparePingTimeValue} from './utils';
3032

3133
import './NodesColumns.scss';
3234

@@ -41,15 +43,22 @@ export function getNodeIdColumn<T extends {NodeId?: string | number}>(): Column<
4143
align: DataTable.RIGHT,
4244
};
4345
}
44-
export function getHostColumn<T extends NodeHostData>({
45-
getNodeRef,
46-
database,
47-
}: GetNodesColumnsParams): Column<T> {
46+
export function getHostColumn<T extends NodeHostData>(
47+
{getNodeRef, database}: GetNodesColumnsParams,
48+
{statusForIcon = 'SystemState'}: {statusForIcon?: StatusForIcon} = {},
49+
): Column<T> {
4850
return {
4951
name: NODES_COLUMNS_IDS.Host,
5052
header: NODES_COLUMNS_TITLES.Host,
5153
render: ({row}) => {
52-
return <NodeHostWrapper node={row} getNodeRef={getNodeRef} database={database} />;
54+
return (
55+
<NodeHostWrapper
56+
node={row}
57+
getNodeRef={getNodeRef}
58+
database={database}
59+
statusForIcon={statusForIcon}
60+
/>
61+
);
5362
},
5463
width: 350,
5564
align: DataTable.LEFT,
@@ -363,3 +372,163 @@ export function getMissingDisksColumn<T extends {Missing?: number}>(): Column<T>
363372
defaultOrder: DataTable.DESCENDING,
364373
};
365374
}
375+
376+
// Network diagnostics columns
377+
export function getConnectionsColumn<T extends {Connections?: number}>(): Column<T> {
378+
return {
379+
name: NODES_COLUMNS_IDS.Connections,
380+
header: NODES_COLUMNS_TITLES.Connections,
381+
render: ({row}) => (isNumeric(row.Connections) ? row.Connections : EMPTY_DATA_PLACEHOLDER),
382+
align: DataTable.RIGHT,
383+
width: 130,
384+
};
385+
}
386+
export function getNetworkUtilizationColumn<
387+
T extends {
388+
NetworkUtilization?: number;
389+
NetworkUtilizationMin?: number;
390+
NetworkUtilizationMax?: number;
391+
},
392+
>(): Column<T> {
393+
return {
394+
name: NODES_COLUMNS_IDS.NetworkUtilization,
395+
header: NODES_COLUMNS_TITLES.NetworkUtilization,
396+
render: ({row}) => {
397+
const {NetworkUtilization, NetworkUtilizationMin = 0, NetworkUtilizationMax = 0} = row;
398+
399+
if (!isNumeric(NetworkUtilization)) {
400+
return EMPTY_DATA_PLACEHOLDER;
401+
}
402+
403+
return (
404+
<CellWithPopover
405+
placement={['top', 'auto']}
406+
fullWidth
407+
content={
408+
<DefinitionList responsive>
409+
<DefinitionList.Item key={'NetworkUtilization'} name={i18n('sum')}>
410+
{formatPercent(NetworkUtilization)}
411+
</DefinitionList.Item>
412+
<DefinitionList.Item key={'NetworkUtilizationMin'} name={i18n('min')}>
413+
{formatPercent(NetworkUtilizationMin)}
414+
</DefinitionList.Item>
415+
<DefinitionList.Item key={'NetworkUtilizationMax'} name={i18n('max')}>
416+
{formatPercent(NetworkUtilizationMax)}
417+
</DefinitionList.Item>
418+
</DefinitionList>
419+
}
420+
>
421+
{formatPercent(NetworkUtilization)}
422+
</CellWithPopover>
423+
);
424+
},
425+
align: DataTable.RIGHT,
426+
width: 110,
427+
};
428+
}
429+
export function getSendThroughputColumn<T extends {SendThroughput?: string}>(): Column<T> {
430+
return {
431+
name: NODES_COLUMNS_IDS.SendThroughput,
432+
header: NODES_COLUMNS_TITLES.SendThroughput,
433+
render: ({row}) =>
434+
isNumeric(row.SendThroughput)
435+
? bytesToSpeed(row.SendThroughput)
436+
: EMPTY_DATA_PLACEHOLDER,
437+
align: DataTable.RIGHT,
438+
width: 110,
439+
};
440+
}
441+
export function getReceiveThroughputColumn<T extends {ReceiveThroughput?: string}>(): Column<T> {
442+
return {
443+
name: NODES_COLUMNS_IDS.ReceiveThroughput,
444+
header: NODES_COLUMNS_TITLES.ReceiveThroughput,
445+
render: ({row}) =>
446+
isNumeric(row.ReceiveThroughput)
447+
? bytesToSpeed(row.ReceiveThroughput)
448+
: EMPTY_DATA_PLACEHOLDER,
449+
align: DataTable.RIGHT,
450+
width: 110,
451+
};
452+
}
453+
export function getPingTimeColumn<
454+
T extends {
455+
PingTimeUs?: string;
456+
PingTimeMinUs?: string;
457+
PingTimeMaxUs?: string;
458+
},
459+
>(): Column<T> {
460+
return {
461+
name: NODES_COLUMNS_IDS.PingTime,
462+
header: NODES_COLUMNS_TITLES.PingTime,
463+
render: ({row}) => {
464+
const {PingTimeUs, PingTimeMinUs = 0, PingTimeMaxUs = 0} = row;
465+
466+
if (!isNumeric(PingTimeUs)) {
467+
return EMPTY_DATA_PLACEHOLDER;
468+
}
469+
470+
return (
471+
<CellWithPopover
472+
placement={['top', 'auto']}
473+
fullWidth
474+
content={
475+
<DefinitionList responsive>
476+
<DefinitionList.Item key={'PingTimeUs'} name={i18n('avg')}>
477+
{preparePingTimeValue(PingTimeUs)}
478+
</DefinitionList.Item>
479+
<DefinitionList.Item key={'PingTimeMinUs'} name={i18n('min')}>
480+
{preparePingTimeValue(PingTimeMinUs)}
481+
</DefinitionList.Item>
482+
<DefinitionList.Item key={'PingTimeMaxUs'} name={i18n('max')}>
483+
{preparePingTimeValue(PingTimeMaxUs)}
484+
</DefinitionList.Item>
485+
</DefinitionList>
486+
}
487+
>
488+
{preparePingTimeValue(PingTimeUs)}
489+
</CellWithPopover>
490+
);
491+
},
492+
align: DataTable.RIGHT,
493+
width: 110,
494+
};
495+
}
496+
export function getClockSkewColumn<
497+
T extends {ClockSkewUs?: string; ClockSkewMinUs?: string; ClockSkewMaxUs?: string},
498+
>(): Column<T> {
499+
return {
500+
name: NODES_COLUMNS_IDS.ClockSkew,
501+
header: NODES_COLUMNS_TITLES.ClockSkew,
502+
render: ({row}) => {
503+
const {ClockSkewUs, ClockSkewMinUs = 0, ClockSkewMaxUs = 0} = row;
504+
505+
if (!isNumeric(ClockSkewUs)) {
506+
return EMPTY_DATA_PLACEHOLDER;
507+
}
508+
509+
return (
510+
<CellWithPopover
511+
placement={['top', 'auto']}
512+
fullWidth
513+
content={
514+
<DefinitionList responsive>
515+
<DefinitionList.Item key={'ClockSkewUs'} name={i18n('avg')}>
516+
{prepareClockSkewValue(ClockSkewUs)}
517+
</DefinitionList.Item>
518+
<DefinitionList.Item key={'ClockSkewMinUs'} name={i18n('min')}>
519+
{prepareClockSkewValue(ClockSkewMinUs)}
520+
</DefinitionList.Item>
521+
<DefinitionList.Item key={'ClockSkewMaxUs'} name={i18n('max')}>
522+
{prepareClockSkewValue(ClockSkewMaxUs)}
523+
</DefinitionList.Item>
524+
</DefinitionList>
525+
}
526+
>
527+
{prepareClockSkewValue(ClockSkewUs)}
528+
</CellWithPopover>
529+
);
530+
},
531+
align: DataTable.RIGHT,
532+
width: 110,
533+
};
534+
}

src/components/nodesColumns/constants.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const NODES_COLUMNS_IDS = {
2222
Load: 'Load',
2323
DiskSpaceUsage: 'DiskSpaceUsage',
2424
TotalSessions: 'TotalSessions',
25+
Connections: 'Connections',
26+
NetworkUtilization: 'NetworkUtilization',
27+
SendThroughput: 'SendThroughput',
28+
ReceiveThroughput: 'ReceiveThroughput',
29+
PingTime: 'PingTime',
30+
ClockSkew: 'ClockSkew',
2531
Missing: 'Missing',
2632
Tablets: 'Tablets',
2733
PDisks: 'PDisks',
@@ -80,6 +86,24 @@ export const NODES_COLUMNS_TITLES = {
8086
get TotalSessions() {
8187
return i18n('sessions');
8288
},
89+
get Connections() {
90+
return i18n('connections');
91+
},
92+
get NetworkUtilization() {
93+
return i18n('utilization');
94+
},
95+
get SendThroughput() {
96+
return i18n('send');
97+
},
98+
get ReceiveThroughput() {
99+
return i18n('receive');
100+
},
101+
get PingTime() {
102+
return i18n('ping');
103+
},
104+
get ClockSkew() {
105+
return i18n('skew');
106+
},
83107
get Missing() {
84108
return i18n('missing');
85109
},
@@ -162,6 +186,12 @@ export const NODES_COLUMNS_TO_DATA_FIELDS: Record<NodesColumnId, NodesRequiredFi
162186
Load: ['LoadAverage'],
163187
DiskSpaceUsage: ['DiskSpaceUsage'],
164188
TotalSessions: ['SystemState'],
189+
Connections: ['Connections'],
190+
NetworkUtilization: ['NetworkUtilization'],
191+
SendThroughput: ['SendThroughput'],
192+
ReceiveThroughput: ['ReceiveThroughput'],
193+
PingTime: ['PingTime'],
194+
ClockSkew: ['ClockSkew'],
165195
Missing: ['Missing'],
166196
Tablets: ['Tablets', 'Database'],
167197
PDisks: ['PDisks'],
@@ -184,6 +214,12 @@ const NODES_COLUMNS_TO_SORT_FIELDS: Record<NodesColumnId, NodesSortValue | undef
184214
Load: 'LoadAverage',
185215
DiskSpaceUsage: 'DiskSpaceUsage',
186216
TotalSessions: undefined,
217+
Connections: 'Connections',
218+
NetworkUtilization: 'NetworkUtilization',
219+
SendThroughput: 'SendThroughput',
220+
ReceiveThroughput: 'ReceiveThroughput',
221+
PingTime: 'PingTime',
222+
ClockSkew: 'ClockSkew',
187223
Missing: 'Missing',
188224
Tablets: undefined,
189225
PDisks: undefined,

src/components/nodesColumns/i18n/en.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,18 @@
2424

2525
"system-state": "System State",
2626
"connect-status": "Connect Status",
27+
"utilization": "Utilization",
2728
"network-utilization": "Network Utilization",
29+
"connections": "Connections",
2830
"clock-skew": "Clock Skew",
29-
"ping-time": "Ping Time"
31+
"skew": "Skew",
32+
"ping-time": "Ping Time",
33+
"ping": "Ping",
34+
"send": "Send",
35+
"receive": "Receive",
36+
37+
"max": "Max",
38+
"min": "Min",
39+
"avg": "Avg",
40+
"sum": "Sum"
3041
}

0 commit comments

Comments
 (0)