Skip to content

Commit 6c307d8

Browse files
CopilotadameatDaryaVorontsova
authored
feat: add network tab to node page with peer connectivity visualization (#2826)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> Co-authored-by: Daria Vorontsova <darvorontsova@yandex-team.ru> Co-authored-by: Darya <148378389+DaryaVorontsova@users.noreply.github.com>
1 parent fa0abe3 commit 6c307d8

File tree

21 files changed

+662
-8
lines changed

21 files changed

+662
-8
lines changed

src/components/nodesColumns/columns.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../../utils/dataFormatters/dataFormatters';
1515
import {getUsageSeverity} from '../../utils/generateEvaluator';
1616
import type {Column} from '../../utils/tableUtils/types';
17+
import {formatToMs, parseUsToMs} from '../../utils/timeParsers';
1718
import {bytesToSpeed, isNumeric} from '../../utils/utils';
1819
import {CellWithPopover} from '../CellWithPopover/CellWithPopover';
1920
import {MemoryViewer} from '../MemoryViewer/MemoryViewer';
@@ -453,7 +454,7 @@ export function getSendThroughputColumn<T extends {SendThroughput?: string}>():
453454
header: NODES_COLUMNS_TITLES.SendThroughput,
454455
render: ({row}) =>
455456
isNumeric(row.SendThroughput)
456-
? bytesToSpeed(row.SendThroughput)
457+
? bytesToSpeed(row.SendThroughput, 1)
457458
: EMPTY_DATA_PLACEHOLDER,
458459
align: DataTable.RIGHT,
459460
width: 110,
@@ -465,7 +466,7 @@ export function getReceiveThroughputColumn<T extends {ReceiveThroughput?: string
465466
header: NODES_COLUMNS_TITLES.ReceiveThroughput,
466467
render: ({row}) =>
467468
isNumeric(row.ReceiveThroughput)
468-
? bytesToSpeed(row.ReceiveThroughput)
469+
? bytesToSpeed(row.ReceiveThroughput, 1)
469470
: EMPTY_DATA_PLACEHOLDER,
470471
align: DataTable.RIGHT,
471472
width: 110,
@@ -551,3 +552,33 @@ export function getClockSkewColumn<
551552
width: 110,
552553
};
553554
}
555+
556+
// Peers columns
557+
558+
export function getPeerSkewColumn<T extends {ClockSkewUs?: string | number}>(): Column<T> {
559+
return {
560+
name: NODES_COLUMNS_IDS.ClockSkew,
561+
header: NODES_COLUMNS_TITLES.ClockSkew,
562+
align: DataTable.RIGHT,
563+
width: 110,
564+
resizeMinWidth: 90,
565+
render: ({row}) =>
566+
isNumeric(row.ClockSkewUs)
567+
? formatToMs(parseUsToMs(row.ClockSkewUs, 1))
568+
: EMPTY_DATA_PLACEHOLDER,
569+
};
570+
}
571+
572+
export function getPeerPingColumn<T extends {PingTimeUs?: string | number}>(): Column<T> {
573+
return {
574+
name: NODES_COLUMNS_IDS.PingTime,
575+
header: NODES_COLUMNS_TITLES.PingTime,
576+
align: DataTable.RIGHT,
577+
width: 110,
578+
resizeMinWidth: 90,
579+
render: ({row}) =>
580+
isNumeric(row.PingTimeUs)
581+
? formatToMs(parseUsToMs(row.PingTimeUs))
582+
: EMPTY_DATA_PLACEHOLDER,
583+
};
584+
}

src/containers/Node/Node.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import {Tab, TabList, TabProvider} from '@gravity-ui/uikit';
44
import {skipToken} from '@reduxjs/toolkit/query';
55
import {Helmet} from 'react-helmet-async';
6-
import {useRouteMatch} from 'react-router-dom';
6+
import {useHistory, useRouteMatch} from 'react-router-dom';
77
import {useQueryParams} from 'use-query-params';
88

99
import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle';
@@ -17,6 +17,7 @@ import {
1717
useCapabilitiesLoaded,
1818
useConfigAvailable,
1919
useDiskPagesAvailable,
20+
useViewerPeersHandlerAvailable,
2021
} from '../../store/reducers/capabilities/hooks';
2122
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
2223
import {nodeApi} from '../../store/reducers/node/node';
@@ -27,6 +28,7 @@ import {useIsViewerUser} from '../../utils/hooks/useIsUserAllowedToMakeChanges';
2728
import {checkIsStorageNode} from '../../utils/nodes';
2829
import {useAppTitle} from '../App/AppTitleContext';
2930
import {Configs} from '../Configs/Configs';
31+
import {NodeNetwork} from '../Node/NodeNetwork/NodeNetwork';
3032
import {PaginatedStorage} from '../Storage/PaginatedStorage';
3133
import {Tablets} from '../Tablets/Tablets';
3234

@@ -48,6 +50,7 @@ export function Node() {
4850
const configsAvailable = isViewerUser && hasConfigs;
4951

5052
const dispatch = useTypedDispatch();
53+
const history = useHistory();
5154

5255
const match = useRouteMatch<{id: string; activeTab: string}>(routes.node);
5356

@@ -69,6 +72,7 @@ export function Node() {
6972

7073
const capabilitiesLoaded = useCapabilitiesLoaded();
7174
const isDiskPagesAvailable = useDiskPagesAvailable();
75+
const isPeersHandlerAvailable = useViewerPeersHandlerAvailable();
7276

7377
const pageLoading = isLoading || !capabilitiesLoaded;
7478

@@ -90,13 +94,23 @@ export function Node() {
9094
if (!threadsQuantity) {
9195
skippedTabs.push('threads');
9296
}
97+
if (!isPeersHandlerAvailable) {
98+
skippedTabs.push('network');
99+
}
93100
const actualNodeTabs = NODE_TABS.filter((el) => !skippedTabs.includes(el.id));
94101

95102
const actualActiveTab =
96103
actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0];
97104

98105
return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs};
99-
}, [isStorageNode, isDiskPagesAvailable, activeTabId, threadsQuantity, configsAvailable]);
106+
}, [
107+
isStorageNode,
108+
isDiskPagesAvailable,
109+
isPeersHandlerAvailable,
110+
activeTabId,
111+
threadsQuantity,
112+
configsAvailable,
113+
]);
100114

101115
const database = tenantNameFromQuery?.toString();
102116

@@ -116,6 +130,18 @@ export function Node() {
116130
}
117131
}, [dispatch, database, nodeId, isLoading, isStorageNode, databaseName]);
118132

133+
React.useEffect(() => {
134+
if (!nodeId || !activeTab) {
135+
return;
136+
}
137+
138+
if (activeTab.id !== activeTabId) {
139+
const path = getDefaultNodePath({id: nodeId, activeTab: activeTab.id}, {database});
140+
141+
history.replace(path);
142+
}
143+
}, [nodeId, database, activeTab.id, activeTabId, history, activeTab]);
144+
119145
return (
120146
<div className={b(null)} ref={container}>
121147
{<NodePageHelmet node={node} activeTabTitle={activeTab.title} />}
@@ -281,6 +307,10 @@ function NodePageContent({
281307
return <Configs database={database} scrollContainerRef={parentContainer} />;
282308
}
283309

310+
case 'network': {
311+
return <NodeNetwork nodeId={nodeId} scrollContainerRef={parentContainer} />;
312+
}
313+
284314
default:
285315
return false;
286316
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react';
2+
3+
import {PaginatedTableWithLayout} from '../../../components/PaginatedTable/PaginatedTableWithLayout';
4+
import {TableColumnSetup} from '../../../components/TableColumnSetup/TableColumnSetup';
5+
import {useBridgeModeEnabled} from '../../../store/reducers/capabilities/hooks';
6+
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
7+
import {useSelectedColumns} from '../../../utils/hooks/useSelectedColumns';
8+
import {useNodesPageQueryParams} from '../../Nodes/useNodesPageQueryParams';
9+
10+
import {NodeNetworkControlsWithTableState} from './NodeNetworkControls/NodeNetworkControlsWithTableState';
11+
import {NodeNetworkTable} from './NodeNetworkTable';
12+
import {getNodeNetworkColumns} from './columns';
13+
import {
14+
NODE_NETWORK_COLUMNS_IDS,
15+
NODE_NETWORK_COLUMNS_TITLES,
16+
NODE_NETWORK_DEFAULT_COLUMNS,
17+
NODE_NETWORK_REQUIRED_COLUMNS,
18+
NODE_NETWORK_TABLE_SELECTED_COLUMNS_KEY,
19+
} from './constants';
20+
21+
interface NodeNetworkProps {
22+
nodeId: string;
23+
scrollContainerRef: React.RefObject<HTMLDivElement>;
24+
}
25+
26+
export function NodeNetwork({nodeId, scrollContainerRef}: NodeNetworkProps) {
27+
const database = useDatabaseFromQuery();
28+
const isBridgeModeEnabled = useBridgeModeEnabled();
29+
30+
const {searchValue, handleSearchQueryChange} = useNodesPageQueryParams(
31+
undefined, // We don't need use groupByParams yet
32+
false, // withPeerRoleFilter = false for this tab
33+
);
34+
35+
const allColumns = React.useMemo(() => {
36+
const columns = getNodeNetworkColumns({database});
37+
38+
if (!isBridgeModeEnabled) {
39+
return columns.filter((column) => column.name !== NODE_NETWORK_COLUMNS_IDS.PileName);
40+
}
41+
42+
return columns;
43+
}, [database, isBridgeModeEnabled]);
44+
45+
const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns(
46+
allColumns,
47+
NODE_NETWORK_TABLE_SELECTED_COLUMNS_KEY,
48+
NODE_NETWORK_COLUMNS_TITLES,
49+
NODE_NETWORK_DEFAULT_COLUMNS,
50+
NODE_NETWORK_REQUIRED_COLUMNS,
51+
);
52+
53+
return (
54+
<PaginatedTableWithLayout
55+
controls={
56+
<NodeNetworkControlsWithTableState
57+
searchValue={searchValue}
58+
onSearchChange={handleSearchQueryChange}
59+
/>
60+
}
61+
extraControls={
62+
<TableColumnSetup
63+
popupWidth={200}
64+
items={columnsToSelect}
65+
showStatus
66+
onUpdate={setColumns}
67+
/>
68+
}
69+
table={
70+
<NodeNetworkTable
71+
nodeId={nodeId}
72+
searchValue={searchValue}
73+
columns={columnsToShow}
74+
scrollContainerRef={scrollContainerRef}
75+
/>
76+
}
77+
tableWrapperProps={{
78+
scrollContainerRef,
79+
scrollDependencies: [searchValue],
80+
}}
81+
fullHeight
82+
/>
83+
);
84+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
3+
import {EntitiesCount} from '../../../../components/EntitiesCount';
4+
import {Search} from '../../../../components/Search';
5+
import i18n from '../i18n';
6+
7+
interface NodeNetworkControlsProps {
8+
searchValue: string;
9+
onSearchChange: (value: string) => void;
10+
11+
entitiesCountCurrent: number;
12+
entitiesCountTotal: number;
13+
entitiesLoading: boolean;
14+
}
15+
16+
export function NodeNetworkControls({
17+
searchValue,
18+
onSearchChange,
19+
entitiesCountCurrent,
20+
entitiesCountTotal,
21+
entitiesLoading,
22+
}: NodeNetworkControlsProps) {
23+
return (
24+
<React.Fragment>
25+
<Search
26+
value={searchValue}
27+
onChange={onSearchChange}
28+
placeholder={i18n('search-placeholder')}
29+
width={238}
30+
/>
31+
<EntitiesCount
32+
current={entitiesCountCurrent}
33+
total={entitiesCountTotal}
34+
label={i18n('field_peers')}
35+
loading={entitiesLoading}
36+
/>
37+
</React.Fragment>
38+
);
39+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {usePaginatedTableState} from '../../../../components/PaginatedTable/PaginatedTableContext';
2+
3+
import {NodeNetworkControls} from './NodeNetworkControls';
4+
5+
interface NodeNetworkControlsWithTableStateProps {
6+
searchValue: string;
7+
onSearchChange: (value: string) => void;
8+
}
9+
10+
export function NodeNetworkControlsWithTableState({
11+
searchValue,
12+
onSearchChange,
13+
}: NodeNetworkControlsWithTableStateProps) {
14+
const {tableState} = usePaginatedTableState();
15+
const {foundEntities, totalEntities, isInitialLoad} = tableState;
16+
17+
return (
18+
<NodeNetworkControls
19+
searchValue={searchValue}
20+
onSearchChange={onSearchChange}
21+
entitiesCountCurrent={foundEntities}
22+
entitiesCountTotal={totalEntities}
23+
entitiesLoading={isInitialLoad}
24+
/>
25+
);
26+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
3+
import {ResizeablePaginatedTable} from '../../../components/PaginatedTable';
4+
import type {PaginatedTableData} from '../../../components/PaginatedTable';
5+
import {renderPaginatedTableErrorMessage} from '../../../utils/renderPaginatedTableErrorMessage';
6+
import type {Column} from '../../../utils/tableUtils/types';
7+
8+
import {NODE_NETWORK_COLUMNS_WIDTH_LS_KEY} from './constants';
9+
import {getNodePeers} from './helpers/getNodePeers';
10+
import type {NodePeerRow} from './helpers/nodeNetworkMapper';
11+
import i18n from './i18n';
12+
13+
interface NodeNetworkTableProps {
14+
nodeId: string;
15+
searchValue: string;
16+
columns: Column<NodePeerRow>[];
17+
scrollContainerRef: React.RefObject<HTMLElement>;
18+
onDataFetched?: (data: PaginatedTableData<NodePeerRow>) => void;
19+
}
20+
21+
export function NodeNetworkTable({
22+
nodeId,
23+
searchValue,
24+
columns,
25+
scrollContainerRef,
26+
onDataFetched,
27+
}: NodeNetworkTableProps) {
28+
const filters = React.useMemo(
29+
() => ({
30+
nodeId,
31+
searchValue: searchValue || undefined,
32+
}),
33+
[nodeId, searchValue],
34+
);
35+
36+
const renderEmptyDataMessage = React.useCallback(() => i18n('alert_no-network-data'), []);
37+
38+
return (
39+
<ResizeablePaginatedTable
40+
columnsWidthLSKey={NODE_NETWORK_COLUMNS_WIDTH_LS_KEY}
41+
scrollContainerRef={scrollContainerRef}
42+
columns={columns}
43+
fetchData={getNodePeers}
44+
filters={filters}
45+
tableName={i18n('table_node-peers')}
46+
renderErrorMessage={renderPaginatedTableErrorMessage}
47+
renderEmptyDataMessage={renderEmptyDataMessage}
48+
onDataFetched={onDataFetched}
49+
/>
50+
);
51+
}

0 commit comments

Comments
 (0)