From 38969b6b72a088be0bdf7a1241c6a6e8f83ea13f Mon Sep 17 00:00:00 2001 From: astandrik Date: Mon, 16 Dec 2024 18:34:24 +0300 Subject: [PATCH 1/7] feat: vdisks in 2 rows --- src/containers/Storage/PDisk/PDisk.scss | 19 ++- .../columns/StorageNodesColumns.scss | 17 +- .../Storage/StorageNodes/getNodes.ts | 42 +++-- src/mocks/storage/nodes.ts | 158 ++++++++++++++++++ 4 files changed, 202 insertions(+), 34 deletions(-) create mode 100644 src/mocks/storage/nodes.ts diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index b5654b1d70..4cd9dccc56 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -1,28 +1,35 @@ .pdisk-storage { position: relative; - width: 120px; + display: flex; + flex-direction: column; + + min-width: 120px; &__content { position: relative; display: block; + flex: 1; - border-radius: 4px; // to match interactive area with disk shape + border-radius: 4px; } &__vdisks { display: flex; - // this breaks disks relative sizes, but disks rarely exceed one line - flex-wrap: wrap; + overflow: hidden; // Prevent overflow + flex: 0 0 auto; gap: 2px; margin-bottom: 4px; + + white-space: nowrap; } &__vdisks-item { - flex-basis: 3px; - flex-shrink: 0; + flex: 0 0 3px; + + min-width: 3px; .stack__layer { .data-table__row:hover & { diff --git a/src/containers/Storage/StorageNodes/columns/StorageNodesColumns.scss b/src/containers/Storage/StorageNodes/columns/StorageNodesColumns.scss index 8171ffa363..96e3f493bc 100644 --- a/src/containers/Storage/StorageNodes/columns/StorageNodesColumns.scss +++ b/src/containers/Storage/StorageNodes/columns/StorageNodesColumns.scss @@ -5,20 +5,13 @@ &__pdisks-wrapper { display: flex; - justify-content: left; - align-items: flex-end; + gap: 10px; - width: max-content; - height: 40px; + width: 100%; } - &__pdisks-item { - flex-grow: 1; - - max-width: 200px; - margin-right: 10px; - &:last-child { - margin-right: 0px; - } + &__pdisks-item { + display: flex; + flex-shrink: 0; } } diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index be9a0714e0..dedf1f3880 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -3,6 +3,7 @@ import { NODES_COLUMNS_TO_DATA_FIELDS, getNodesColumnSortField, } from '../../../components/nodesColumns/constants'; +import {generateNodes} from '../../../mocks/storage/nodes'; import type { PreparedStorageNode, PreparedStorageNodeFilters, @@ -44,22 +45,31 @@ export const getStorageNodes: FetchData< const dataFieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); - const response = await window.api.viewer.getNodes({ - type, - storage, - limit, - offset, - sort, - filter: searchValue, - uptime: getUptimeParamValue(nodesUptimeFilter), - with: visibleEntities, - database, - node_id: nodeId, - group_id: groupId, - filter_group: filterGroup, - filter_group_by: filterGroupBy, - fieldsRequired: dataFieldsRequired, - }); + let response; + if (process.env.NODE_ENV === 'development') { + // Get mock configuration from URL parameters or use defaults + const urlParams = new URLSearchParams(window.location.search); + const pdisks = parseInt(urlParams.get('pdisks') || '10', 10); + const vdisksPerPDisk = parseInt(urlParams.get('vdisksPerPDisk') || '2', 10); + response = generateNodes(5, {vdisksCount: pdisks * vdisksPerPDisk, pdisksCount: pdisks}); + } else { + response = await window.api.viewer.getNodes({ + type, + storage, + limit, + offset, + sort, + filter: searchValue, + uptime: getUptimeParamValue(nodesUptimeFilter), + with: visibleEntities, + database, + node_id: nodeId, + group_id: groupId, + filter_group: filterGroup, + filter_group_by: filterGroupBy, + fieldsRequired: dataFieldsRequired, + }); + } const preparedResponse = prepareStorageNodesResponse(response); return { data: preparedResponse.nodes || [], diff --git a/src/mocks/storage/nodes.ts b/src/mocks/storage/nodes.ts new file mode 100644 index 0000000000..14fb9f7682 --- /dev/null +++ b/src/mocks/storage/nodes.ts @@ -0,0 +1,158 @@ +import {EFlag} from '../../types/api/enums'; +import type { + TEndpoint, + TNodeInfo, + TNodesInfo, + TPoolStats, + TSystemStateInfo, +} from '../../types/api/nodes'; +import {TPDiskState} from '../../types/api/pdisk'; +import {EVDiskState} from '../../types/api/vdisk'; + +// Different disk sizes to simulate variety (in bytes) +const DISK_SIZES = [ + '68719476736', // 64 GB + '137438953472', // 128 GB + '274877906944', // 256 GB + '549755813888', // 512 GB + '1099511627776', // 1 TB +]; + +const getRandomDiskSize = () => DISK_SIZES[Math.floor(Math.random() * DISK_SIZES.length)]; + +const generatePoolStats = (count = 5): TPoolStats[] => { + const poolNames = ['System', 'User', 'Batch', 'IO', 'IC'] as const; + return poolNames.slice(0, count).map((Name) => ({ + Name, + Usage: Math.random() * 0.02, + Threads: Math.floor(Math.random() * 3) + 1, + })); +}; + +const generateEndpoints = (): TEndpoint[] => [ + {Name: 'ic', Address: ':19001'}, + {Name: 'http-mon', Address: ':8765'}, + {Name: 'grpcs', Address: ':2135'}, + {Name: 'grpc', Address: ':2136'}, +]; + +const generateSystemState = (nodeId: number): TSystemStateInfo => ({ + StartTime: '1734358137851', + ChangeTime: '1734358421375', + LoadAverage: [3.381347656, 2.489257813, 1.279296875], + NumberOfCpus: 8, + SystemState: EFlag.Green, + NodeId: nodeId, + Host: `localhost-${nodeId}`, + Version: 'main.95ce0df', + PoolStats: generatePoolStats(), + Endpoints: generateEndpoints(), + Roles: ['Bootstrapper', 'StateStorage', 'StateStorageBoard', 'SchemeBoard', 'Storage'], + MemoryLimit: '2147483648', + MaxDiskUsage: 0.002349853516, + Location: { + DataCenter: '1', + Rack: '1', + Unit: '1', + }, + TotalSessions: 0, + CoresUsed: 0.07583969556, + CoresTotal: 8, +}); + +const generatePDisk = (nodeId: number, pdiskId: number, totalSize = '68719476736') => ({ + PDiskId: pdiskId, + ChangeTime: '1734358142074', + Path: `/ydb_data/pdisk${pdiskId}l3ki78no.data`, + Guid: pdiskId.toString(), + Category: '0', + TotalSize: totalSize, + AvailableSize: (Number(totalSize) * 0.9).toString(), // 90% available by default + State: TPDiskState.Normal, + NodeId: nodeId, + Device: EFlag.Green, + Realtime: EFlag.Green, + SerialNumber: '', + SystemSize: '213909504', + LogUsedSize: '35651584', + LogTotalSize: '68486692864', + EnforcedDynamicSlotSize: '22817013760', +}); + +const generateVDisk = (nodeId: number, vdiskId: number, pdiskId: number) => ({ + VDiskId: { + GroupID: vdiskId, + GroupGeneration: 1, + Ring: 0, + Domain: 0, + VDisk: 0, + }, + ChangeTime: '1734358420919', + PDiskId: pdiskId, + VDiskSlotId: vdiskId, + Guid: '1', + Kind: '0', + NodeId: nodeId, + VDiskState: EVDiskState.OK, + DiskSpace: EFlag.Green, + SatisfactionRank: { + FreshRank: { + Flag: EFlag.Green, + }, + LevelRank: { + Flag: EFlag.Green, + }, + }, + Replicated: true, + ReplicationProgress: 1, + ReplicationSecondsRemaining: 0, + AllocatedSize: '0', + AvailableSize: '22817013760', + HasUnreadableBlobs: false, + IncarnationGuid: '11528832187803248876', + InstanceGuid: '14836434871903384493', + FrontQueues: EFlag.Green, + StoragePoolName: 'static', + ReadThroughput: '0', + WriteThroughput: '420', +}); + +interface NodeGeneratorOptions { + vdisksCount?: number; + pdisksCount?: number; +} + +const DEFAULT_OPTIONS: NodeGeneratorOptions = { + vdisksCount: 12, + pdisksCount: 4, +}; + +const generateNode = (nodeId: number, options: NodeGeneratorOptions = {}): TNodeInfo => { + const pdisksCount = options.pdisksCount ?? DEFAULT_OPTIONS.pdisksCount; + const vdisksCount = options.vdisksCount ?? DEFAULT_OPTIONS.vdisksCount; + + return { + NodeId: nodeId, + UptimeSeconds: 284, + CpuUsage: 0.00947996, + DiskSpaceUsage: 0.234985, + SystemState: generateSystemState(nodeId), + PDisks: Array.from({length: pdisksCount!}, (_, i) => + generatePDisk(nodeId, i + 1, getRandomDiskSize()), + ), + VDisks: Array.from({length: vdisksCount!}, (_, i) => { + // Distribute VDisks evenly across PDisks + const pdiskId = (i % pdisksCount!) + 1; + return generateVDisk(nodeId, i, pdiskId); + }), + }; +}; + +export const generateNodes = (count = 1, options?: NodeGeneratorOptions): TNodesInfo => { + const nodes = Array.from({length: count}, (_, i) => generateNode(i + 1, options)); + return { + TotalNodes: count.toString(), + FoundNodes: count.toString(), + Nodes: nodes, + }; +}; From ada29cdf96accf223a52bfd343102507b175abc3 Mon Sep 17 00:00:00 2001 From: astandrik Date: Mon, 16 Dec 2024 18:44:09 +0300 Subject: [PATCH 2/7] chore: use mocks --- src/containers/Storage/StorageNodes/getNodes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index dedf1f3880..9be1bbe455 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -46,9 +46,9 @@ export const getStorageNodes: FetchData< const dataFieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); let response; - if (process.env.NODE_ENV === 'development') { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('mocks')) { // Get mock configuration from URL parameters or use defaults - const urlParams = new URLSearchParams(window.location.search); const pdisks = parseInt(urlParams.get('pdisks') || '10', 10); const vdisksPerPDisk = parseInt(urlParams.get('vdisksPerPDisk') || '2', 10); response = generateNodes(5, {vdisksCount: pdisks * vdisksPerPDisk, pdisksCount: pdisks}); From 3bceb228e8a71c57e1612a60f9bc05ad66fa48ac Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 16 Dec 2024 19:11:32 +0300 Subject: [PATCH 3/7] rm overflow --- src/containers/Storage/PDisk/PDisk.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index 4cd9dccc56..3078e3f189 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -17,7 +17,6 @@ &__vdisks { display: flex; - overflow: hidden; // Prevent overflow flex: 0 0 auto; gap: 2px; From 74909ea954bfe324610d0d9a18c51eb2793ed000 Mon Sep 17 00:00:00 2001 From: astandrik Date: Tue, 17 Dec 2024 16:45:10 +0300 Subject: [PATCH 4/7] fix: review fixes --- src/containers/Storage/Disks/Disks.scss | 2 +- src/containers/Storage/PDisk/PDisk.scss | 14 +- src/containers/Storage/PDisk/PDisk.tsx | 16 +- .../columns/StorageNodesColumns.scss | 1 + .../Storage/StorageNodes/columns/columns.tsx | 7 +- .../Storage/StorageNodes/getNodes.ts | 2 +- src/mocks/storage/nodes.ts | 53 ++++- .../calculateMaximumSlotsPerDisk.test.ts | 190 ++++++++++++++++++ .../prepareGroupsDisks.test.ts | 0 src/store/reducers/storage/types.ts | 1 + src/store/reducers/storage/utils.ts | 33 ++- src/types/api/nodes.ts | 2 + 12 files changed, 300 insertions(+), 21 deletions(-) create mode 100644 src/store/reducers/storage/__tests__/calculateMaximumSlotsPerDisk.test.ts rename src/store/reducers/storage/{__test__ => __tests__}/prepareGroupsDisks.test.ts (100%) diff --git a/src/containers/Storage/Disks/Disks.scss b/src/containers/Storage/Disks/Disks.scss index 128c13d7f7..75c8f9d99b 100644 --- a/src/containers/Storage/Disks/Disks.scss +++ b/src/containers/Storage/Disks/Disks.scss @@ -26,7 +26,7 @@ } &__pdisk-item { - width: 80px; + min-width: 80px; } &__pdisk-progress-bar { --progress-bar-full-height: 20px; diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index 3078e3f189..a3706279ae 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -1,10 +1,18 @@ .pdisk-storage { + --pdisk-vdisk-width: 3px; + --pdisk-gap-width: 2px; + position: relative; display: flex; flex-direction: column; + justify-content: flex-end; min-width: 120px; + width: calc( + var(--pdisk-max-slots, 1) * var(--pdisk-vdisk-width) + (var(--pdisk-max-slots, 1) - 1) * + var(--pdisk-gap-width) + ); &__content { position: relative; @@ -18,7 +26,7 @@ &__vdisks { display: flex; flex: 0 0 auto; - gap: 2px; + gap: var(--pdisk-gap-width); margin-bottom: 4px; @@ -26,9 +34,9 @@ } &__vdisks-item { - flex: 0 0 3px; + flex: 0 0 var(--pdisk-vdisk-width); - min-width: 3px; + min-width: var(--pdisk-vdisk-width); .stack__layer { .data-table__row:hover & { diff --git a/src/containers/Storage/PDisk/PDisk.tsx b/src/containers/Storage/PDisk/PDisk.tsx index b37b5bbf40..e6cc14073b 100644 --- a/src/containers/Storage/PDisk/PDisk.tsx +++ b/src/containers/Storage/PDisk/PDisk.tsx @@ -16,6 +16,8 @@ import './PDisk.scss'; const b = cn('pdisk-storage'); +const PDISK_MAX_SLOTS_CSS_VAR = '--pdisk-max-slots'; + interface PDiskProps { data?: PreparedPDisk; vDisks?: PreparedVDisk[]; @@ -25,6 +27,7 @@ interface PDiskProps { className?: string; progressBarClassName?: string; viewContext?: StorageViewContext; + maximumSlotsPerDisk?: string; } export const PDisk = ({ @@ -36,6 +39,7 @@ export const PDisk = ({ className, progressBarClassName, viewContext, + maximumSlotsPerDisk, }: PDiskProps) => { const {NodeId, PDiskId} = data; const pDiskIdsDefined = valueIsDefined(NodeId) && valueIsDefined(PDiskId); @@ -73,7 +77,17 @@ export const PDisk = ({ } return ( -
+
{renderVDisks()} - +
); })} diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index 9be1bbe455..a5c0a3601f 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -51,7 +51,7 @@ export const getStorageNodes: FetchData< // Get mock configuration from URL parameters or use defaults const pdisks = parseInt(urlParams.get('pdisks') || '10', 10); const vdisksPerPDisk = parseInt(urlParams.get('vdisksPerPDisk') || '2', 10); - response = generateNodes(5, {vdisksCount: pdisks * vdisksPerPDisk, pdisksCount: pdisks}); + response = generateNodes(5, {maxVdisksPerPDisk: vdisksPerPDisk, maxPdisks: pdisks}); } else { response = await window.api.viewer.getNodes({ type, diff --git a/src/mocks/storage/nodes.ts b/src/mocks/storage/nodes.ts index 14fb9f7682..3184c740e8 100644 --- a/src/mocks/storage/nodes.ts +++ b/src/mocks/storage/nodes.ts @@ -118,18 +118,27 @@ const generateVDisk = (nodeId: number, vdiskId: number, pdiskId: number) => ({ }); interface NodeGeneratorOptions { - vdisksCount?: number; - pdisksCount?: number; + maxVdisksPerPDisk?: number; + maxPdisks?: number; } const DEFAULT_OPTIONS: NodeGeneratorOptions = { - vdisksCount: 12, - pdisksCount: 4, + maxVdisksPerPDisk: 3, + maxPdisks: 4, }; const generateNode = (nodeId: number, options: NodeGeneratorOptions = {}): TNodeInfo => { - const pdisksCount = options.pdisksCount ?? DEFAULT_OPTIONS.pdisksCount; - const vdisksCount = options.vdisksCount ?? DEFAULT_OPTIONS.vdisksCount; + const maxPdisks = options.maxPdisks ?? DEFAULT_OPTIONS.maxPdisks!; + const maxVdisksPerPDisk = options.maxVdisksPerPDisk ?? DEFAULT_OPTIONS.maxVdisksPerPDisk!; + + // Generate a random number of pdisks up to maxPdisks + const pdisksCount = Math.floor(Math.random() * maxPdisks) + 1; + + // For each pdisk, generate a random number of vdisks up to maxVdisksPerPDisk + const pdiskVdisksCounts = Array.from({length: pdisksCount}, () => + Math.floor(Math.random() * maxVdisksPerPDisk), + ); + const totalVdisks = pdiskVdisksCounts.reduce((sum: number, count: number) => sum + count, 0); return { NodeId: nodeId, @@ -137,22 +146,44 @@ const generateNode = (nodeId: number, options: NodeGeneratorOptions = {}): TNode CpuUsage: 0.00947996, DiskSpaceUsage: 0.234985, SystemState: generateSystemState(nodeId), - PDisks: Array.from({length: pdisksCount!}, (_, i) => + PDisks: Array.from({length: pdisksCount}, (_, i) => generatePDisk(nodeId, i + 1, getRandomDiskSize()), ), - VDisks: Array.from({length: vdisksCount!}, (_, i) => { - // Distribute VDisks evenly across PDisks - const pdiskId = (i % pdisksCount!) + 1; - return generateVDisk(nodeId, i, pdiskId); + VDisks: Array.from({length: totalVdisks}, (_, i) => { + // Find which pdisk this vdisk belongs to based on the distribution + let pdiskIndex = 0; + let vdiskCount = pdiskVdisksCounts[0]; + while (i >= vdiskCount && pdiskIndex < pdisksCount - 1) { + pdiskIndex++; + vdiskCount += pdiskVdisksCounts[pdiskIndex]; + } + return generateVDisk(nodeId, i, pdiskIndex + 1); }), }; }; export const generateNodes = (count = 1, options?: NodeGeneratorOptions): TNodesInfo => { const nodes = Array.from({length: count}, (_, i) => generateNode(i + 1, options)); + + // Calculate MaximumSlotsPerDisk as the maximum number of vdisks on any pdisk + let maxSlotsPerDisk = 0; + nodes.forEach((node) => { + if (node.VDisks) { + const pdiskVdiskCounts = new Map(); + node.VDisks.forEach((vdisk) => { + if (typeof vdisk.PDiskId === 'number') { + const count = (pdiskVdiskCounts.get(vdisk.PDiskId) || 0) + 1; + pdiskVdiskCounts.set(vdisk.PDiskId, count); + maxSlotsPerDisk = Math.max(maxSlotsPerDisk, count); + } + }); + } + }); + return { TotalNodes: count.toString(), FoundNodes: count.toString(), Nodes: nodes, + MaximumSlotsPerDisk: maxSlotsPerDisk.toString(), }; }; diff --git a/src/store/reducers/storage/__tests__/calculateMaximumSlotsPerDisk.test.ts b/src/store/reducers/storage/__tests__/calculateMaximumSlotsPerDisk.test.ts new file mode 100644 index 0000000000..0815f84fbf --- /dev/null +++ b/src/store/reducers/storage/__tests__/calculateMaximumSlotsPerDisk.test.ts @@ -0,0 +1,190 @@ +import type {TNodeInfo} from '../../../../types/api/nodes'; +import {TPDiskState} from '../../../../types/api/pdisk'; +import {EVDiskState} from '../../../../types/api/vdisk'; +import type {TVDiskID} from '../../../../types/api/vdisk'; +import {calculateMaximumSlotsPerDisk} from '../utils'; + +const createVDiskId = (id: number): TVDiskID => ({ + GroupID: id, + GroupGeneration: 1, + Ring: 1, + Domain: 1, + VDisk: id, +}); + +describe('calculateMaximumSlotsPerDisk', () => { + it('should return providedMaximumSlotsPerDisk when it is provided', () => { + const nodes: TNodeInfo[] = []; + const providedMaximumSlotsPerDisk = '5'; + + expect(calculateMaximumSlotsPerDisk(nodes, providedMaximumSlotsPerDisk)).toBe('5'); + }); + + it('should return "1" for empty nodes array', () => { + const nodes: TNodeInfo[] = []; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('1'); + }); + + it('should return "1" for undefined nodes', () => { + expect(calculateMaximumSlotsPerDisk(undefined)).toBe('1'); + }); + + it('should return "1" for nodes without PDisks or VDisks', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + }, + ]; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('1'); + }); + + it('should calculate maximum slots correctly for single node with one PDisk and multiple VDisks', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + ], + VDisks: [ + { + VDiskId: createVDiskId(1), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(2), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + ], + }, + ]; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('2'); + }); + + it('should calculate maximum slots across multiple nodes', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + ], + VDisks: [ + { + VDiskId: createVDiskId(1), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + ], + }, + { + NodeId: 2, + SystemState: {}, + PDisks: [ + { + PDiskId: 2, + State: TPDiskState.Normal, + }, + ], + VDisks: [ + { + VDiskId: createVDiskId(2), + PDiskId: 2, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(3), + PDiskId: 2, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(4), + PDiskId: 2, + VDiskState: EVDiskState.OK, + }, + ], + }, + ]; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('3'); + }); + + it('should handle nodes with multiple PDisks', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + { + PDiskId: 2, + State: TPDiskState.Normal, + }, + ], + VDisks: [ + { + VDiskId: createVDiskId(1), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(2), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(3), + PDiskId: 2, + VDiskState: EVDiskState.OK, + }, + ], + }, + ]; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('2'); + }); + + it('should ignore VDisks with non-matching PDiskId', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + ], + VDisks: [ + { + VDiskId: createVDiskId(1), + PDiskId: 1, + VDiskState: EVDiskState.OK, + }, + { + VDiskId: createVDiskId(2), + PDiskId: 2, // Non-matching PDiskId + VDiskState: EVDiskState.OK, + }, + ], + }, + ]; + + expect(calculateMaximumSlotsPerDisk(nodes)).toBe('1'); + }); +}); diff --git a/src/store/reducers/storage/__test__/prepareGroupsDisks.test.ts b/src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts similarity index 100% rename from src/store/reducers/storage/__test__/prepareGroupsDisks.test.ts rename to src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts diff --git a/src/store/reducers/storage/types.ts b/src/store/reducers/storage/types.ts index e9378e6485..829405f28e 100644 --- a/src/store/reducers/storage/types.ts +++ b/src/store/reducers/storage/types.ts @@ -35,6 +35,7 @@ export interface PreparedStorageNode extends PreparedNodeSystemState { VDisks?: PreparedVDisk[]; Missing: number; + MaximumSlotsPerDisk: string; } export interface PreparedStorageGroupFilters { diff --git a/src/store/reducers/storage/utils.ts b/src/store/reducers/storage/utils.ts index 8605d2eae5..e759800b3d 100644 --- a/src/store/reducers/storage/utils.ts +++ b/src/store/reducers/storage/utils.ts @@ -188,7 +188,10 @@ const prepareStorageGroups = ( // ==== Prepare nodes ==== -const prepareStorageNodeData = (node: TNodeInfo): PreparedStorageNode => { +const prepareStorageNodeData = ( + node: TNodeInfo, + maximumSlotsPerDisk: string, +): PreparedStorageNode => { const missing = node.PDisks?.filter((pDisk) => { return pDisk.State !== TPDiskState.Normal; @@ -214,13 +217,36 @@ const prepareStorageNodeData = (node: TNodeInfo): PreparedStorageNode => { PDisks: pDisks, VDisks: vDisks, Missing: missing, + MaximumSlotsPerDisk: maximumSlotsPerDisk, }; }; +export const calculateMaximumSlotsPerDisk = ( + nodes: TNodeInfo[] | undefined, + providedMaximumSlotsPerDisk?: string, +) => { + if (providedMaximumSlotsPerDisk) { + return providedMaximumSlotsPerDisk; + } + + return String( + Math.max( + 1, + ...(nodes || []).flatMap((node) => + (node.PDisks || []).map( + (pDisk) => + (node.VDisks || []).filter((vDisk) => vDisk.PDiskId === pDisk.PDiskId) + .length || 0, + ), + ), + ), + ); +}; + // ==== Prepare responses ==== export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageResponse => { - const {Nodes, TotalNodes, FoundNodes, NodeGroups} = data; + const {Nodes, TotalNodes, FoundNodes, NodeGroups, MaximumSlotsPerDisk} = data; const tableGroups = NodeGroups?.map(({GroupName, NodeCount}) => { if (GroupName && NodeCount) { @@ -232,7 +258,8 @@ export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageRe return undefined; }).filter((group): group is TableGroup => Boolean(group)); - const preparedNodes = Nodes?.map(prepareStorageNodeData); + const maximumSlots = calculateMaximumSlotsPerDisk(Nodes, MaximumSlotsPerDisk); + const preparedNodes = Nodes?.map((node) => prepareStorageNodeData(node, maximumSlots)); return { nodes: preparedNodes, diff --git a/src/types/api/nodes.ts b/src/types/api/nodes.ts index af800716d2..716d974ab9 100644 --- a/src/types/api/nodes.ts +++ b/src/types/api/nodes.ts @@ -18,6 +18,8 @@ export interface TNodesInfo { TotalNodes: string; /** uint64 */ FoundNodes: string; + /** uint64 */ + MaximumSlotsPerDisk?: string; } export interface TNodeInfo { From aec3f146d6bebea13a4ea733969e4591ff0eb247 Mon Sep 17 00:00:00 2001 From: astandrik Date: Tue, 17 Dec 2024 16:50:49 +0300 Subject: [PATCH 5/7] fix: lint --- src/containers/Storage/PDisk/PDisk.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index a3706279ae..d6aa887fe6 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -8,11 +8,11 @@ flex-direction: column; justify-content: flex-end; - min-width: 120px; width: calc( var(--pdisk-max-slots, 1) * var(--pdisk-vdisk-width) + (var(--pdisk-max-slots, 1) - 1) * var(--pdisk-gap-width) ); + min-width: 120px; &__content { position: relative; From 059c9b2487cd8f284cc82df8b07d686d5d0cea35 Mon Sep 17 00:00:00 2001 From: astandrik Date: Wed, 18 Dec 2024 18:14:42 +0300 Subject: [PATCH 6/7] fix: delete mocks --- .../Storage/StorageNodes/getNodes.ts | 43 ++-- src/mocks/storage/nodes.ts | 189 ------------------ 2 files changed, 17 insertions(+), 215 deletions(-) delete mode 100644 src/mocks/storage/nodes.ts diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index a5c0a3601f..fa1fca20e4 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -3,7 +3,6 @@ import { NODES_COLUMNS_TO_DATA_FIELDS, getNodesColumnSortField, } from '../../../components/nodesColumns/constants'; -import {generateNodes} from '../../../mocks/storage/nodes'; import type { PreparedStorageNode, PreparedStorageNodeFilters, @@ -45,31 +44,23 @@ export const getStorageNodes: FetchData< const dataFieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); - let response; - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.get('mocks')) { - // Get mock configuration from URL parameters or use defaults - const pdisks = parseInt(urlParams.get('pdisks') || '10', 10); - const vdisksPerPDisk = parseInt(urlParams.get('vdisksPerPDisk') || '2', 10); - response = generateNodes(5, {maxVdisksPerPDisk: vdisksPerPDisk, maxPdisks: pdisks}); - } else { - response = await window.api.viewer.getNodes({ - type, - storage, - limit, - offset, - sort, - filter: searchValue, - uptime: getUptimeParamValue(nodesUptimeFilter), - with: visibleEntities, - database, - node_id: nodeId, - group_id: groupId, - filter_group: filterGroup, - filter_group_by: filterGroupBy, - fieldsRequired: dataFieldsRequired, - }); - } + const response = await window.api.viewer.getNodes({ + type, + storage, + limit, + offset, + sort, + filter: searchValue, + uptime: getUptimeParamValue(nodesUptimeFilter), + with: visibleEntities, + database, + node_id: nodeId, + group_id: groupId, + filter_group: filterGroup, + filter_group_by: filterGroupBy, + fieldsRequired: dataFieldsRequired, + }); + const preparedResponse = prepareStorageNodesResponse(response); return { data: preparedResponse.nodes || [], diff --git a/src/mocks/storage/nodes.ts b/src/mocks/storage/nodes.ts deleted file mode 100644 index 3184c740e8..0000000000 --- a/src/mocks/storage/nodes.ts +++ /dev/null @@ -1,189 +0,0 @@ -import {EFlag} from '../../types/api/enums'; -import type { - TEndpoint, - TNodeInfo, - TNodesInfo, - TPoolStats, - TSystemStateInfo, -} from '../../types/api/nodes'; -import {TPDiskState} from '../../types/api/pdisk'; -import {EVDiskState} from '../../types/api/vdisk'; - -// Different disk sizes to simulate variety (in bytes) -const DISK_SIZES = [ - '68719476736', // 64 GB - '137438953472', // 128 GB - '274877906944', // 256 GB - '549755813888', // 512 GB - '1099511627776', // 1 TB -]; - -const getRandomDiskSize = () => DISK_SIZES[Math.floor(Math.random() * DISK_SIZES.length)]; - -const generatePoolStats = (count = 5): TPoolStats[] => { - const poolNames = ['System', 'User', 'Batch', 'IO', 'IC'] as const; - return poolNames.slice(0, count).map((Name) => ({ - Name, - Usage: Math.random() * 0.02, - Threads: Math.floor(Math.random() * 3) + 1, - })); -}; - -const generateEndpoints = (): TEndpoint[] => [ - {Name: 'ic', Address: ':19001'}, - {Name: 'http-mon', Address: ':8765'}, - {Name: 'grpcs', Address: ':2135'}, - {Name: 'grpc', Address: ':2136'}, -]; - -const generateSystemState = (nodeId: number): TSystemStateInfo => ({ - StartTime: '1734358137851', - ChangeTime: '1734358421375', - LoadAverage: [3.381347656, 2.489257813, 1.279296875], - NumberOfCpus: 8, - SystemState: EFlag.Green, - NodeId: nodeId, - Host: `localhost-${nodeId}`, - Version: 'main.95ce0df', - PoolStats: generatePoolStats(), - Endpoints: generateEndpoints(), - Roles: ['Bootstrapper', 'StateStorage', 'StateStorageBoard', 'SchemeBoard', 'Storage'], - MemoryLimit: '2147483648', - MaxDiskUsage: 0.002349853516, - Location: { - DataCenter: '1', - Rack: '1', - Unit: '1', - }, - TotalSessions: 0, - CoresUsed: 0.07583969556, - CoresTotal: 8, -}); - -const generatePDisk = (nodeId: number, pdiskId: number, totalSize = '68719476736') => ({ - PDiskId: pdiskId, - ChangeTime: '1734358142074', - Path: `/ydb_data/pdisk${pdiskId}l3ki78no.data`, - Guid: pdiskId.toString(), - Category: '0', - TotalSize: totalSize, - AvailableSize: (Number(totalSize) * 0.9).toString(), // 90% available by default - State: TPDiskState.Normal, - NodeId: nodeId, - Device: EFlag.Green, - Realtime: EFlag.Green, - SerialNumber: '', - SystemSize: '213909504', - LogUsedSize: '35651584', - LogTotalSize: '68486692864', - EnforcedDynamicSlotSize: '22817013760', -}); - -const generateVDisk = (nodeId: number, vdiskId: number, pdiskId: number) => ({ - VDiskId: { - GroupID: vdiskId, - GroupGeneration: 1, - Ring: 0, - Domain: 0, - VDisk: 0, - }, - ChangeTime: '1734358420919', - PDiskId: pdiskId, - VDiskSlotId: vdiskId, - Guid: '1', - Kind: '0', - NodeId: nodeId, - VDiskState: EVDiskState.OK, - DiskSpace: EFlag.Green, - SatisfactionRank: { - FreshRank: { - Flag: EFlag.Green, - }, - LevelRank: { - Flag: EFlag.Green, - }, - }, - Replicated: true, - ReplicationProgress: 1, - ReplicationSecondsRemaining: 0, - AllocatedSize: '0', - AvailableSize: '22817013760', - HasUnreadableBlobs: false, - IncarnationGuid: '11528832187803248876', - InstanceGuid: '14836434871903384493', - FrontQueues: EFlag.Green, - StoragePoolName: 'static', - ReadThroughput: '0', - WriteThroughput: '420', -}); - -interface NodeGeneratorOptions { - maxVdisksPerPDisk?: number; - maxPdisks?: number; -} - -const DEFAULT_OPTIONS: NodeGeneratorOptions = { - maxVdisksPerPDisk: 3, - maxPdisks: 4, -}; - -const generateNode = (nodeId: number, options: NodeGeneratorOptions = {}): TNodeInfo => { - const maxPdisks = options.maxPdisks ?? DEFAULT_OPTIONS.maxPdisks!; - const maxVdisksPerPDisk = options.maxVdisksPerPDisk ?? DEFAULT_OPTIONS.maxVdisksPerPDisk!; - - // Generate a random number of pdisks up to maxPdisks - const pdisksCount = Math.floor(Math.random() * maxPdisks) + 1; - - // For each pdisk, generate a random number of vdisks up to maxVdisksPerPDisk - const pdiskVdisksCounts = Array.from({length: pdisksCount}, () => - Math.floor(Math.random() * maxVdisksPerPDisk), - ); - const totalVdisks = pdiskVdisksCounts.reduce((sum: number, count: number) => sum + count, 0); - - return { - NodeId: nodeId, - UptimeSeconds: 284, - CpuUsage: 0.00947996, - DiskSpaceUsage: 0.234985, - SystemState: generateSystemState(nodeId), - PDisks: Array.from({length: pdisksCount}, (_, i) => - generatePDisk(nodeId, i + 1, getRandomDiskSize()), - ), - VDisks: Array.from({length: totalVdisks}, (_, i) => { - // Find which pdisk this vdisk belongs to based on the distribution - let pdiskIndex = 0; - let vdiskCount = pdiskVdisksCounts[0]; - while (i >= vdiskCount && pdiskIndex < pdisksCount - 1) { - pdiskIndex++; - vdiskCount += pdiskVdisksCounts[pdiskIndex]; - } - return generateVDisk(nodeId, i, pdiskIndex + 1); - }), - }; -}; - -export const generateNodes = (count = 1, options?: NodeGeneratorOptions): TNodesInfo => { - const nodes = Array.from({length: count}, (_, i) => generateNode(i + 1, options)); - - // Calculate MaximumSlotsPerDisk as the maximum number of vdisks on any pdisk - let maxSlotsPerDisk = 0; - nodes.forEach((node) => { - if (node.VDisks) { - const pdiskVdiskCounts = new Map(); - node.VDisks.forEach((vdisk) => { - if (typeof vdisk.PDiskId === 'number') { - const count = (pdiskVdiskCounts.get(vdisk.PDiskId) || 0) + 1; - pdiskVdiskCounts.set(vdisk.PDiskId, count); - maxSlotsPerDisk = Math.max(maxSlotsPerDisk, count); - } - }); - } - }); - - return { - TotalNodes: count.toString(), - FoundNodes: count.toString(), - Nodes: nodes, - MaximumSlotsPerDisk: maxSlotsPerDisk.toString(), - }; -}; From 48f3d6fa8fa13e9c8ecc59517e24b25d69944fd3 Mon Sep 17 00:00:00 2001 From: astandrik Date: Wed, 18 Dec 2024 18:15:40 +0300 Subject: [PATCH 7/7] fix: rm newline --- src/containers/Storage/StorageNodes/getNodes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index fa1fca20e4..be9a0714e0 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -60,7 +60,6 @@ export const getStorageNodes: FetchData< filter_group_by: filterGroupBy, fieldsRequired: dataFieldsRequired, }); - const preparedResponse = prepareStorageNodesResponse(response); return { data: preparedResponse.nodes || [],