From e5d0823d7869d547f9c92d3681e8abc5c70000b9 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sat, 2 Nov 2024 11:10:33 +0300 Subject: [PATCH] feat: implement simple and narrow vertical progress bar (#1560) --- .../CellWithPopover/CellWithPopover.scss | 4 + .../CellWithPopover/CellWithPopover.tsx | 6 +- .../ProgressViewer/ProgressViewer.tsx | 4 +- .../PoolTooltipContent/PoolTooltipContent.tsx | 2 +- src/components/nodesColumns/NodesColumns.scss | 6 + src/components/nodesColumns/columns.tsx | 130 +++++++++++++++++- src/components/nodesColumns/constants.ts | 10 ++ src/components/nodesColumns/i18n/en.json | 6 +- src/containers/Nodes/columns/columns.tsx | 4 + src/containers/Nodes/columns/constants.ts | 2 +- src/containers/Nodes/getNodes.ts | 3 +- .../Storage/StorageNodes/columns/columns.tsx | 4 + .../Storage/StorageNodes/columns/constants.ts | 2 +- .../Storage/StorageNodes/getNodes.ts | 8 +- .../TenantCpu/TopNodesByCpu.tsx | 4 +- src/types/api/nodes.ts | 6 +- src/utils/nodes.ts | 21 +++ tests/suites/nodes/nodes.test.ts | 2 +- 18 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 src/components/nodesColumns/NodesColumns.scss diff --git a/src/components/CellWithPopover/CellWithPopover.scss b/src/components/CellWithPopover/CellWithPopover.scss index 1d12c3ec4..6c0c428a7 100644 --- a/src/components/CellWithPopover/CellWithPopover.scss +++ b/src/components/CellWithPopover/CellWithPopover.scss @@ -16,5 +16,9 @@ .g-popover__handler { display: inline; } + + &_full-width { + width: 100%; + } } } diff --git a/src/components/CellWithPopover/CellWithPopover.tsx b/src/components/CellWithPopover/CellWithPopover.tsx index 14529d8b4..a2daa0a57 100644 --- a/src/components/CellWithPopover/CellWithPopover.tsx +++ b/src/components/CellWithPopover/CellWithPopover.tsx @@ -9,6 +9,7 @@ const b = cn('ydb-cell-with-popover'); interface CellWithPopoverProps extends PopoverProps { wrapperClassName?: string; + fullWidth?: boolean; } const DELAY_TIMEOUT = 100; @@ -17,14 +18,15 @@ export function CellWithPopover({ children, className, wrapperClassName, + fullWidth, ...props }: CellWithPopoverProps) { return ( -
+
{children} diff --git a/src/components/ProgressViewer/ProgressViewer.tsx b/src/components/ProgressViewer/ProgressViewer.tsx index 6f33d6492..2176de3ca 100644 --- a/src/components/ProgressViewer/ProgressViewer.tsx +++ b/src/components/ProgressViewer/ProgressViewer.tsx @@ -48,6 +48,7 @@ export interface ProgressViewerProps { inverseColorize?: boolean; warningThreshold?: number; dangerThreshold?: number; + hideCapacity?: boolean; } export function ProgressViewer({ @@ -61,6 +62,7 @@ export function ProgressViewer({ inverseColorize, warningThreshold = 60, dangerThreshold = 80, + hideCapacity, }: ProgressViewerProps) { const theme = useTheme(); @@ -94,7 +96,7 @@ export function ProgressViewer({ }; const renderContent = () => { - if (isNumeric(capacity)) { + if (isNumeric(capacity) && !hideCapacity) { return `${valueText} ${divider} ${capacityText}`; } diff --git a/src/components/TooltipsContent/PoolTooltipContent/PoolTooltipContent.tsx b/src/components/TooltipsContent/PoolTooltipContent/PoolTooltipContent.tsx index a25aa25aa..c0973c4dd 100644 --- a/src/components/TooltipsContent/PoolTooltipContent/PoolTooltipContent.tsx +++ b/src/components/TooltipsContent/PoolTooltipContent/PoolTooltipContent.tsx @@ -1,7 +1,7 @@ import type {TPoolStats} from '../../../types/api/nodes'; import {InfoViewer, createInfoFormatter, formatObject} from '../../InfoViewer'; -const formatPool = createInfoFormatter({ +export const formatPool = createInfoFormatter({ values: { Usage: (value) => value && `${(Number(value) * 100).toFixed(2)} %`, }, diff --git a/src/components/nodesColumns/NodesColumns.scss b/src/components/nodesColumns/NodesColumns.scss new file mode 100644 index 000000000..b342bcc11 --- /dev/null +++ b/src/components/nodesColumns/NodesColumns.scss @@ -0,0 +1,6 @@ +.ydb-nodes-columns { + &__column-ram, + &__column-cpu { + min-width: 40px; + } +} diff --git a/src/components/nodesColumns/columns.tsx b/src/components/nodesColumns/columns.tsx index f726c7b86..e5a09a780 100644 --- a/src/components/nodesColumns/columns.tsx +++ b/src/components/nodesColumns/columns.tsx @@ -1,24 +1,36 @@ import DataTable from '@gravity-ui/react-data-table'; +import {DefinitionList} from '@gravity-ui/uikit'; import {getLoadSeverityForNode} from '../../store/reducers/nodes/utils'; import type {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 {formatStorageValuesToGb} from '../../utils/dataFormatters/dataFormatters'; +import { + formatStorageValues, + formatStorageValuesToGb, +} from '../../utils/dataFormatters/dataFormatters'; import {getSpaceUsageSeverity} from '../../utils/storage'; import type {Column} from '../../utils/tableUtils/types'; +import {isNumeric} from '../../utils/utils'; import {CellWithPopover} from '../CellWithPopover/CellWithPopover'; import {NodeHostWrapper} from '../NodeHostWrapper/NodeHostWrapper'; import type {NodeHostData} from '../NodeHostWrapper/NodeHostWrapper'; import {PoolsGraph} from '../PoolsGraph/PoolsGraph'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {TabletsStatistic} from '../TabletsStatistic'; +import {formatPool} from '../TooltipsContent'; import {UsageLabel} from '../UsageLabel/UsageLabel'; import {NODES_COLUMNS_IDS, NODES_COLUMNS_TITLES} from './constants'; +import i18n from './i18n'; import type {GetNodesColumnsParams} from './types'; +import './NodesColumns.scss'; + +const b = cn('ydb-nodes-columns'); + export function getNodeIdColumn(): Column { return { name: NODES_COLUMNS_IDS.NodeId, @@ -111,6 +123,57 @@ export function getMemoryColumn< resizeMinWidth: 170, }; } + +export function getRAMColumn(): Column { + return { + name: NODES_COLUMNS_IDS.RAM, + header: NODES_COLUMNS_TITLES.RAM, + sortAccessor: ({MemoryUsed = 0}) => Number(MemoryUsed), + defaultOrder: DataTable.DESCENDING, + render: ({row}) => { + const [memoryUsed, memoryLimit] = + isNumeric(row.MemoryUsed) && isNumeric(row.MemoryLimit) + ? formatStorageValues( + Number(row.MemoryUsed), + Number(row.MemoryLimit), + 'gb', + undefined, + true, + ) + : [0, 0]; + return ( + + + {memoryUsed} + + + {memoryLimit} + + + } + > + + formatStorageValues(value, total, 'gb', undefined, true) + } + className={b('column-ram')} + colorizeProgress + hideCapacity + /> + + ); + }, + align: DataTable.LEFT, + width: 80, + resizeMinWidth: 40, + }; +} export function getSharedCacheUsageColumn< T extends {SharedCacheUsed?: string | number; SharedCacheLimit?: string | number}, >(): Column { @@ -130,10 +193,10 @@ export function getSharedCacheUsageColumn< resizeMinWidth: 170, }; } -export function getCpuColumn(): Column { +export function getPoolsColumn(): Column { return { - name: NODES_COLUMNS_IDS.CPU, - header: NODES_COLUMNS_TITLES.CPU, + name: NODES_COLUMNS_IDS.Pools, + header: NODES_COLUMNS_TITLES.Pools, sortAccessor: ({PoolStats = []}) => Math.max(...PoolStats.map(({Usage}) => Number(Usage))), defaultOrder: DataTable.DESCENDING, render: ({row}) => @@ -143,6 +206,65 @@ export function getCpuColumn(): Column 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}) => { + if (!row.PoolStats) { + return EMPTY_DATA_PLACEHOLDER; + } + + let totalPoolUsage = + isNumeric(row.CoresUsed) && isNumeric(row.CoresTotal) + ? row.CoresUsed / row.CoresTotal + : undefined; + + if (totalPoolUsage === undefined) { + 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, + )} + + } + > + + + ); + }, + align: DataTable.LEFT, + width: 80, + resizeMinWidth: 40, + }; +} export function getLoadAverageColumn(): Column { return { name: NODES_COLUMNS_IDS.LoadAverage, diff --git a/src/components/nodesColumns/constants.ts b/src/components/nodesColumns/constants.ts index 97813b90f..c3a53b990 100644 --- a/src/components/nodesColumns/constants.ts +++ b/src/components/nodesColumns/constants.ts @@ -14,7 +14,9 @@ export const NODES_COLUMNS_IDS = { Version: 'Version', Uptime: 'Uptime', Memory: 'Memory', + RAM: 'RAM', CPU: 'CPU', + Pools: 'Pools', LoadAverage: 'LoadAverage', Load: 'Load', DiskSpaceUsage: 'DiskSpaceUsage', @@ -54,6 +56,12 @@ export const NODES_COLUMNS_TITLES = { get Memory() { return i18n('memory'); }, + get RAM() { + return i18n('ram'); + }, + get Pools() { + return i18n('pools'); + }, get CPU() { return i18n('cpu'); }, @@ -94,6 +102,8 @@ export const NODES_COLUMNS_TO_DATA_FIELDS: Record(), getUptimeColumn(), getMemoryColumn(), + getRAMColumn(), + getPoolsColumn(), getCpuColumn(), getLoadAverageColumn(), getTabletsColumn(params), diff --git a/src/containers/Nodes/columns/constants.ts b/src/containers/Nodes/columns/constants.ts index 5ebff16cc..c0c701cf6 100644 --- a/src/containers/Nodes/columns/constants.ts +++ b/src/containers/Nodes/columns/constants.ts @@ -10,7 +10,7 @@ export const DEFAULT_NODES_COLUMNS: NodesColumnId[] = [ 'Version', 'Uptime', 'Memory', - 'CPU', + 'Pools', 'LoadAverage', 'Tablets', ]; diff --git a/src/containers/Nodes/getNodes.ts b/src/containers/Nodes/getNodes.ts index 7fd7a11ef..20d7458f2 100644 --- a/src/containers/Nodes/getNodes.ts +++ b/src/containers/Nodes/getNodes.ts @@ -5,6 +5,7 @@ import {prepareNodesData} from '../../store/reducers/nodes/utils'; import type {NodesRequestParams} from '../../types/api/nodes'; import {prepareSortValue} from '../../utils/filters'; import { + NODES_SORT_VALUE_TO_FIELD, getProblemParamValue, getUptimeParamValue, isSortableNodesProperty, @@ -35,7 +36,7 @@ export const getNodes: FetchData< const {path, database, searchValue, problemFilter, uptimeFilter} = filters ?? {}; const sort = isSortableNodesProperty(columnId) - ? prepareSortValue(columnId, sortOrder) + ? prepareSortValue(NODES_SORT_VALUE_TO_FIELD[columnId], sortOrder) : undefined; const dataFieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); diff --git a/src/containers/Storage/StorageNodes/columns/columns.tsx b/src/containers/Storage/StorageNodes/columns/columns.tsx index 5f72e4123..3a15a4265 100644 --- a/src/containers/Storage/StorageNodes/columns/columns.tsx +++ b/src/containers/Storage/StorageNodes/columns/columns.tsx @@ -9,6 +9,8 @@ import { getMissingDisksColumn, getNodeIdColumn, getNodeNameColumn, + getPoolsColumn, + getRAMColumn, getRackColumn, getUptimeColumn, getVersionColumn, @@ -72,6 +74,8 @@ export const getStorageNodesColumns = ({ getRackColumn(), getVersionColumn(), getMemoryColumn(), + getRAMColumn(), + getPoolsColumn(), getCpuColumn(), getDiskSpaceUsageColumn(), getUptimeColumn(), diff --git a/src/containers/Storage/StorageNodes/columns/constants.ts b/src/containers/Storage/StorageNodes/columns/constants.ts index 30e81ca73..18b792053 100644 --- a/src/containers/Storage/StorageNodes/columns/constants.ts +++ b/src/containers/Storage/StorageNodes/columns/constants.ts @@ -13,7 +13,7 @@ export const DEFAULT_STORAGE_NODES_COLUMNS: NodesColumnId[] = [ 'Host', 'DC', 'Rack', - 'CPU', + 'Pools', 'Uptime', 'PDisks', ]; diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index 86ca308b5..53b897807 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -7,7 +7,11 @@ import type { import {prepareStorageNodesResponse} from '../../../store/reducers/storage/utils'; import type {NodesRequestParams} from '../../../types/api/nodes'; import {prepareSortValue} from '../../../utils/filters'; -import {getUptimeParamValue, isSortableNodesProperty} from '../../../utils/nodes'; +import { + NODES_SORT_VALUE_TO_FIELD, + getUptimeParamValue, + isSortableNodesProperty, +} from '../../../utils/nodes'; import {getRequiredDataFields} from '../../../utils/tableUtils/getRequiredDataFields'; export const getStorageNodes: FetchData< @@ -37,7 +41,7 @@ export const getStorageNodes: FetchData< const {sortOrder, columnId} = sortParams ?? {}; const sort = isSortableNodesProperty(columnId) - ? prepareSortValue(columnId, sortOrder) + ? prepareSortValue(NODES_SORT_VALUE_TO_FIELD[columnId], sortOrder) : undefined; const dataFieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx index 00b764624..079a2a303 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx @@ -1,9 +1,9 @@ import type {Column} from '@gravity-ui/react-data-table'; import { - getCpuColumn, getHostColumn, getNodeIdColumn, + getPoolsColumn, } from '../../../../../components/nodesColumns/columns'; import { NODES_COLUMNS_TO_DATA_FIELDS, @@ -29,7 +29,7 @@ export function getTopNodesByCpuColumns( const hostColumn = {...getHostColumn(params), width: undefined}; const columns = [ - getCpuColumn(), + getPoolsColumn(), getNodeIdColumn(), hostColumn, ]; diff --git a/src/types/api/nodes.ts b/src/types/api/nodes.ts index 8a3e63a45..be4b5e287 100644 --- a/src/types/api/nodes.ts +++ b/src/types/api/nodes.ts @@ -77,6 +77,8 @@ export interface TSystemStateInfo { /** double */ MaxDiskUsage?: number; Location?: TNodeLocation; + CoresUsed?: number; + CoresTotal?: number; /** * int64 @@ -193,7 +195,9 @@ export type NodesSortValue = | 'Memory' | `Missing` | `DiskSpaceUsage` - | `Database`; + | `Database` + | 'Pools' + | 'RAM'; export type NodesSort = BackendSortParam; diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts index 7ee61283f..f7d63ad86 100644 --- a/src/utils/nodes.ts +++ b/src/utils/nodes.ts @@ -115,7 +115,28 @@ export const NODES_SORT_VALUES: NodesSortValue[] = [ `Missing`, `DiskSpaceUsage`, `Database`, + 'Pools', + 'RAM', ]; +// Maps column names to actual fields on backend for sorting. +export const NODES_SORT_VALUE_TO_FIELD: Record = { + NodeId: 'NodeId', + Host: 'Host', + NodeName: 'NodeName', + DC: 'DC', + Rack: 'Rack', + Version: 'Version', + Uptime: 'Uptime', + CPU: 'CPU', + LoadAverage: 'LoadAverage', + Memory: 'Memory', + Missing: 'Missing', + DiskSpaceUsage: 'DiskSpaceUsage', + Database: 'Database', + Pools: 'CPU', + RAM: 'Memory', +}; + export const isSortableNodesProperty = (value: unknown): value is NodesSortValue => NODES_SORT_VALUES.includes(value as NodesSortValue); diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index fd895e466..d4de19d82 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -127,7 +127,7 @@ test.describe('Test Nodes Paginated Table', async () => { expect(rowData).toHaveProperty('Host'); expect(rowData).toHaveProperty('Uptime'); expect(rowData).toHaveProperty('Memory'); - expect(rowData).toHaveProperty('CPU'); + expect(rowData).toHaveProperty('Pools'); }); test('Column values can be retrieved correctly', async ({page}) => {