diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index d6aa887fe..d615a278f 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -1,18 +1,12 @@ .pdisk-storage { - --pdisk-vdisk-width: 3px; - --pdisk-gap-width: 2px; - position: relative; display: flex; flex-direction: column; justify-content: flex-end; - width: calc( - var(--pdisk-max-slots, 1) * var(--pdisk-vdisk-width) + (var(--pdisk-max-slots, 1) - 1) * - var(--pdisk-gap-width) - ); - min-width: 120px; + width: var(--pdisk-width); + min-width: var(--pdisk-min-width); &__content { position: relative; diff --git a/src/containers/Storage/PDisk/PDisk.tsx b/src/containers/Storage/PDisk/PDisk.tsx index e6cc14073..b37b5bbf4 100644 --- a/src/containers/Storage/PDisk/PDisk.tsx +++ b/src/containers/Storage/PDisk/PDisk.tsx @@ -16,8 +16,6 @@ import './PDisk.scss'; const b = cn('pdisk-storage'); -const PDISK_MAX_SLOTS_CSS_VAR = '--pdisk-max-slots'; - interface PDiskProps { data?: PreparedPDisk; vDisks?: PreparedVDisk[]; @@ -27,7 +25,6 @@ interface PDiskProps { className?: string; progressBarClassName?: string; viewContext?: StorageViewContext; - maximumSlotsPerDisk?: string; } export const PDisk = ({ @@ -39,7 +36,6 @@ export const PDisk = ({ className, progressBarClassName, viewContext, - maximumSlotsPerDisk, }: PDiskProps) => { const {NodeId, PDiskId} = data; const pDiskIdsDefined = valueIsDefined(NodeId) && valueIsDefined(PDiskId); @@ -77,17 +73,7 @@ export const PDisk = ({ } return ( -
+
{renderVDisks()} { return { name: NODES_COLUMNS_IDS.PDisks, header: NODES_COLUMNS_TITLES.PDisks, className: b('pdisks-column'), render: ({row}) => { + const pDiskStyles = { + [MAX_SLOTS_CSS_VAR]: row.MaximumSlotsPerDisk, + [MAX_DISKS_CSS_VAR]: row.MaximumDisksPerNode, + } as React.CSSProperties; + return ( -
+
{row.PDisks?.map((pDisk) => { const vDisks = row.VDisks?.filter( (vdisk) => vdisk.PDiskId === pDisk.PDiskId, @@ -45,12 +53,7 @@ const getPDisksColumn = ({viewContext}: GetStorageNodesColumnsParams): StorageNo return (
- +
); })} @@ -59,7 +62,6 @@ const getPDisksColumn = ({viewContext}: GetStorageNodesColumnsParams): StorageNo }, align: DataTable.CENTER, sortable: false, - width: 900, resizeable: false, }; }; diff --git a/src/store/reducers/storage/__tests__/calculateMaximumDisksPerNode.test.ts b/src/store/reducers/storage/__tests__/calculateMaximumDisksPerNode.test.ts new file mode 100644 index 000000000..30774f66b --- /dev/null +++ b/src/store/reducers/storage/__tests__/calculateMaximumDisksPerNode.test.ts @@ -0,0 +1,137 @@ +import type {TNodeInfo} from '../../../../types/api/nodes'; +import {TPDiskState} from '../../../../types/api/pdisk'; +import {calculateMaximumDisksPerNode} from '../utils'; + +describe('calculateMaximumDisksPerNode', () => { + it('should return providedMaximumDisksPerNode when it is provided', () => { + const nodes: TNodeInfo[] = []; + const providedMaximumDisksPerNode = '5'; + + expect(calculateMaximumDisksPerNode(nodes, providedMaximumDisksPerNode)).toBe('5'); + }); + + it('should return "1" for empty nodes array', () => { + const nodes: TNodeInfo[] = []; + + expect(calculateMaximumDisksPerNode(nodes)).toBe('1'); + }); + + it('should return "1" for undefined nodes', () => { + expect(calculateMaximumDisksPerNode(undefined)).toBe('1'); + }); + + it('should return "1" for nodes without PDisks', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + }, + ]; + + expect(calculateMaximumDisksPerNode(nodes)).toBe('1'); + }); + + it('should calculate maximum disks correctly for single node with multiple PDisks', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + { + PDiskId: 2, + State: TPDiskState.Normal, + }, + { + PDiskId: 3, + State: TPDiskState.Normal, + }, + ], + }, + ]; + + expect(calculateMaximumDisksPerNode(nodes)).toBe('3'); + }); + + it('should calculate maximum disks across multiple nodes', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + ], + }, + { + NodeId: 2, + SystemState: {}, + PDisks: [ + { + PDiskId: 2, + State: TPDiskState.Normal, + }, + { + PDiskId: 3, + State: TPDiskState.Normal, + }, + ], + }, + { + NodeId: 3, + SystemState: {}, + PDisks: [ + { + PDiskId: 4, + State: TPDiskState.Normal, + }, + { + PDiskId: 5, + State: TPDiskState.Normal, + }, + { + PDiskId: 6, + State: TPDiskState.Normal, + }, + { + PDiskId: 7, + State: TPDiskState.Normal, + }, + ], + }, + ]; + + expect(calculateMaximumDisksPerNode(nodes)).toBe('4'); + }); + + it('should handle nodes with empty PDisks array', () => { + const nodes: TNodeInfo[] = [ + { + NodeId: 1, + SystemState: {}, + PDisks: [], + }, + { + NodeId: 2, + SystemState: {}, + PDisks: [ + { + PDiskId: 1, + State: TPDiskState.Normal, + }, + { + PDiskId: 2, + State: TPDiskState.Normal, + }, + ], + }, + ]; + + expect(calculateMaximumDisksPerNode(nodes)).toBe('2'); + }); +}); diff --git a/src/store/reducers/storage/types.ts b/src/store/reducers/storage/types.ts index 829405f28..b2c750fd9 100644 --- a/src/store/reducers/storage/types.ts +++ b/src/store/reducers/storage/types.ts @@ -36,6 +36,7 @@ export interface PreparedStorageNode extends PreparedNodeSystemState { Missing: number; MaximumSlotsPerDisk: string; + MaximumDisksPerNode: string; } export interface PreparedStorageGroupFilters { diff --git a/src/store/reducers/storage/utils.ts b/src/store/reducers/storage/utils.ts index e759800b3..09f365a76 100644 --- a/src/store/reducers/storage/utils.ts +++ b/src/store/reducers/storage/utils.ts @@ -191,6 +191,7 @@ const prepareStorageGroups = ( const prepareStorageNodeData = ( node: TNodeInfo, maximumSlotsPerDisk: string, + maximumDisksPerNode: string, ): PreparedStorageNode => { const missing = node.PDisks?.filter((pDisk) => { @@ -218,35 +219,59 @@ const prepareStorageNodeData = ( VDisks: vDisks, Missing: missing, MaximumSlotsPerDisk: maximumSlotsPerDisk, + MaximumDisksPerNode: maximumDisksPerNode, }; }; +/** + * Calculates the maximum number of VDisk slots per PDisk across all nodes + * A slot represents a VDisk that can be allocated to a PDisk + */ export const calculateMaximumSlotsPerDisk = ( nodes: TNodeInfo[] | undefined, providedMaximumSlotsPerDisk?: string, -) => { +): 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, - ), - ), - ), - ); + const safeNodes = nodes || []; + const slotsPerDiskCounts = safeNodes.flatMap((node) => { + const safePDisks = node.PDisks || []; + const safeVDisks = node.VDisks || []; + + return safePDisks.map((pDisk) => { + const vDisksOnPDisk = safeVDisks.filter((vDisk) => vDisk.PDiskId === pDisk.PDiskId); + return vDisksOnPDisk.length || 0; + }); + }); + + const maxSlots = Math.max(1, ...slotsPerDiskCounts); + return String(maxSlots); +}; + +/** + * Calculates the maximum number of PDisks per node across all nodes + */ +export const calculateMaximumDisksPerNode = ( + nodes: TNodeInfo[] | undefined, + providedMaximumDisksPerNode?: string, +): string => { + if (providedMaximumDisksPerNode) { + return providedMaximumDisksPerNode; + } + + const safeNodes = nodes || []; + const disksPerNodeCounts = safeNodes.map((node) => node.PDisks?.length || 0); + const maxDisks = Math.max(1, ...disksPerNodeCounts); + return String(maxDisks); }; // ==== Prepare responses ==== export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageResponse => { - const {Nodes, TotalNodes, FoundNodes, NodeGroups, MaximumSlotsPerDisk} = data; + const {Nodes, TotalNodes, FoundNodes, NodeGroups, MaximumSlotsPerDisk, MaximumDisksPerNode} = + data; const tableGroups = NodeGroups?.map(({GroupName, NodeCount}) => { if (GroupName && NodeCount) { @@ -259,7 +284,10 @@ export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageRe }).filter((group): group is TableGroup => Boolean(group)); const maximumSlots = calculateMaximumSlotsPerDisk(Nodes, MaximumSlotsPerDisk); - const preparedNodes = Nodes?.map((node) => prepareStorageNodeData(node, maximumSlots)); + const maximumDisks = calculateMaximumDisksPerNode(Nodes, MaximumDisksPerNode); + const preparedNodes = Nodes?.map((node) => + prepareStorageNodeData(node, maximumSlots, maximumDisks), + ); return { nodes: preparedNodes, diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index 2b9014038..b7883909c 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -395,3 +395,23 @@ background-color: unset; } } + +@mixin calculate-storage-nodes-pdisk-variables() { + --pdisk-vdisk-width: 3px; + --pdisk-gap-width: 2px; + --pdisk-min-width: 120px; + --pdisk-margin: 10px; + + --pdisk-width: max( + calc( + var(--maximum-slots, 1) * var(--pdisk-vdisk-width) + (var(--maximum-slots, 1) - 1) * + var(--pdisk-gap-width) + ), + var(--pdisk-min-width) + ); + + --pdisks-container-width: calc( + var(--maximum-disks, 1) * var(--pdisk-width) + (var(--maximum-disks, 1) - 1) * + var(--pdisk-margin) + ); +} diff --git a/src/types/api/nodes.ts b/src/types/api/nodes.ts index 716d974ab..8c01c5bdc 100644 --- a/src/types/api/nodes.ts +++ b/src/types/api/nodes.ts @@ -20,6 +20,8 @@ export interface TNodesInfo { FoundNodes: string; /** uint64 */ MaximumSlotsPerDisk?: string; + /** uint64 */ + MaximumDisksPerNode?: string; } export interface TNodeInfo {