diff --git a/src/containers/Node/Network/NodeNetwork.scss b/src/containers/Node/Network/NodeNetwork.scss new file mode 100644 index 0000000000..d923d11d00 --- /dev/null +++ b/src/containers/Node/Network/NodeNetwork.scss @@ -0,0 +1,98 @@ +.node-network { + &__inner { + padding: 20px; + } + + &__controls-wrapper { + margin-bottom: 20px; + } + + &__controls { + display: flex; + align-items: center; + gap: 16px; + } + + &__problem-filter { + margin-right: 12px; + } + + &__checkbox-wrapper { + display: flex; + align-items: center; + } + + &__nodes-row { + display: flex; + gap: 32px; + align-items: flex-start; + } + + &__left, + &__right { + flex: 1; + } + + &__section-title { + font-size: 16px; + font-weight: 500; + margin-bottom: 16px; + } + + &__nodes-container { + margin-bottom: 24px; + } + + &__nodes-title { + font-size: 14px; + font-weight: 500; + margin-bottom: 12px; + } + + &__nodes { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + &__rack-column { + display: flex; + flex-direction: column; + align-items: center; + margin-right: 16px; + } + + &__rack-index { + font-size: 12px; + margin-bottom: 8px; + min-height: 16px; + } + + &__link { + color: var(--g-color-text-primary); + text-decoration: underline; + + &:hover { + color: var(--g-color-text-primary); + } + } + + &__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + text-align: center; + } + + &__placeholder-img { + margin-bottom: 16px; + opacity: 0.5; + } + + &__placeholder-text { + color: var(--g-color-text-secondary); + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/containers/Node/Network/NodeNetwork.tsx b/src/containers/Node/Network/NodeNetwork.tsx new file mode 100644 index 0000000000..ee16416195 --- /dev/null +++ b/src/containers/Node/Network/NodeNetwork.tsx @@ -0,0 +1,302 @@ +import React from 'react'; + +import {Checkbox, Icon, Loader} from '@gravity-ui/uikit'; +import {Link} from 'react-router-dom'; + +import {ResponseError} from '../../../components/Errors/ResponseError'; +import {Illustration} from '../../../components/Illustration'; +import {ProblemFilter} from '../../../components/ProblemFilter'; +import {networkApi} from '../../../store/reducers/network/network'; +import { + ProblemFilterValues, + changeFilter, + selectProblemFilter, +} from '../../../store/reducers/settings/settings'; +import {hideTooltip, showTooltip} from '../../../store/reducers/tooltip'; +import type {TNetNodeInfo, TNetNodePeerInfo} from '../../../types/api/netInfo'; +import {cn} from '../../../utils/cn'; +import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; +import {getDefaultNodePath} from '../NodePages'; + +import {NodeNetwork as NodeNetworkComponent} from '../../../containers/Tenant/Diagnostics/Network/NodeNetwork/NodeNetwork'; +import {getConnectedNodesCount} from '../../../containers/Tenant/Diagnostics/Network/utils'; + +import networkIcon from '../../../assets/icons/network.svg'; + +import './NodeNetwork.scss'; + +const b = cn('node-network'); + +interface NodeNetworkProps { + nodeId: string; + tenantName?: string; +} + +export function NodeNetwork({nodeId, tenantName}: NodeNetworkProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const filter = useTypedSelector(selectProblemFilter); + const dispatch = useTypedDispatch(); + + const [showId, setShowId] = React.useState(false); + const [showRacks, setShowRacks] = React.useState(false); + + const {currentData, isFetching, error} = networkApi.useGetNetworkInfoQuery( + tenantName || 'unknown', + { + pollingInterval: autoRefreshInterval, + }, + ); + const loading = isFetching && currentData === undefined; + + if (loading) { + return ( +
+ +
+ ); + } + + const netWorkInfo = currentData; + const allNodes = (netWorkInfo?.Tenants && netWorkInfo.Tenants[0].Nodes) ?? []; + + // Find the current node and its peers + const currentNode = allNodes.find((node) => node.NodeId.toString() === nodeId); + const peers = currentNode?.Peers ?? []; + + if (!error && !currentNode) { + return
No network data found for node {nodeId}
; + } + + if (!error && allNodes.length === 0) { + return
No nodes data
; + } + + // Group current node by type for consistent display + const currentNodeGrouped: Record = currentNode + ? {[currentNode.NodeType]: [currentNode]} + : {}; + + // Group peers by type + const peersGrouped = groupNodesByField(peers, 'NodeType'); + + return ( +
+ {error ? : null} + {currentNode ? ( +
+
+
+ { + dispatch(changeFilter(v)); + }} + className={b('problem-filter')} + /> +
+ { + setShowId(!showId); + }} + checked={showId} + > + ID + +
+
+ { + setShowRacks(!showRacks); + }} + checked={showRacks} + > + Racks + +
+
+
+ +
+
+
Current Node
+ +
+ +
+ {peers.length > 0 ? ( +
+
+ Network peers of node{' '} + + {currentNode.NodeId} + +
+
+ +
+
+ ) : ( +
+
+ +
+
+ No network peers found for this node +
+
+ )} +
+
+
+ ) : null} +
+ ); +} + +interface NodesProps { + nodes: Record; + showId?: boolean; + showRacks?: boolean; + filter: ProblemFilterValues; + dispatch: ReturnType; + isCurrentNode: boolean; +} + +function Nodes({nodes, showId, showRacks, filter, dispatch, isCurrentNode}: NodesProps) { + let problemNodesCount = 0; + + const result = Object.keys(nodes).map((key, j) => { + const nodesGroupedByRack = groupNodesByField(nodes[key], 'Rack'); + return ( +
+
{key} nodes
+
+ {showRacks + ? Object.keys(nodesGroupedByRack).map((rackKey, i) => ( +
+
+ {rackKey === 'undefined' ? '?' : rackKey} +
+ {nodesGroupedByRack[rackKey].map((nodeInfo, index) => { + let capacity, connected; + if ('Peers' in nodeInfo && nodeInfo.Peers) { + capacity = nodeInfo.Peers.length; + connected = getConnectedNodesCount(nodeInfo.Peers); + } + + if ( + (filter === ProblemFilterValues.PROBLEMS && + capacity !== connected) || + filter === ProblemFilterValues.ALL + ) { + problemNodesCount++; + return ( + { + dispatch(showTooltip(...params)); + }} + onMouseLeave={() => { + dispatch(hideTooltip()); + }} + onClick={undefined} + isBlurred={false} + /> + ); + } + return null; + })} +
+ )) + : nodes[key].map((nodeInfo, index) => { + let capacity, connected; + if ('Peers' in nodeInfo && nodeInfo.Peers) { + capacity = nodeInfo.Peers.length; + connected = getConnectedNodesCount(nodeInfo.Peers); + } + + if ( + (filter === ProblemFilterValues.PROBLEMS && + capacity !== connected) || + filter === ProblemFilterValues.ALL + ) { + problemNodesCount++; + return ( + { + dispatch(showTooltip(...params)); + }} + onMouseLeave={() => { + dispatch(hideTooltip()); + }} + onClick={undefined} + isBlurred={false} + /> + ); + } + return null; + })} +
+
+ ); + }); + + if (filter === ProblemFilterValues.PROBLEMS && problemNodesCount === 0) { + return ; + } else { + return result; + } +} + +function groupNodesByField>( + nodes: T[], + field: 'NodeType' | 'Rack', +) { + return nodes.reduce>((acc, node) => { + const fieldValue = node[field] || 'undefined'; + if (acc[fieldValue]) { + acc[fieldValue].push(node); + } else { + acc[fieldValue] = [node]; + } + return acc; + }, {}); +} \ No newline at end of file diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 4d7b19a535..044d5c15d7 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -28,6 +28,7 @@ import {Tablets} from '../Tablets/Tablets'; import type {NodeTab} from './NodePages'; import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages'; +import {NodeNetwork} from './Network/NodeNetwork'; import NodeStructure from './NodeStructure/NodeStructure'; import {Threads} from './Threads/Threads'; import i18n from './i18n'; @@ -259,6 +260,10 @@ function NodePageContent({ return ; } + case 'network': { + return ; + } + default: return false; } diff --git a/src/containers/Node/NodePages.ts b/src/containers/Node/NodePages.ts index 3f4c23ac13..34e8eaf85f 100644 --- a/src/containers/Node/NodePages.ts +++ b/src/containers/Node/NodePages.ts @@ -12,6 +12,7 @@ const NODE_TABS_IDS = { tablets: 'tablets', structure: 'structure', threads: 'threads', + network: 'network', } as const; export type NodeTab = ValueOf; @@ -41,6 +42,12 @@ export const NODE_TABS = [ return i18n('tabs.threads'); }, }, + { + id: NODE_TABS_IDS.network, + get title() { + return i18n('tabs.network'); + }, + }, ]; export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets); diff --git a/src/containers/Node/i18n/en.json b/src/containers/Node/i18n/en.json index 0762ce7af3..a395bacb2a 100644 --- a/src/containers/Node/i18n/en.json +++ b/src/containers/Node/i18n/en.json @@ -6,6 +6,7 @@ "tabs.structure": "Structure", "tabs.tablets": "Tablets", "tabs.threads": "Threads", + "tabs.network": "Network", "node": "Node", "fqdn": "FQDN",