diff --git a/src/components/NodeHostWrapper/NodeHostWrapper.tsx b/src/components/NodeHostWrapper/NodeHostWrapper.tsx index 2ab44f11b..a17960272 100644 --- a/src/components/NodeHostWrapper/NodeHostWrapper.tsx +++ b/src/components/NodeHostWrapper/NodeHostWrapper.tsx @@ -2,7 +2,7 @@ import {PopoverBehavior} from '@gravity-ui/uikit'; import {getDefaultNodePath} from '../../containers/Node/NodePages'; import type {NodeAddress} from '../../types/additionalProps'; -import type {TSystemStateInfo} from '../../types/api/nodes'; +import type {TNodeInfo, TSystemStateInfo} from '../../types/api/nodes'; import { createDeveloperUIInternalPageHref, createDeveloperUILinkWithNodeId, @@ -13,22 +13,33 @@ import {EntityStatus} from '../EntityStatus/EntityStatus'; import {NodeEndpointsTooltipContent} from '../TooltipsContent'; export type NodeHostData = NodeAddress & + Pick & Pick & { NodeId: string | number; TenantName?: string; }; +export type StatusForIcon = 'SystemState' | 'ConnectStatus'; + interface NodeHostWrapperProps { node: NodeHostData; getNodeRef?: (node?: NodeAddress) => string | null; database?: string; + statusForIcon?: StatusForIcon; } -export const NodeHostWrapper = ({node, getNodeRef, database}: NodeHostWrapperProps) => { +export const NodeHostWrapper = ({ + node, + getNodeRef, + database, + statusForIcon, +}: NodeHostWrapperProps) => { if (!node.Host) { return ; } + const status = statusForIcon === 'ConnectStatus' ? node.ConnectStatus : node.SystemState; + const isNodeAvailable = !isUnavailableNode(node); let developerUIInternalHref: string | undefined; @@ -56,12 +67,7 @@ export const NodeHostWrapper = ({node, getNodeRef, database}: NodeHostWrapperPro behavior={PopoverBehavior.Immediate} delayClosing={200} > - + ); }; diff --git a/src/components/nodesColumns/__test__/utils.test.ts b/src/components/nodesColumns/__test__/utils.test.ts new file mode 100644 index 000000000..683184642 --- /dev/null +++ b/src/components/nodesColumns/__test__/utils.test.ts @@ -0,0 +1,30 @@ +import {UNBREAKABLE_GAP} from '../../../utils/utils'; +import {prepareClockSkewValue, preparePingTimeValue} from '../utils'; + +describe('preparePingTimeValue', () => { + it('Should correctly prepare value', () => { + expect(preparePingTimeValue(1)).toEqual(`0${UNBREAKABLE_GAP}ms`); + expect(preparePingTimeValue(100)).toEqual(`0.1${UNBREAKABLE_GAP}ms`); + expect(preparePingTimeValue(5_550)).toEqual(`6${UNBREAKABLE_GAP}ms`); + expect(preparePingTimeValue(100_000)).toEqual(`100${UNBREAKABLE_GAP}ms`); + }); +}); + +describe('prepareClockSkewValue', () => { + it('Should correctly prepare 0 or very low values', () => { + expect(prepareClockSkewValue(0)).toEqual(`0${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(10)).toEqual(`0${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(-10)).toEqual(`0${UNBREAKABLE_GAP}ms`); + }); + it('Should correctly prepare positive values', () => { + expect(prepareClockSkewValue(100)).toEqual(`+0.1${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(5_500)).toEqual(`+6${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(100_000)).toEqual(`+100${UNBREAKABLE_GAP}ms`); + }); + + it('Should correctly prepare negative values', () => { + expect(prepareClockSkewValue(-100)).toEqual(`-0.1${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(-5_500)).toEqual(`-6${UNBREAKABLE_GAP}ms`); + expect(prepareClockSkewValue(-100_000)).toEqual(`-100${UNBREAKABLE_GAP}ms`); + }); +}); diff --git a/src/components/nodesColumns/columns.tsx b/src/components/nodesColumns/columns.tsx index 88ba1f9ff..e2f73e7dc 100644 --- a/src/components/nodesColumns/columns.tsx +++ b/src/components/nodesColumns/columns.tsx @@ -8,16 +8,17 @@ import {valueIsDefined} from '../../utils'; import {cn} from '../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import { + formatPercent, formatStorageValues, formatStorageValuesToGb, } from '../../utils/dataFormatters/dataFormatters'; import {getSpaceUsageSeverity} from '../../utils/storage'; import type {Column} from '../../utils/tableUtils/types'; -import {isNumeric} from '../../utils/utils'; +import {bytesToSpeed, isNumeric} from '../../utils/utils'; import {CellWithPopover} from '../CellWithPopover/CellWithPopover'; import {MemoryViewer} from '../MemoryViewer/MemoryViewer'; import {NodeHostWrapper} from '../NodeHostWrapper/NodeHostWrapper'; -import type {NodeHostData} from '../NodeHostWrapper/NodeHostWrapper'; +import type {NodeHostData, StatusForIcon} from '../NodeHostWrapper/NodeHostWrapper'; import {PoolsGraph} from '../PoolsGraph/PoolsGraph'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {TabletsStatistic} from '../TabletsStatistic'; @@ -27,6 +28,7 @@ import {UsageLabel} from '../UsageLabel/UsageLabel'; import {NODES_COLUMNS_IDS, NODES_COLUMNS_TITLES} from './constants'; import i18n from './i18n'; import type {GetNodesColumnsParams} from './types'; +import {prepareClockSkewValue, preparePingTimeValue} from './utils'; import './NodesColumns.scss'; @@ -41,15 +43,22 @@ export function getNodeIdColumn(): Column< align: DataTable.RIGHT, }; } -export function getHostColumn({ - getNodeRef, - database, -}: GetNodesColumnsParams): Column { +export function getHostColumn( + {getNodeRef, database}: GetNodesColumnsParams, + {statusForIcon = 'SystemState'}: {statusForIcon?: StatusForIcon} = {}, +): Column { return { name: NODES_COLUMNS_IDS.Host, header: NODES_COLUMNS_TITLES.Host, render: ({row}) => { - return ; + return ( + + ); }, width: 350, align: DataTable.LEFT, @@ -363,3 +372,163 @@ export function getMissingDisksColumn(): Column defaultOrder: DataTable.DESCENDING, }; } + +// Network diagnostics columns +export function getConnectionsColumn(): Column { + return { + name: NODES_COLUMNS_IDS.Connections, + header: NODES_COLUMNS_TITLES.Connections, + render: ({row}) => (isNumeric(row.Connections) ? row.Connections : EMPTY_DATA_PLACEHOLDER), + align: DataTable.RIGHT, + width: 130, + }; +} +export function getNetworkUtilizationColumn< + T extends { + NetworkUtilization?: number; + NetworkUtilizationMin?: number; + NetworkUtilizationMax?: number; + }, +>(): Column { + return { + name: NODES_COLUMNS_IDS.NetworkUtilization, + header: NODES_COLUMNS_TITLES.NetworkUtilization, + render: ({row}) => { + const {NetworkUtilization, NetworkUtilizationMin = 0, NetworkUtilizationMax = 0} = row; + + if (!isNumeric(NetworkUtilization)) { + return EMPTY_DATA_PLACEHOLDER; + } + + return ( + + + {formatPercent(NetworkUtilization)} + + + {formatPercent(NetworkUtilizationMin)} + + + {formatPercent(NetworkUtilizationMax)} + + + } + > + {formatPercent(NetworkUtilization)} + + ); + }, + align: DataTable.RIGHT, + width: 110, + }; +} +export function getSendThroughputColumn(): Column { + return { + name: NODES_COLUMNS_IDS.SendThroughput, + header: NODES_COLUMNS_TITLES.SendThroughput, + render: ({row}) => + isNumeric(row.SendThroughput) + ? bytesToSpeed(row.SendThroughput) + : EMPTY_DATA_PLACEHOLDER, + align: DataTable.RIGHT, + width: 110, + }; +} +export function getReceiveThroughputColumn(): Column { + return { + name: NODES_COLUMNS_IDS.ReceiveThroughput, + header: NODES_COLUMNS_TITLES.ReceiveThroughput, + render: ({row}) => + isNumeric(row.ReceiveThroughput) + ? bytesToSpeed(row.ReceiveThroughput) + : EMPTY_DATA_PLACEHOLDER, + align: DataTable.RIGHT, + width: 110, + }; +} +export function getPingTimeColumn< + T extends { + PingTimeUs?: string; + PingTimeMinUs?: string; + PingTimeMaxUs?: string; + }, +>(): Column { + return { + name: NODES_COLUMNS_IDS.PingTime, + header: NODES_COLUMNS_TITLES.PingTime, + render: ({row}) => { + const {PingTimeUs, PingTimeMinUs = 0, PingTimeMaxUs = 0} = row; + + if (!isNumeric(PingTimeUs)) { + return EMPTY_DATA_PLACEHOLDER; + } + + return ( + + + {preparePingTimeValue(PingTimeUs)} + + + {preparePingTimeValue(PingTimeMinUs)} + + + {preparePingTimeValue(PingTimeMaxUs)} + + + } + > + {preparePingTimeValue(PingTimeUs)} + + ); + }, + align: DataTable.RIGHT, + width: 110, + }; +} +export function getClockSkewColumn< + T extends {ClockSkewUs?: string; ClockSkewMinUs?: string; ClockSkewMaxUs?: string}, +>(): Column { + return { + name: NODES_COLUMNS_IDS.ClockSkew, + header: NODES_COLUMNS_TITLES.ClockSkew, + render: ({row}) => { + const {ClockSkewUs, ClockSkewMinUs = 0, ClockSkewMaxUs = 0} = row; + + if (!isNumeric(ClockSkewUs)) { + return EMPTY_DATA_PLACEHOLDER; + } + + return ( + + + {prepareClockSkewValue(ClockSkewUs)} + + + {prepareClockSkewValue(ClockSkewMinUs)} + + + {prepareClockSkewValue(ClockSkewMaxUs)} + + + } + > + {prepareClockSkewValue(ClockSkewUs)} + + ); + }, + align: DataTable.RIGHT, + width: 110, + }; +} diff --git a/src/components/nodesColumns/constants.ts b/src/components/nodesColumns/constants.ts index 788f81632..de75a77c5 100644 --- a/src/components/nodesColumns/constants.ts +++ b/src/components/nodesColumns/constants.ts @@ -22,6 +22,12 @@ export const NODES_COLUMNS_IDS = { Load: 'Load', DiskSpaceUsage: 'DiskSpaceUsage', TotalSessions: 'TotalSessions', + Connections: 'Connections', + NetworkUtilization: 'NetworkUtilization', + SendThroughput: 'SendThroughput', + ReceiveThroughput: 'ReceiveThroughput', + PingTime: 'PingTime', + ClockSkew: 'ClockSkew', Missing: 'Missing', Tablets: 'Tablets', PDisks: 'PDisks', @@ -80,6 +86,24 @@ export const NODES_COLUMNS_TITLES = { get TotalSessions() { return i18n('sessions'); }, + get Connections() { + return i18n('connections'); + }, + get NetworkUtilization() { + return i18n('utilization'); + }, + get SendThroughput() { + return i18n('send'); + }, + get ReceiveThroughput() { + return i18n('receive'); + }, + get PingTime() { + return i18n('ping'); + }, + get ClockSkew() { + return i18n('skew'); + }, get Missing() { return i18n('missing'); }, @@ -162,6 +186,12 @@ export const NODES_COLUMNS_TO_DATA_FIELDS: Record { - const {searchValue, uptimeFilter} = useNodesPageQueryParams(); + const {searchValue, uptimeFilter} = useNodesPageQueryParams(undefined); const {problemFilter} = useProblemFilter(); const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -71,6 +71,7 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) = entitiesCountCurrent={nodes.length} entitiesCountTotal={totalNodes} entitiesLoading={isLoading} + groupByParams={undefined} /> ); }; diff --git a/src/containers/Nodes/NodesControls/NodesControls.tsx b/src/containers/Nodes/NodesControls/NodesControls.tsx index ebf4d851d..45e42e435 100644 --- a/src/containers/Nodes/NodesControls/NodesControls.tsx +++ b/src/containers/Nodes/NodesControls/NodesControls.tsx @@ -9,6 +9,7 @@ import {Search} from '../../../components/Search'; import {UptimeFilter} from '../../../components/UptimeFIlter'; import {useViewerNodesHandlerHasGroupingBySystemState} from '../../../store/reducers/capabilities/hooks'; import {useProblemFilter} from '../../../store/reducers/settings/hooks'; +import type {NodesGroupByField} from '../../../types/api/nodes'; import {getNodesGroupByOptions} from '../columns/constants'; import i18n from '../i18n'; import {b} from '../shared'; @@ -16,6 +17,7 @@ import {useNodesPageQueryParams} from '../useNodesPageQueryParams'; interface NodesControlsProps { withGroupBySelect?: boolean; + groupByParams: NodesGroupByField[] | undefined; columnsToSelect: TableColumnSetupItem[]; handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void; @@ -27,6 +29,7 @@ interface NodesControlsProps { export function NodesControls({ withGroupBySelect, + groupByParams = [], columnsToSelect, handleSelectedColumnsUpdate, @@ -43,11 +46,11 @@ export function NodesControls({ handleSearchQueryChange, handleUptimeFilterChange, handleGroupByParamChange, - } = useNodesPageQueryParams(); + } = useNodesPageQueryParams(groupByParams); const {problemFilter, handleProblemFilterChange} = useProblemFilter(); const systemStateGroupingAvailable = useViewerNodesHandlerHasGroupingBySystemState(); - const groupByoptions = getNodesGroupByOptions(systemStateGroupingAvailable); + const groupByoptions = getNodesGroupByOptions(groupByParams, systemStateGroupingAvailable); const handleGroupBySelectUpdate = (value: string[]) => { handleGroupByParamChange(value[0]); diff --git a/src/containers/Nodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes.tsx index f7b08ec09..5a3f74c98 100644 --- a/src/containers/Nodes/PaginatedNodes.tsx +++ b/src/containers/Nodes/PaginatedNodes.tsx @@ -2,38 +2,66 @@ import React from 'react'; import {ResponseError} from '../../components/Errors/ResponseError'; import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; -import type {RenderControls} from '../../components/PaginatedTable'; +import type {Column, RenderControls} from '../../components/PaginatedTable'; import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; +import {NODES_COLUMNS_TITLES} from '../../components/nodesColumns/constants'; +import type {NodesColumnId} from '../../components/nodesColumns/constants'; import { useCapabilitiesLoaded, useViewerNodesHandlerHasGrouping, } from '../../store/reducers/capabilities/hooks'; import {nodesApi} from '../../store/reducers/nodes/nodes'; +import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; import {useProblemFilter} from '../../store/reducers/settings/hooks'; import type {AdditionalNodesProps} from '../../types/additionalProps'; +import type {NodesGroupByField} from '../../types/api/nodes'; import {useAutoRefreshInterval} from '../../utils/hooks'; +import {useSelectedColumns} from '../../utils/hooks/useSelectedColumns'; import {NodesUptimeFilterValues} from '../../utils/nodes'; import {TableGroup} from '../Storage/TableGroup/TableGroup'; import {useExpandedGroups} from '../Storage/TableGroup/useExpandedTableGroups'; import {NodesControls} from './NodesControls/NodesControls'; import {PaginatedNodesTable} from './PaginatedNodesTable'; -import {useNodesSelectedColumns} from './columns/hooks'; +import {getNodesColumns} from './columns/columns'; +import { + ALL_NODES_GROUP_BY_PARAMS, + DEFAULT_NODES_COLUMNS, + NODES_TABLE_SELECTED_COLUMNS_LS_KEY, + REQUIRED_NODES_COLUMNS, +} from './columns/constants'; import i18n from './i18n'; import {b} from './shared'; import {useNodesPageQueryParams} from './useNodesPageQueryParams'; import './Nodes.scss'; -interface PaginatedNodesProps { +export interface PaginatedNodesProps { path?: string; database?: string; parentRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; + + columns?: Column[]; + defaultColumnsIds?: NodesColumnId[]; + requiredColumnsIds?: NodesColumnId[]; + selectedColumnsKey?: string; + groupByParams?: NodesGroupByField[]; } -export function PaginatedNodes(props: PaginatedNodesProps) { - const {uptimeFilter, groupByParam, handleUptimeFilterChange} = useNodesPageQueryParams(); +export function PaginatedNodes({ + path, + database, + parentRef, + additionalNodesProps, + columns = getNodesColumns({database, getNodeRef: additionalNodesProps?.getNodeRef}), + defaultColumnsIds = DEFAULT_NODES_COLUMNS, + requiredColumnsIds = REQUIRED_NODES_COLUMNS, + selectedColumnsKey = NODES_TABLE_SELECTED_COLUMNS_LS_KEY, + groupByParams = ALL_NODES_GROUP_BY_PARAMS, +}: PaginatedNodesProps) { + const {uptimeFilter, groupByParam, handleUptimeFilterChange} = + useNodesPageQueryParams(groupByParams); const {problemFilter, handleProblemFilterChange} = useProblemFilter(); const capabilitiesLoaded = useCapabilitiesLoaded(); @@ -59,29 +87,76 @@ export function PaginatedNodes(props: PaginatedNodesProps) { const renderContent = () => { if (viewerNodesHandlerHasGrouping && groupByParam) { - return ; + return ( + + ); } - return ; + return ( + + ); }; return {renderContent()}; } -function NodesComponent({path, database, parentRef, additionalNodesProps}: PaginatedNodesProps) { - const {searchValue, uptimeFilter} = useNodesPageQueryParams(); +interface PaginatedNodesComponentProps { + path?: string; + database?: string; + parentRef: React.RefObject; + + columns: Column[]; + defaultColumnsIds: NodesColumnId[]; + requiredColumnsIds: NodesColumnId[]; + selectedColumnsKey: string; + groupByParams: NodesGroupByField[]; +} + +function NodesComponent({ + path, + database, + parentRef, + columns, + defaultColumnsIds, + requiredColumnsIds, + selectedColumnsKey, + groupByParams, +}: PaginatedNodesComponentProps) { + const {searchValue, uptimeFilter} = useNodesPageQueryParams(groupByParams); const {problemFilter} = useProblemFilter(); const viewerNodesHandlerHasGrouping = useViewerNodesHandlerHasGrouping(); - const {columnsToShow, columnsToSelect, setColumns} = useNodesSelectedColumns({ - getNodeRef: additionalNodesProps?.getNodeRef, - database, - }); + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + columns, + selectedColumnsKey, + NODES_COLUMNS_TITLES, + defaultColumnsIds, + requiredColumnsIds, + ); const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => { return ( param !== 'SystemState'); + return groupByParams.filter((param) => param !== 'SystemState'); } - return ALL_NODES_GROUP_BY_PARAMS; + return groupByParams; } -export function getNodesGroupByOptions(withSystemStateGroupBy?: boolean): SelectOption[] { - return getAvailableNodesGroupByParams(withSystemStateGroupBy).map((param) => { +export function getNodesGroupByOptions( + groupByParams: NodesGroupByField[], + withSystemStateGroupBy?: boolean, +): SelectOption[] { + return prepareGroupByParams(groupByParams, withSystemStateGroupBy).map((param) => { return { value: param, content: getNodesGroupByFieldTitle(param), @@ -50,9 +56,10 @@ export function getNodesGroupByOptions(withSystemStateGroupBy?: boolean): Select export function parseNodesGroupByParam( paramToParse: unknown, + groupByParams: NodesGroupByField[], withSystemStateGroupBy?: boolean, ): NodesGroupByField | undefined { - const availableParams = getAvailableNodesGroupByParams(withSystemStateGroupBy); + const availableParams = prepareGroupByParams(groupByParams, withSystemStateGroupBy); return availableParams.find((groupByField) => groupByField === paramToParse); } diff --git a/src/containers/Nodes/useNodesPageQueryParams.ts b/src/containers/Nodes/useNodesPageQueryParams.ts index a3f617813..ff824efa6 100644 --- a/src/containers/Nodes/useNodesPageQueryParams.ts +++ b/src/containers/Nodes/useNodesPageQueryParams.ts @@ -1,12 +1,13 @@ import {StringParam, useQueryParams} from 'use-query-params'; import {useViewerNodesHandlerHasGroupingBySystemState} from '../../store/reducers/capabilities/hooks'; +import type {NodesGroupByField} from '../../types/api/nodes'; import type {NodesUptimeFilterValues} from '../../utils/nodes'; import {nodesUptimeFilterValuesSchema} from '../../utils/nodes'; import {parseNodesGroupByParam} from './columns/constants'; -export function useNodesPageQueryParams() { +export function useNodesPageQueryParams(groupByParams: NodesGroupByField[] | undefined) { const [queryParams, setQueryParams] = useQueryParams({ uptimeFilter: StringParam, search: StringParam, @@ -19,6 +20,7 @@ export function useNodesPageQueryParams() { const systemStateGroupingAvailable = useViewerNodesHandlerHasGroupingBySystemState(); const groupByParam = parseNodesGroupByParam( queryParams.nodesGroupBy, + groupByParams ?? [], systemStateGroupingAvailable, ); diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 118aa1cd5..6f54f674a 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -28,7 +28,7 @@ import Describe from './Describe/Describe'; import DetailedOverview from './DetailedOverview/DetailedOverview'; import {getDataBasePages, getPagesByType} from './DiagnosticsPages'; import {HotKeys} from './HotKeys/HotKeys'; -import {Network} from './Network/Network'; +import {NetworkWrapper} from './Network/NetworkWrapper'; import {Partitions} from './Partitions/Partitions'; import {TopQueries} from './TopQueries'; import {TopShards} from './TopShards'; @@ -117,7 +117,14 @@ function Diagnostics(props: DiagnosticsProps) { return ; } case TENANT_DIAGNOSTICS_TABS_IDS.network: { - return ; + return ( + + ); } case TENANT_DIAGNOSTICS_TABS_IDS.describe: { return ; diff --git a/src/containers/Tenant/Diagnostics/Network/NetworkTable/columns.tsx b/src/containers/Tenant/Diagnostics/Network/NetworkTable/columns.tsx new file mode 100644 index 000000000..f98714a93 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Network/NetworkTable/columns.tsx @@ -0,0 +1,43 @@ +import { + getClockSkewColumn, + getConnectionsColumn, + getCpuColumn, + getDataCenterColumn, + getHostColumn, + getNetworkUtilizationColumn, + getNodeIdColumn, + getPingTimeColumn, + getPoolsColumn, + getRackColumn, + getReceiveThroughputColumn, + getSendThroughputColumn, + getUptimeColumn, +} from '../../../../../components/nodesColumns/columns'; +import {isSortableNodesColumn} from '../../../../../components/nodesColumns/constants'; +import type {GetNodesColumnsParams} from '../../../../../components/nodesColumns/types'; +import type {NodesPreparedEntity} from '../../../../../store/reducers/nodes/types'; +import type {Column} from '../../../../../utils/tableUtils/types'; + +export function getNetworkTableNodesColumns( + params: GetNodesColumnsParams, +): Column[] { + const columns = [ + getNodeIdColumn(), + getHostColumn(params, {statusForIcon: 'ConnectStatus'}), + getDataCenterColumn(), + getRackColumn(), + getUptimeColumn(), + getCpuColumn(), + getPoolsColumn(), + getConnectionsColumn(), + getNetworkUtilizationColumn(), + getSendThroughputColumn(), + getReceiveThroughputColumn(), + getPingTimeColumn(), + getClockSkewColumn(), + ]; + + return columns.map((column) => { + return {...column, sortable: isSortableNodesColumn(column.name)}; + }); +} diff --git a/src/containers/Tenant/Diagnostics/Network/NetworkTable/constants.ts b/src/containers/Tenant/Diagnostics/Network/NetworkTable/constants.ts new file mode 100644 index 000000000..e6854dcb7 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Network/NetworkTable/constants.ts @@ -0,0 +1,28 @@ +import type {NodesColumnId} from '../../../../../components/nodesColumns/constants'; +import type {NodesGroupByField} from '../../../../../types/api/nodes'; + +export const NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY = 'networkNodesTableSelectedColumns'; + +export const NETWORK_DEFAULT_NODES_COLUMNS: NodesColumnId[] = [ + 'NodeId', + 'Host', + 'Connections', + 'NetworkUtilization', + 'SendThroughput', + 'ReceiveThroughput', + 'PingTime', + 'ClockSkew', +]; + +export const NETWORK_REQUIRED_NODES_COLUMNS: NodesColumnId[] = ['NodeId']; + +export const NETWORK_NODES_GROUP_BY_PARAMS = [ + 'Host', + 'DC', + 'Rack', + 'Uptime', + 'ConnectStatus', + 'NetworkUtilization', + 'PingTime', + 'ClockSkew', +] as const satisfies NodesGroupByField[]; diff --git a/src/containers/Tenant/Diagnostics/Network/NetworkWrapper.tsx b/src/containers/Tenant/Diagnostics/Network/NetworkWrapper.tsx new file mode 100644 index 000000000..ff1972e8c --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Network/NetworkWrapper.tsx @@ -0,0 +1,61 @@ +import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; +import { + useCapabilitiesLoaded, + useViewerNodesHandlerHasNetworkStats, +} from '../../../../store/reducers/capabilities/hooks'; +import {ENABLE_NETWORK_TABLE_KEY} from '../../../../utils/constants'; +import {useSetting} from '../../../../utils/hooks'; +import type {PaginatedNodesProps} from '../../../Nodes/PaginatedNodes'; +import {PaginatedNodes} from '../../../Nodes/PaginatedNodes'; + +import {Network} from './Network'; +import {getNetworkTableNodesColumns} from './NetworkTable/columns'; +import { + NETWORK_DEFAULT_NODES_COLUMNS, + NETWORK_NODES_GROUP_BY_PARAMS, + NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY, + NETWORK_REQUIRED_NODES_COLUMNS, +} from './NetworkTable/constants'; + +interface NetworkWrapperProps + extends Pick { + database: string; +} + +export function NetworkWrapper({ + database, + path, + parentRef, + additionalNodesProps, +}: NetworkWrapperProps) { + const capabilitiesLoaded = useCapabilitiesLoaded(); + const viewerNodesHasNetworkStats = useViewerNodesHandlerHasNetworkStats(); + const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY); + + const shouldUseNetworkNodesTable = viewerNodesHasNetworkStats && networkTableEnabled; + + const renderContent = () => { + if (shouldUseNetworkNodesTable) { + return ( + + ); + } + + return ; + }; + + return {renderContent()}; +} diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index 0cc194e6e..b7631d990 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -33,6 +33,8 @@ "settings.usePaginatedTables.title": "Use paginated tables", "settings.usePaginatedTables.description": " Use table with data load on scroll for Nodes and Storage tabs. It will increase performance, but could work unstable", + "settings.enableNetworkTable.title": "Enable network table", + "settings.useShowPlanToSvg.title": "Plan to svg", "settings.useShowPlanToSvg.description": " Show \"Plan to svg\" button in query result widow (if query was executed with full stats option).", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 583199b2c..e23e12c92 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -6,6 +6,7 @@ import { AUTOCOMPLETE_ON_ENTER, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, ENABLE_AUTOCOMPLETE, + ENABLE_NETWORK_TABLE_KEY, INVERTED_DISKS_KEY, LANGUAGE_KEY, SHOW_DOMAIN_DATABASE_KEY, @@ -97,6 +98,11 @@ export const usePaginatedTables: SettingProps = { description: i18n('settings.usePaginatedTables.description'), }; +export const enableNetworkTable: SettingProps = { + settingKey: ENABLE_NETWORK_TABLE_KEY, + title: i18n('settings.enableNetworkTable.title'), +}; + export const useShowPlanToSvgTables: SettingProps = { settingKey: USE_SHOW_PLAN_SVG_KEY, title: i18n('settings.useShowPlanToSvg.title'), @@ -145,7 +151,7 @@ export const appearanceSection: SettingsSection = { export const experimentsSection: SettingsSection = { id: 'experimentsSection', title: i18n('section.experiments'), - settings: [usePaginatedTables, useShowPlanToSvgTables], + settings: [usePaginatedTables, enableNetworkTable, useShowPlanToSvgTables], }; export const devSettingsSection: SettingsSection = { id: 'devSettingsSection', diff --git a/src/services/settings.ts b/src/services/settings.ts index 325065072..fcdf6b0ec 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -5,6 +5,7 @@ import { AUTO_REFRESH_INTERVAL, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, ENABLE_AUTOCOMPLETE, + ENABLE_NETWORK_TABLE_KEY, INVERTED_DISKS_KEY, IS_HOTKEYS_HELP_HIDDEN_KEY, LANGUAGE_KEY, @@ -38,6 +39,7 @@ export const DEFAULT_USER_SETTINGS = { [ASIDE_HEADER_COMPACT_KEY]: true, [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], [USE_PAGINATED_TABLES_KEY]: true, + [ENABLE_NETWORK_TABLE_KEY]: false, [USE_SHOW_PLAN_SVG_KEY]: false, [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, [ENABLE_AUTOCOMPLETE]: true, diff --git a/src/store/reducers/capabilities/hooks.ts b/src/store/reducers/capabilities/hooks.ts index f1ed542c4..86288a619 100644 --- a/src/store/reducers/capabilities/hooks.ts +++ b/src/store/reducers/capabilities/hooks.ts @@ -62,6 +62,10 @@ export const useViewerNodesHandlerHasGroupingBySystemState = () => { return useGetFeatureVersion('/viewer/nodes') > 11; }; +export const useViewerNodesHandlerHasNetworkStats = () => { + return useGetFeatureVersion('/viewer/nodes') > 12; +}; + export const useFeatureFlagsAvailable = () => { return useGetFeatureVersion('/viewer/feature_flags') > 1; }; diff --git a/src/store/reducers/nodes/types.ts b/src/store/reducers/nodes/types.ts index efc7de8c3..de73fd410 100644 --- a/src/store/reducers/nodes/types.ts +++ b/src/store/reducers/nodes/types.ts @@ -33,6 +33,20 @@ export interface NodesPreparedEntity extends PreparedNodeSystemState { Endpoints?: TEndpoint[]; TotalSessions?: number; + + Connections?: number; + ConnectStatus?: EFlag; + ReceiveThroughput?: string; + SendThroughput?: string; + NetworkUtilization?: number; + NetworkUtilizationMin?: number; + NetworkUtilizationMax?: number; + ClockSkewUs?: string; + ClockSkewMinUs?: string; + ClockSkewMaxUs?: string; + PingTimeUs?: string; + PingTimeMinUs?: string; + PingTimeMaxUs?: string; } export interface NodesSortParams { diff --git a/src/store/reducers/nodes/utils.ts b/src/store/reducers/nodes/utils.ts index 69c508042..757d8d826 100644 --- a/src/store/reducers/nodes/utils.ts +++ b/src/store/reducers/nodes/utils.ts @@ -8,10 +8,11 @@ export const prepareNodesData = (data: TNodesInfo): NodesHandledResponse => { const rawNodes = data.Nodes || []; const preparedNodes = rawNodes.map((node) => { + const {SystemState, ...restNodeParams} = node; + return { - ...prepareNodeSystemState(node.SystemState), - Tablets: node.Tablets, - NodeId: node.NodeId, + ...restNodeParams, + ...prepareNodeSystemState(SystemState), }; }); diff --git a/src/types/api/nodes.ts b/src/types/api/nodes.ts index 65e07a2d5..ec83e4cf0 100644 --- a/src/types/api/nodes.ts +++ b/src/types/api/nodes.ts @@ -23,14 +23,46 @@ export interface TNodesInfo { export interface TNodeInfo { NodeId: number; + Database?: string; + CpuUsage?: number; DiskSpaceUsage?: number; UptimeSeconds?: number; + Disconnected?: boolean; SystemState: TSystemStateInfo; PDisks?: TPDiskStateInfo[]; VDisks?: TVDiskStateInfo[]; Tablets?: TTabletStateInfo[]; + + // Network stats + Connections?: number; // total number of live connections + ConnectStatus?: EFlag; // Max + /** uint64 */ + ReceiveThroughput?: string; + /** uint64 */ + SendThroughput?: string; + NetworkUtilization?: number; // Sum + NetworkUtilizationMin?: number; + NetworkUtilizationMax?: number; + /** int64 */ + ClockSkewUs?: string; // Avg + /** int64 */ + ClockSkewMinUs?: string; + /** int64 */ + ClockSkewMaxUs?: string; + /** int64 */ + ReverseClockSkewUs?: string; // Avg + /** uint64 */ + PingTimeUs?: string; // Avg + /** uint64 */ + PingTimeMinUs?: string; + /** uint64 */ + PingTimeMaxUs?: string; + /** uint64 */ + ReversePingTimeUs?: string; // Avg + Peers?: TNodeStateInfo[]; + ReversePeers?: TNodeStateInfo[]; } export interface TNodesGroup { @@ -116,6 +148,35 @@ export interface TSystemStateInfo { NodeName?: string; } +interface TNodeStateInfo { + PeerName?: string; + Connected?: boolean; + NodeId?: number; + /** uint64 */ + ChangeTime?: string; + /** uint32 */ + OutputQueueSize?: string; + ConnectStatus?: EFlag; + /** uint64 */ + ConnectTime?: string; + PeerNodeId?: number; + /** int64 */ + ClockSkewUs?: string; // a positive value means the peer is ahead in time; a negative value means it's behind + /** uint32 */ + PingTimeUs?: string; // RTT for the peer + Utilization?: number; // network utilization 0-1 + ScopeId?: unknown; + /** uint32 */ + Count?: string; + /** uint64 */ + BytesWritten?: string; // bytes written to the socket + /** uint64 */ + WriteThroughput?: string; // bytes written per second + SessionState?: ESessionState; +} + +type ESessionState = 'CLOSED' | 'PENDING_CONNECTION' | 'CONNECTED'; + export type PoolName = 'System' | 'User' | 'Batch' | 'IO' | 'IC'; export interface TPoolStats { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3f378d70a..6deaa75d4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -134,6 +134,8 @@ export const TENANT_INITIAL_PAGE_KEY = 'saved_tenant_initial_tab'; // Old key value for backward compatibility export const USE_PAGINATED_TABLES_KEY = 'useBackendParamsForTables'; +export const ENABLE_NETWORK_TABLE_KEY = 'enableNetworkTable'; + export const USE_SHOW_PLAN_SVG_KEY = 'useShowPlanToSvg'; // Setting to hide domain in database list diff --git a/src/utils/dataFormatters/dataFormatters.ts b/src/utils/dataFormatters/dataFormatters.ts index 3d9824f97..24cb335b6 100644 --- a/src/utils/dataFormatters/dataFormatters.ts +++ b/src/utils/dataFormatters/dataFormatters.ts @@ -108,12 +108,17 @@ export const formatNumber = (number?: unknown) => { return configuredNumeral(number).format('0,0.[00000]'); }; -export const formatPercent = (number?: unknown) => { +export const formatPercent = (number?: unknown, precision = 2) => { if (!isNumeric(number)) { return ''; } - const configuredNumber = configuredNumeral(number); - const format = '0%'; + + // Numeral doesn't work well with very low numbers (for example 2e-27) + // We can receive such numbers from backend in float fields + // So we need apply toFixed before configuration + const preparedNumber = Number(number).toFixed(precision); + const configuredNumber = configuredNumeral(preparedNumber); + const format = '0.[00]%'; return configuredNumber.format(format); }; @@ -123,12 +128,14 @@ export const formatSecondsToHours = (seconds: number) => { }; export const roundToPrecision = (value: number | string, precision = 0) => { - let [digits] = String(value).split('.'); - if (Number(value) < 1) { + // Prevent "-" counting as digit in negative values + const valueAbs = Math.abs(Number(value)); + let [digits] = String(valueAbs).split('.'); + if (Number(valueAbs) < 1) { digits = ''; } if (digits.length >= precision) { - return Math.round(Number(value)); + return Number(Number(value).toFixed(0)); } return Number(Number(value).toFixed(precision - digits.length)); }; diff --git a/src/utils/timeParsers/parsers.ts b/src/utils/timeParsers/parsers.ts index 0ce73b868..5c7792488 100644 --- a/src/utils/timeParsers/parsers.ts +++ b/src/utils/timeParsers/parsers.ts @@ -1,4 +1,5 @@ import type {IProtobufTimeObject} from '../../types/api/common'; +import {roundToPrecision} from '../dataFormatters/dataFormatters'; import {isNumeric} from '../utils'; import {parseProtobufDurationToMs, parseProtobufTimestampToMs} from './protobufParsers'; @@ -18,10 +19,10 @@ export const parseTimestampToIdleTime = (value: string | IProtobufTimeObject | u return duration < 0 ? 0 : duration; }; -export const parseUsToMs = (value: string | number | undefined) => { +export const parseUsToMs = (value: string | number | undefined, precision = 0) => { if (!value || !isNumeric(value)) { return 0; } - return Math.round(Number(value) / 1000); + return roundToPrecision(Number(value) / 1000, precision); };