From 9a61fde9211b56303684cf9770fe343453fd6d3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:53:36 +0000 Subject: [PATCH 1/3] Initial plan From e96a344f7395b7f7a6303cce5b50e4c01715d101 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:09:22 +0000 Subject: [PATCH 2/3] feat: add network peer information to node page Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> --- .../FullNodeViewer/FullNodeViewer.tsx | 19 ++++++- .../FullNodeViewer/NodeNetworkInfo.tsx | 56 +++++++++++++++++++ src/components/FullNodeViewer/i18n/en.json | 12 +++- src/containers/Node/Node.tsx | 7 ++- src/store/reducers/node/node.ts | 18 ++++++ src/utils/yaMetrica.ts | 11 ++-- 6 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/components/FullNodeViewer/NodeNetworkInfo.tsx diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx index 2251740f42..bc4747577e 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.tsx +++ b/src/components/FullNodeViewer/FullNodeViewer.tsx @@ -1,5 +1,7 @@ import {Flex} from '@gravity-ui/uikit'; +import {skipToken} from '@reduxjs/toolkit/query'; +import {nodeApi} from '../../store/reducers/node/node'; import type {PreparedNode} from '../../store/reducers/node/types'; import {cn} from '../../utils/cn'; import {useNodeDeveloperUIHref} from '../../utils/hooks/useNodeDeveloperUIHref'; @@ -10,6 +12,7 @@ import {PoolUsage} from '../PoolUsage/PoolUsage'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {NodeUptime} from '../UptimeViewer/UptimeViewer'; +import {NodeNetworkInfo} from './NodeNetworkInfo'; import i18n from './i18n'; import './FullNodeViewer.scss'; @@ -19,14 +22,20 @@ const b = cn('full-node-viewer'); interface FullNodeViewerProps { node?: PreparedNode; className?: string; + database?: string; } const getLoadAverageIntervalTitle = (index: number) => { return [i18n('la-interval-1m'), i18n('la-interval-5m'), i18n('la-interval-15m')][index]; }; -export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { +export const FullNodeViewer = ({node, className, database}: FullNodeViewerProps) => { const developerUIHref = useNodeDeveloperUIHref(node); + const nodeId = node?.NodeId?.toString(); + const {currentData: nodeNetworkInfo} = nodeApi.useGetNodeNetworkInfoQuery( + nodeId && database ? {nodeId, database} : skipToken, + ); + const commonInfo: InfoViewerItem[] = []; if (node?.Tenants?.length) { @@ -86,6 +95,14 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { info={endpointsInfo} /> ) : null} + + {nodeNetworkInfo ? ( + + ) : null} diff --git a/src/components/FullNodeViewer/NodeNetworkInfo.tsx b/src/components/FullNodeViewer/NodeNetworkInfo.tsx new file mode 100644 index 0000000000..b69c221dbe --- /dev/null +++ b/src/components/FullNodeViewer/NodeNetworkInfo.tsx @@ -0,0 +1,56 @@ +import {getDefaultNodePath} from '../../containers/Node/NodePages'; +import type {TNodeInfo} from '../../types/api/nodes'; +import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; +import {InfoViewer} from '../InfoViewer/InfoViewer'; +import type {InfoViewerItem} from '../InfoViewer/InfoViewer'; +import {InternalLink} from '../InternalLink'; + +import i18n from './i18n'; + +interface NodeNetworkInfoProps { + nodeNetworkInfo?: TNodeInfo | null; + className?: string; + database?: string; +} + +export const NodeNetworkInfo = ({nodeNetworkInfo, className, database}: NodeNetworkInfoProps) => { + const peers = nodeNetworkInfo?.Peers; + + if (!peers || peers.length === 0) { + return null; + } + + const connectedPeers = peers.filter((peer) => peer.Connected); + const totalPeers = peers.length; + + const networkInfo: InfoViewerItem[] = [ + { + label: i18n('network.total-peers'), + value: totalPeers, + }, + { + label: i18n('network.connected-peers'), + value: connectedPeers.length, + }, + ]; + + // Show up to 5 peers in the info section + const displayPeers = peers.slice(0, 5); + + displayPeers.forEach((peer, index) => { + const nodeLink = peer.NodeId ? ( + + {peer.NodeId} + + ) : ( + EMPTY_DATA_PLACEHOLDER + ); + + networkInfo.push({ + label: `${i18n('network.peer-node-id')} ${index + 1}`, + value: nodeLink, + }); + }); + + return ; +}; diff --git a/src/components/FullNodeViewer/i18n/en.json b/src/components/FullNodeViewer/i18n/en.json index d20f622645..ae9f6dbb52 100644 --- a/src/components/FullNodeViewer/i18n/en.json +++ b/src/components/FullNodeViewer/i18n/en.json @@ -17,5 +17,15 @@ "title.endpoints": "Endpoints", "title.roles": "Roles", "title.pools": "Pools", - "title.load-average": "Load average" + "title.load-average": "Load average", + "title.network": "Network", + + "network.connected-peers": "Connected peers", + "network.total-peers": "Total peers", + "network.peer-node-id": "Node ID", + "network.peer-host": "Host", + "network.peer-dc": "DC", + "network.peer-rack": "Rack", + "network.peer-connected": "Connected", + "network.peer-status": "Status" } diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 4d7b19a535..acb395fc7f 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -109,7 +109,7 @@ export function Node() { {} {} {error ? : null} - {} + {} {nodeId ? ( ; } - return ; + return ; } interface NodePageContentProps { diff --git a/src/store/reducers/node/node.ts b/src/store/reducers/node/node.ts index 645f206528..9f6b7c40d9 100644 --- a/src/store/reducers/node/node.ts +++ b/src/store/reducers/node/node.ts @@ -15,6 +15,24 @@ export const nodeApi = api.injectEndpoints({ }, providesTags: ['All'], }), + getNodeNetworkInfo: build.query({ + queryFn: async ({nodeId, database}: {nodeId: string; database?: string}, {signal}) => { + try { + const data = await window.api.viewer.getNodes( + { + node_id: nodeId, + database, + fieldsRequired: ['Peers'], + }, + {signal}, + ); + return {data: data.Nodes?.[0] || null}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), getNodeStructure: build.query({ queryFn: async ({nodeId}: {nodeId: string}, {signal}) => { try { diff --git a/src/utils/yaMetrica.ts b/src/utils/yaMetrica.ts index b6465eee4b..2b8f37b125 100644 --- a/src/utils/yaMetrica.ts +++ b/src/utils/yaMetrica.ts @@ -2,11 +2,10 @@ import {uiFactory} from '../uiFactory/uiFactory'; /** * Interface for a counter that provides methods for tracking metrics. - * - * @method hit - Tracks a hit event with optional arguments. https://yandex.ru/support/metrica/ru/objects/hit - * @method params - Sets parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/params-method - * @method userParams - Sets user-specific parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/user-params - * @method reachGoal - Tracks a goal achievement event with optional arguments. https://yandex.ru/support/metrica/ru/objects/reachgoal + * @function hit - Tracks a hit event with optional arguments. https://yandex.ru/support/metrica/ru/objects/hit + * @function params - Sets parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/params-method + * @function userParams - Sets user-specific parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/user-params + * @function reachGoal - Tracks a goal achievement event with optional arguments. https://yandex.ru/support/metrica/ru/objects/reachgoal */ export interface Counter { hit: (...args: unknown[]) => void; @@ -21,7 +20,6 @@ const yaMetricaMap = uiFactory.yaMetricaMap; * A fake implementation of a counter metric for Yandex.Metrica. * This class is used when the actual Yandex.Metrica counter is not defined, * and it provides a warning message the first time any of its methods are called. - * * @property name - The name of the counter. * @property warnShown - Flag to indicate if the warning has been shown. */ @@ -61,7 +59,6 @@ class FakeMetrica implements Counter { /** * Retrieves a Yandex Metrica instance by name from the global window object. * If no instance is found for the given name, returns a FakeMetrica instance instead. - * * @param name The name of the metrica to retrieve * @returns The Yandex Metrica instance if found, otherwise a FakeMetrica instance */ From d4205992ea8204bcb018ed1fe6870352b1fc7910 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 04:25:15 +0000 Subject: [PATCH 3/3] feat: add network tab to node page with peer connectivity visualization Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> --- .../FullNodeViewer/FullNodeViewer.tsx | 19 +- .../FullNodeViewer/NodeNetworkInfo.tsx | 56 ---- src/components/FullNodeViewer/i18n/en.json | 12 +- src/containers/Node/Network/NodeNetwork.scss | 98 ++++++ src/containers/Node/Network/NodeNetwork.tsx | 302 ++++++++++++++++++ src/containers/Node/Node.tsx | 12 +- src/containers/Node/NodePages.ts | 7 + src/containers/Node/i18n/en.json | 1 + src/store/reducers/node/node.ts | 18 -- src/utils/yaMetrica.ts | 11 +- 10 files changed, 425 insertions(+), 111 deletions(-) delete mode 100644 src/components/FullNodeViewer/NodeNetworkInfo.tsx create mode 100644 src/containers/Node/Network/NodeNetwork.scss create mode 100644 src/containers/Node/Network/NodeNetwork.tsx diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx index bc4747577e..2251740f42 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.tsx +++ b/src/components/FullNodeViewer/FullNodeViewer.tsx @@ -1,7 +1,5 @@ import {Flex} from '@gravity-ui/uikit'; -import {skipToken} from '@reduxjs/toolkit/query'; -import {nodeApi} from '../../store/reducers/node/node'; import type {PreparedNode} from '../../store/reducers/node/types'; import {cn} from '../../utils/cn'; import {useNodeDeveloperUIHref} from '../../utils/hooks/useNodeDeveloperUIHref'; @@ -12,7 +10,6 @@ import {PoolUsage} from '../PoolUsage/PoolUsage'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {NodeUptime} from '../UptimeViewer/UptimeViewer'; -import {NodeNetworkInfo} from './NodeNetworkInfo'; import i18n from './i18n'; import './FullNodeViewer.scss'; @@ -22,20 +19,14 @@ const b = cn('full-node-viewer'); interface FullNodeViewerProps { node?: PreparedNode; className?: string; - database?: string; } const getLoadAverageIntervalTitle = (index: number) => { return [i18n('la-interval-1m'), i18n('la-interval-5m'), i18n('la-interval-15m')][index]; }; -export const FullNodeViewer = ({node, className, database}: FullNodeViewerProps) => { +export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { const developerUIHref = useNodeDeveloperUIHref(node); - const nodeId = node?.NodeId?.toString(); - const {currentData: nodeNetworkInfo} = nodeApi.useGetNodeNetworkInfoQuery( - nodeId && database ? {nodeId, database} : skipToken, - ); - const commonInfo: InfoViewerItem[] = []; if (node?.Tenants?.length) { @@ -95,14 +86,6 @@ export const FullNodeViewer = ({node, className, database}: FullNodeViewerProps) info={endpointsInfo} /> ) : null} - - {nodeNetworkInfo ? ( - - ) : null} diff --git a/src/components/FullNodeViewer/NodeNetworkInfo.tsx b/src/components/FullNodeViewer/NodeNetworkInfo.tsx deleted file mode 100644 index b69c221dbe..0000000000 --- a/src/components/FullNodeViewer/NodeNetworkInfo.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import {getDefaultNodePath} from '../../containers/Node/NodePages'; -import type {TNodeInfo} from '../../types/api/nodes'; -import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; -import {InfoViewer} from '../InfoViewer/InfoViewer'; -import type {InfoViewerItem} from '../InfoViewer/InfoViewer'; -import {InternalLink} from '../InternalLink'; - -import i18n from './i18n'; - -interface NodeNetworkInfoProps { - nodeNetworkInfo?: TNodeInfo | null; - className?: string; - database?: string; -} - -export const NodeNetworkInfo = ({nodeNetworkInfo, className, database}: NodeNetworkInfoProps) => { - const peers = nodeNetworkInfo?.Peers; - - if (!peers || peers.length === 0) { - return null; - } - - const connectedPeers = peers.filter((peer) => peer.Connected); - const totalPeers = peers.length; - - const networkInfo: InfoViewerItem[] = [ - { - label: i18n('network.total-peers'), - value: totalPeers, - }, - { - label: i18n('network.connected-peers'), - value: connectedPeers.length, - }, - ]; - - // Show up to 5 peers in the info section - const displayPeers = peers.slice(0, 5); - - displayPeers.forEach((peer, index) => { - const nodeLink = peer.NodeId ? ( - - {peer.NodeId} - - ) : ( - EMPTY_DATA_PLACEHOLDER - ); - - networkInfo.push({ - label: `${i18n('network.peer-node-id')} ${index + 1}`, - value: nodeLink, - }); - }); - - return ; -}; diff --git a/src/components/FullNodeViewer/i18n/en.json b/src/components/FullNodeViewer/i18n/en.json index ae9f6dbb52..d20f622645 100644 --- a/src/components/FullNodeViewer/i18n/en.json +++ b/src/components/FullNodeViewer/i18n/en.json @@ -17,15 +17,5 @@ "title.endpoints": "Endpoints", "title.roles": "Roles", "title.pools": "Pools", - "title.load-average": "Load average", - "title.network": "Network", - - "network.connected-peers": "Connected peers", - "network.total-peers": "Total peers", - "network.peer-node-id": "Node ID", - "network.peer-host": "Host", - "network.peer-dc": "DC", - "network.peer-rack": "Rack", - "network.peer-connected": "Connected", - "network.peer-status": "Status" + "title.load-average": "Load average" } 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 acb395fc7f..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'; @@ -109,7 +110,7 @@ export function Node() { {} {} {error ? : null} - {} + {} {nodeId ? ( ; } - return ; + return ; } interface NodePageContentProps { @@ -260,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", diff --git a/src/store/reducers/node/node.ts b/src/store/reducers/node/node.ts index 9f6b7c40d9..645f206528 100644 --- a/src/store/reducers/node/node.ts +++ b/src/store/reducers/node/node.ts @@ -15,24 +15,6 @@ export const nodeApi = api.injectEndpoints({ }, providesTags: ['All'], }), - getNodeNetworkInfo: build.query({ - queryFn: async ({nodeId, database}: {nodeId: string; database?: string}, {signal}) => { - try { - const data = await window.api.viewer.getNodes( - { - node_id: nodeId, - database, - fieldsRequired: ['Peers'], - }, - {signal}, - ); - return {data: data.Nodes?.[0] || null}; - } catch (error) { - return {error}; - } - }, - providesTags: ['All'], - }), getNodeStructure: build.query({ queryFn: async ({nodeId}: {nodeId: string}, {signal}) => { try { diff --git a/src/utils/yaMetrica.ts b/src/utils/yaMetrica.ts index 2b8f37b125..b6465eee4b 100644 --- a/src/utils/yaMetrica.ts +++ b/src/utils/yaMetrica.ts @@ -2,10 +2,11 @@ import {uiFactory} from '../uiFactory/uiFactory'; /** * Interface for a counter that provides methods for tracking metrics. - * @function hit - Tracks a hit event with optional arguments. https://yandex.ru/support/metrica/ru/objects/hit - * @function params - Sets parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/params-method - * @function userParams - Sets user-specific parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/user-params - * @function reachGoal - Tracks a goal achievement event with optional arguments. https://yandex.ru/support/metrica/ru/objects/reachgoal + * + * @method hit - Tracks a hit event with optional arguments. https://yandex.ru/support/metrica/ru/objects/hit + * @method params - Sets parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/params-method + * @method userParams - Sets user-specific parameters for the counter with optional arguments. https://yandex.ru/support/metrica/ru/objects/user-params + * @method reachGoal - Tracks a goal achievement event with optional arguments. https://yandex.ru/support/metrica/ru/objects/reachgoal */ export interface Counter { hit: (...args: unknown[]) => void; @@ -20,6 +21,7 @@ const yaMetricaMap = uiFactory.yaMetricaMap; * A fake implementation of a counter metric for Yandex.Metrica. * This class is used when the actual Yandex.Metrica counter is not defined, * and it provides a warning message the first time any of its methods are called. + * * @property name - The name of the counter. * @property warnShown - Flag to indicate if the warning has been shown. */ @@ -59,6 +61,7 @@ class FakeMetrica implements Counter { /** * Retrieves a Yandex Metrica instance by name from the global window object. * If no instance is found for the given name, returns a FakeMetrica instance instead. + * * @param name The name of the metrica to retrieve * @returns The Yandex Metrica instance if found, otherwise a FakeMetrica instance */