import DataTable from '@gravity-ui/react-data-table'; import {DefinitionList, Tooltip} from '@gravity-ui/uikit'; import type {PreparedStorageNode} from '../../store/reducers/storage/types'; import type {TEndpoint, TMemoryStats, TPoolStats} from '../../types/api/nodes'; import type {TTabletStateInfo} from '../../types/api/tablet'; 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 {getUsageSeverity} from '../../utils/generateEvaluator'; import {getNodeMemory} from '../../utils/memory'; import type {Column} from '../../utils/tableUtils/types'; import {formatToMs, parseUsToMs} from '../../utils/timeParsers'; import {bytesToSpeed, isNumeric} from '../../utils/utils'; import {CellWithPopover} from '../CellWithPopover/CellWithPopover'; import {MemoryViewer} from '../MemoryViewer/MemoryViewer'; import {NodeHostWrapper} from '../NodeHostWrapper/NodeHostWrapper'; import {PoolsGraph} from '../PoolsGraph/PoolsGraph'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {TabletsStatistic} from '../TabletsStatistic'; import {formatPool} from '../TooltipsContent'; import {NodeUptime} from '../UptimeViewer/UptimeViewer'; 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'; const b = cn('ydb-nodes-columns'); export function getNodeIdColumn(): Column { return { name: NODES_COLUMNS_IDS.NodeId, header: '#', width: 80, resizeMinWidth: 80, render: ({row}) => row.NodeId, align: DataTable.RIGHT, }; } export function getHostColumn({ database, }: GetNodesColumnsParams): Column { return { name: NODES_COLUMNS_IDS.Host, header: NODES_COLUMNS_TITLES.Host, render: ({row}) => { return ; }, width: 350, align: DataTable.LEFT, }; } export function getICPortColumn(): Column { return { name: NODES_COLUMNS_IDS.ICPort, header: NODES_COLUMNS_TITLES.ICPort, render: ({row}) => row.Endpoints?.find(({Name}) => Name === 'ic')?.Address?.replace(/^:/, '') || EMPTY_DATA_PLACEHOLDER, width: 100, align: DataTable.RIGHT, }; } // Different column for different set of required fields // ConnectStatus is required here, it makes handler to return network stats // On versions before 25-1-2 ConnectStatus also makes handler to return peers - it can significantly increase response size export function getNetworkHostColumn({ database, }: GetNodesColumnsParams): Column { return { name: NODES_COLUMNS_IDS.NetworkHost, header: NODES_COLUMNS_TITLES.NetworkHost, render: ({row}) => { return ( ); }, width: 350, align: DataTable.LEFT, }; } export function getNodeNameColumn(): Column { return { name: NODES_COLUMNS_IDS.NodeName, header: NODES_COLUMNS_TITLES.NodeName, align: DataTable.LEFT, render: ({row}) => row.NodeName || EMPTY_DATA_PLACEHOLDER, width: 200, }; } export function getDatabaseColumn(): Column { return { name: NODES_COLUMNS_IDS.Database, header: NODES_COLUMNS_TITLES.Database, align: DataTable.LEFT, render: ({row}) => row.Database ? ( {row.Database} ) : ( EMPTY_DATA_PLACEHOLDER ), width: 200, }; } export function getDataCenterColumn(): Column { return { name: NODES_COLUMNS_IDS.DC, header: NODES_COLUMNS_TITLES.DC, align: DataTable.LEFT, render: ({row}) => row.DC || EMPTY_DATA_PLACEHOLDER, width: 60, }; } export function getRackColumn(): Column { return { name: NODES_COLUMNS_IDS.Rack, header: NODES_COLUMNS_TITLES.Rack, align: DataTable.LEFT, render: ({row}) => row.Rack || EMPTY_DATA_PLACEHOLDER, width: 100, }; } export function getPileNameColumn(): Column { return { name: NODES_COLUMNS_IDS.PileName, header: i18n('field_pile-name'), align: DataTable.LEFT, render: ({row}) => row.PileName || EMPTY_DATA_PLACEHOLDER, width: 100, }; } export function getVersionColumn(): Column { return { name: NODES_COLUMNS_IDS.Version, header: NODES_COLUMNS_TITLES.Version, width: 200, align: DataTable.LEFT, render: ({row}) => { return {row.Version}; }, }; } export function getUptimeColumn< T extends {StartTime?: string; DisconnectTime?: string}, >(): Column { return { name: NODES_COLUMNS_IDS.Uptime, header: NODES_COLUMNS_TITLES.Uptime, sortAccessor: ({StartTime}) => (StartTime ? -StartTime : 0), render: ({row}) => { return ; }, align: DataTable.RIGHT, width: 120, }; } export function getRAMColumn< T extends {MemoryStats?: TMemoryStats; MemoryUsed?: string; MemoryLimit?: string}, >(): Column { return { name: NODES_COLUMNS_IDS.RAM, header: NODES_COLUMNS_TITLES.RAM, sortAccessor: (row) => getNodeMemory(row).memoryUsed ?? 0, defaultOrder: DataTable.DESCENDING, render: ({row}) => { const {memoryUsed: used, memoryLimit: limit} = getNodeMemory(row); const [memoryUsed, memoryLimit] = formatStorageValues( used, limit, 'gb', undefined, true, ); const hasData = memoryUsed || memoryLimit; const memoryContent = used === undefined ? EMPTY_DATA_PLACEHOLDER : memoryUsed; return ( {memoryUsed && ( {memoryUsed} )} {memoryLimit && ( {memoryLimit} )} } > {used !== undefined && limit !== undefined ? ( formatStorageValues(value, total, 'gb', undefined, true) } className={b('column-ram')} colorizeProgress hideCapacity /> ) : ( memoryContent )} ); }, align: DataTable.LEFT, width: 80, resizeMinWidth: 40, }; } export function getMemoryColumn< T extends {MemoryStats?: TMemoryStats; MemoryUsed?: string; MemoryLimit?: string}, >(): Column { return { name: NODES_COLUMNS_IDS.Memory, header: NODES_COLUMNS_TITLES.Memory, defaultOrder: DataTable.DESCENDING, render: ({row}) => { if (row.MemoryStats) { return ( ); } const {memoryUsed, memoryLimit} = getNodeMemory(row); if (memoryUsed === undefined) { return EMPTY_DATA_PLACEHOLDER; } if (memoryLimit === undefined) { return formatStorageValuesToGb(memoryUsed)[0]; } return ( ); }, align: DataTable.LEFT, width: 300, resizeMinWidth: 170, }; } export function getPoolsColumn(): Column { return { name: NODES_COLUMNS_IDS.Pools, header: NODES_COLUMNS_TITLES.Pools, sortAccessor: ({PoolStats = []}) => Math.max(...PoolStats.map(({Usage}) => Number(Usage))), defaultOrder: DataTable.DESCENDING, render: ({row}) => row.PoolStats ? : EMPTY_DATA_PLACEHOLDER, align: DataTable.LEFT, width: 80, resizeMinWidth: 60, }; } export function getCpuColumn< T extends {PoolStats?: TPoolStats[]; CoresUsed?: number; CoresTotal?: number}, >(): Column { return { name: NODES_COLUMNS_IDS.CPU, header: NODES_COLUMNS_TITLES.CPU, sortAccessor: ({PoolStats = []}) => Math.max(...PoolStats.map(({Usage}) => Number(Usage))), defaultOrder: DataTable.DESCENDING, render: ({row}) => { let totalPoolUsage = isNumeric(row.CoresUsed) && isNumeric(row.CoresTotal) ? row.CoresUsed / row.CoresTotal : undefined; if (totalPoolUsage === undefined && row.PoolStats) { let totalThreadsCount = 0; totalPoolUsage = row.PoolStats.reduce((acc, pool) => { totalThreadsCount += Number(pool.Threads); return acc + Number(pool.Usage) * Number(pool.Threads); }, 0); totalPoolUsage = totalPoolUsage / totalThreadsCount; } return ( {row.PoolStats?.map((pool) => isNumeric(pool.Usage) ? ( {formatPool('Usage', pool.Usage).value} ) : null, )} } > {isNumeric(totalPoolUsage) ? ( ) : ( EMPTY_DATA_PLACEHOLDER )} ); }, align: DataTable.LEFT, width: 80, resizeMinWidth: 40, }; } export function getLoadAverageColumn(): Column { return { name: NODES_COLUMNS_IDS.LoadAverage, header: NODES_COLUMNS_TITLES.LoadAverage, sortAccessor: ({LoadAveragePercents = []}) => LoadAveragePercents[0], defaultOrder: DataTable.DESCENDING, render: ({row}) => { const value = row.LoadAveragePercents && row.LoadAveragePercents.length > 0 ? row.LoadAveragePercents[0] : undefined; if (!isNumeric(value)) { return EMPTY_DATA_PLACEHOLDER; } return ( ); }, align: DataTable.LEFT, width: 170, resizeMinWidth: 170, }; } // The same as loadAverage, but more compact export function getLoadColumn(): Column { return { name: NODES_COLUMNS_IDS.Load, header: NODES_COLUMNS_TITLES.Load, sortAccessor: ({LoadAveragePercents = []}) => LoadAveragePercents[0], defaultOrder: DataTable.DESCENDING, render: ({row}) => row.LoadAveragePercents && row.LoadAveragePercents.length > 0 ? ( ) : ( EMPTY_DATA_PLACEHOLDER ), align: DataTable.LEFT, width: 80, resizeMinWidth: 70, }; } export function getDiskSpaceUsageColumn(): Column { return { name: NODES_COLUMNS_IDS.DiskSpaceUsage, header: NODES_COLUMNS_TITLES.DiskSpaceUsage, render: ({row}) => { return valueIsDefined(row.DiskSpaceUsage) ? ( ) : ( EMPTY_DATA_PLACEHOLDER ); }, align: DataTable.LEFT, width: 115, resizeMinWidth: 75, }; } export function getSessionsColumn(): Column { return { name: NODES_COLUMNS_IDS.TotalSessions, header: NODES_COLUMNS_TITLES.TotalSessions, render: ({row}) => row.TotalSessions ?? EMPTY_DATA_PLACEHOLDER, align: DataTable.RIGHT, width: 100, }; } export function getTabletsColumn< T extends { TenantName?: string; NodeId: string | number; Tablets?: TTabletStateInfo[]; }, >({database}: GetNodesColumnsParams): Column { return { name: NODES_COLUMNS_IDS.Tablets, header: NODES_COLUMNS_TITLES.Tablets, width: 500, resizeMinWidth: 500, render: ({row}) => { return row.Tablets ? ( ) : ( EMPTY_DATA_PLACEHOLDER ); }, align: DataTable.LEFT, sortable: false, }; } export function getMissingDisksColumn(): Column { return { name: NODES_COLUMNS_IDS.Missing, header: NODES_COLUMNS_TITLES.Missing, render: ({row}) => row.Missing, align: DataTable.CENTER, defaultOrder: DataTable.DESCENDING, width: 100, }; } // 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, 1) : 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, 1) : 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, }; } // Peers columns export function getPeerSkewColumn(): Column { return { name: NODES_COLUMNS_IDS.ClockSkew, header: NODES_COLUMNS_TITLES.ClockSkew, align: DataTable.RIGHT, width: 110, resizeMinWidth: 90, render: ({row}) => isNumeric(row.ClockSkewUs) ? formatToMs(parseUsToMs(row.ClockSkewUs, 1)) : EMPTY_DATA_PLACEHOLDER, }; } export function getPeerPingColumn(): Column { return { name: NODES_COLUMNS_IDS.PingTime, header: NODES_COLUMNS_TITLES.PingTime, align: DataTable.RIGHT, width: 110, resizeMinWidth: 90, render: ({row}) => isNumeric(row.PingTimeUs) ? formatToMs(parseUsToMs(row.PingTimeUs)) : EMPTY_DATA_PLACEHOLDER, }; }