-
Notifications
You must be signed in to change notification settings - Fork 17
feat: add network tab to node page with peer connectivity visualization #2826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 19 commits
9a61fde
e96a344
d420599
bac3c4a
4789b5a
931dbd3
3ea01db
aed1a96
6e0088e
2d87248
c62468c
088c9f3
2177317
2b622a1
8bfe94a
f777ecc
560f47e
e22d053
12effdb
68ae8dd
8dd9921
b8e7302
b606d54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import { | |
| useCapabilitiesLoaded, | ||
| useConfigAvailable, | ||
| useDiskPagesAvailable, | ||
| useViewerPeersHandlerAvailable, | ||
| } from '../../store/reducers/capabilities/hooks'; | ||
| import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; | ||
| import {nodeApi} from '../../store/reducers/node/node'; | ||
|
|
@@ -27,6 +28,7 @@ import {useIsViewerUser} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; | |
| import {checkIsStorageNode} from '../../utils/nodes'; | ||
| import {useAppTitle} from '../App/AppTitleContext'; | ||
| import {Configs} from '../Configs/Configs'; | ||
| import {NodeNetwork} from '../Node/NodeNetwork/NodeNetwork'; | ||
| import {PaginatedStorage} from '../Storage/PaginatedStorage'; | ||
| import {Tablets} from '../Tablets/Tablets'; | ||
|
|
||
|
|
@@ -214,6 +216,8 @@ function NodePageContent({ | |
| tabs, | ||
| parentContainer, | ||
| }: NodePageContentProps) { | ||
| const isPeersHandlerAvailable = useViewerPeersHandlerAvailable(); | ||
|
|
||
| const renderTabs = () => { | ||
| return ( | ||
| <div className={b('tabs')}> | ||
|
|
@@ -281,6 +285,14 @@ function NodePageContent({ | |
| return <Configs database={database} scrollContainerRef={parentContainer} />; | ||
| } | ||
|
|
||
| case 'network': { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if not |
||
| if (!isPeersHandlerAvailable) { | ||
| return i18n('alert_no-peers'); | ||
| } | ||
|
|
||
| return <NodeNetwork nodeId={nodeId} scrollContainerRef={parentContainer} />; | ||
| } | ||
|
|
||
| default: | ||
| return false; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import React from 'react'; | ||
|
|
||
| import {PaginatedTableWithLayout} from '../../../components/PaginatedTable/PaginatedTableWithLayout'; | ||
| import {TableColumnSetup} from '../../../components/TableColumnSetup/TableColumnSetup'; | ||
| import {useBridgeModeEnabled} from '../../../store/reducers/capabilities/hooks'; | ||
| import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery'; | ||
| import {useSelectedColumns} from '../../../utils/hooks/useSelectedColumns'; | ||
| import {useNodesPageQueryParams} from '../../Nodes/useNodesPageQueryParams'; | ||
|
|
||
| import {NodeNetworkControlsWithTableState} from './NodeNetworkControls/NodeNetworkControlsWithTableState'; | ||
| import {NodeNetworkTable} from './NodeNetworkTable'; | ||
| import {getNodeNetworkColumns} from './columns'; | ||
| import { | ||
| NODE_NETWORK_COLUMNS_IDS, | ||
| NODE_NETWORK_COLUMNS_TITLES, | ||
| NODE_NETWORK_DEFAULT_COLUMNS, | ||
| NODE_NETWORK_REQUIRED_COLUMNS, | ||
| NODE_NETWORK_TABLE_SELECTED_COLUMNS_KEY, | ||
| } from './constants'; | ||
|
|
||
| interface NodeNetworkProps { | ||
| nodeId: string; | ||
| scrollContainerRef: React.RefObject<HTMLDivElement>; | ||
| } | ||
|
|
||
| export function NodeNetwork({nodeId, scrollContainerRef}: NodeNetworkProps) { | ||
| const database = useDatabaseFromQuery(); | ||
| const isBridgeModeEnabled = useBridgeModeEnabled(); | ||
|
|
||
| const {searchValue, handleSearchQueryChange} = useNodesPageQueryParams( | ||
| undefined, // We don't need use groupByParams yet | ||
| false, // withPeerRoleFilter = false for this tab | ||
| ); | ||
|
|
||
| const allColumns = React.useMemo(() => { | ||
| const columns = getNodeNetworkColumns({database}); | ||
|
|
||
| if (!isBridgeModeEnabled) { | ||
| return columns.filter((column) => column.name !== NODE_NETWORK_COLUMNS_IDS.PileName); | ||
| } | ||
|
|
||
| return columns; | ||
| }, [database, isBridgeModeEnabled]); | ||
|
|
||
| const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( | ||
| allColumns, | ||
| NODE_NETWORK_TABLE_SELECTED_COLUMNS_KEY, | ||
| NODE_NETWORK_COLUMNS_TITLES, | ||
| NODE_NETWORK_DEFAULT_COLUMNS, | ||
| NODE_NETWORK_REQUIRED_COLUMNS, | ||
| ); | ||
|
|
||
| return ( | ||
| <PaginatedTableWithLayout | ||
| controls={ | ||
| <NodeNetworkControlsWithTableState | ||
| searchValue={searchValue} | ||
| onSearchChange={handleSearchQueryChange} | ||
| /> | ||
| } | ||
| extraControls={ | ||
| <TableColumnSetup | ||
| popupWidth={200} | ||
| items={columnsToSelect} | ||
| showStatus | ||
| onUpdate={setColumns} | ||
| /> | ||
| } | ||
| table={ | ||
| <NodeNetworkTable | ||
| nodeId={nodeId} | ||
| searchValue={searchValue} | ||
| columns={columnsToShow} | ||
| scrollContainerRef={scrollContainerRef} | ||
| /> | ||
| } | ||
| tableWrapperProps={{ | ||
| scrollContainerRef, | ||
| scrollDependencies: [searchValue], | ||
| }} | ||
| fullHeight | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import React from 'react'; | ||
|
|
||
| import {EntitiesCount} from '../../../../components/EntitiesCount'; | ||
| import {Search} from '../../../../components/Search'; | ||
|
|
||
| interface NodeNetworkControlsProps { | ||
| searchValue: string; | ||
| onSearchChange: (value: string) => void; | ||
|
|
||
| entitiesCountCurrent: number; | ||
| entitiesCountTotal: number; | ||
| entitiesLoading: boolean; | ||
| } | ||
|
|
||
| export function NodeNetworkControls({ | ||
| searchValue, | ||
| onSearchChange, | ||
| entitiesCountCurrent, | ||
| entitiesCountTotal, | ||
| entitiesLoading, | ||
| }: NodeNetworkControlsProps) { | ||
| return ( | ||
| <React.Fragment> | ||
| <Search | ||
| value={searchValue} | ||
| onChange={onSearchChange} | ||
| placeholder="Search peers" | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| width={238} | ||
| /> | ||
| <EntitiesCount | ||
| current={entitiesCountCurrent} | ||
| total={entitiesCountTotal} | ||
| label="Peers" | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| loading={entitiesLoading} | ||
| /> | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import {usePaginatedTableState} from '../../../../components/PaginatedTable/PaginatedTableContext'; | ||
|
|
||
| import {NodeNetworkControls} from './NodeNetworkControls'; | ||
|
|
||
| interface NodeNetworkControlsWithTableStateProps { | ||
| searchValue: string; | ||
| onSearchChange: (value: string) => void; | ||
| } | ||
|
|
||
| export function NodeNetworkControlsWithTableState({ | ||
| searchValue, | ||
| onSearchChange, | ||
| }: NodeNetworkControlsWithTableStateProps) { | ||
| const {tableState} = usePaginatedTableState(); | ||
| const {foundEntities, totalEntities, isInitialLoad} = tableState; | ||
|
|
||
| return ( | ||
| <NodeNetworkControls | ||
| searchValue={searchValue} | ||
| onSearchChange={onSearchChange} | ||
| entitiesCountCurrent={foundEntities} | ||
| entitiesCountTotal={totalEntities} | ||
| entitiesLoading={isInitialLoad} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import React from 'react'; | ||
|
|
||
| import {ResizeablePaginatedTable} from '../../../components/PaginatedTable'; | ||
| import type {PaginatedTableData} from '../../../components/PaginatedTable'; | ||
| import {renderPaginatedTableErrorMessage} from '../../../utils/renderPaginatedTableErrorMessage'; | ||
| import type {Column} from '../../../utils/tableUtils/types'; | ||
|
|
||
| import {NODE_NETWORK_COLUMNS_WIDTH_LS_KEY} from './constants'; | ||
| import {getNodePeers} from './helpers/getNodePeers'; | ||
| import type {NodePeerRow} from './helpers/nodeNetworkMapper'; | ||
| import i18n from './i18n'; | ||
|
|
||
| interface NodeNetworkTableProps { | ||
| nodeId: string; | ||
| searchValue: string; | ||
| columns: Column<NodePeerRow>[]; | ||
| scrollContainerRef: React.RefObject<HTMLElement>; | ||
| onDataFetched?: (data: PaginatedTableData<NodePeerRow>) => void; | ||
| } | ||
|
|
||
| export function NodeNetworkTable({ | ||
| nodeId, | ||
| searchValue, | ||
| columns, | ||
| scrollContainerRef, | ||
| onDataFetched, | ||
| }: NodeNetworkTableProps) { | ||
| const filters = React.useMemo( | ||
| () => ({ | ||
| nodeId, | ||
| searchValue: searchValue || undefined, | ||
| }), | ||
| [nodeId, searchValue], | ||
| ); | ||
|
|
||
| const renderEmptyDataMessage = React.useCallback(() => i18n('alert_no-network-data'), []); | ||
|
|
||
| return ( | ||
| <ResizeablePaginatedTable | ||
| columnsWidthLSKey={NODE_NETWORK_COLUMNS_WIDTH_LS_KEY} | ||
| scrollContainerRef={scrollContainerRef} | ||
| columns={columns} | ||
| fetchData={getNodePeers} | ||
| filters={filters} | ||
| tableName="node-peers" | ||
| renderErrorMessage={renderPaginatedTableErrorMessage} | ||
| renderEmptyDataMessage={renderEmptyDataMessage} | ||
| onDataFetched={onDataFetched} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import DataTable from '@gravity-ui/react-data-table'; | ||
|
|
||
| import { | ||
| getHostColumn, | ||
| getNodeIdColumn, | ||
| getNodeNameColumn, | ||
| getPeerPingColumn, | ||
| getPeerSkewColumn, | ||
| getPileNameColumn, | ||
| getReceiveThroughputColumn, | ||
| getSendThroughputColumn, | ||
| } from '../../../components/nodesColumns/columns'; | ||
| import type {GetNodesColumnsParams} from '../../../components/nodesColumns/types'; | ||
| import {EMPTY_DATA_PLACEHOLDER} from '../../../lib'; | ||
| import {formatDateTime} from '../../../utils/dataFormatters/dataFormatters'; | ||
| import type {Column} from '../../../utils/tableUtils/types'; | ||
| import {bytesToMB, isNumeric} from '../../../utils/utils'; | ||
|
|
||
| import { | ||
| NODE_NETWORK_COLUMNS_IDS, | ||
| NODE_NETWORK_COLUMNS_TITLES, | ||
| isSortableNodeNetworkColumn, | ||
| } from './constants'; | ||
| import type {NodePeerRow} from './helpers/nodeNetworkMapper'; | ||
|
|
||
| function getPeerConnectTimeColumn<T extends {ConnectTime?: string}>(): Column<T> { | ||
| return { | ||
| name: NODE_NETWORK_COLUMNS_IDS.ConnectTime, | ||
| header: NODE_NETWORK_COLUMNS_TITLES.ConnectTime, | ||
| align: DataTable.LEFT, | ||
| width: 150, | ||
| resizeMinWidth: 120, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please set it up so that the default width is enough to display without ellipsis. |
||
| render: ({row}) => | ||
| row.ConnectTime ? formatDateTime(row.ConnectTime) : EMPTY_DATA_PLACEHOLDER, | ||
| }; | ||
| } | ||
|
|
||
| function getPeerSentBytesColumn<T extends {BytesSend?: string | number}>(): Column<T> { | ||
| return { | ||
| name: NODE_NETWORK_COLUMNS_IDS.BytesSend, | ||
| header: NODE_NETWORK_COLUMNS_TITLES.BytesSend, | ||
| align: DataTable.RIGHT, | ||
| width: 140, | ||
| resizeMinWidth: 120, | ||
| render: ({row}) => | ||
| isNumeric(row.BytesSend) ? bytesToMB(row.BytesSend) : EMPTY_DATA_PLACEHOLDER, | ||
| }; | ||
| } | ||
|
|
||
| function getPeerReceivedBytesColumn<T extends {BytesReceived?: string | number}>(): Column<T> { | ||
| return { | ||
| name: NODE_NETWORK_COLUMNS_IDS.BytesReceived, | ||
| header: NODE_NETWORK_COLUMNS_TITLES.BytesReceived, | ||
| align: DataTable.RIGHT, | ||
| width: 160, | ||
| resizeMinWidth: 130, | ||
| render: ({row}) => | ||
| isNumeric(row.BytesReceived) ? bytesToMB(row.BytesReceived) : EMPTY_DATA_PLACEHOLDER, | ||
| }; | ||
| } | ||
|
|
||
| export function getNodeNetworkColumns(params: GetNodesColumnsParams = {}): Column<NodePeerRow>[] { | ||
| const hostColumn = getHostColumn(params) as unknown as Column<NodePeerRow>; | ||
|
|
||
| const cols: Column<NodePeerRow>[] = [ | ||
| getNodeIdColumn(), | ||
| getNodeNameColumn(), | ||
| getPileNameColumn(), | ||
| hostColumn, | ||
| getPeerConnectTimeColumn(), | ||
| getPeerSkewColumn(), | ||
| getPeerPingColumn(), | ||
| getSendThroughputColumn(), | ||
| getPeerSentBytesColumn(), | ||
| getReceiveThroughputColumn(), | ||
| getPeerReceivedBytesColumn(), | ||
| ]; | ||
|
|
||
| return cols.map((column) => { | ||
| return { | ||
| ...column, | ||
| sortable: isSortableNodeNetworkColumn(column.name), | ||
| }; | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems no need to tune this component?