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 b5654b1d70..d6aa887fe6 100644
--- a/src/containers/Storage/PDisk/PDisk.scss
+++ b/src/containers/Storage/PDisk/PDisk.scss
@@ -1,28 +1,42 @@
.pdisk-storage {
+ --pdisk-vdisk-width: 3px;
+ --pdisk-gap-width: 2px;
+
position: relative;
- width: 120px;
+ 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;
&__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;
- gap: 2px;
+ flex: 0 0 auto;
+ gap: var(--pdisk-gap-width);
margin-bottom: 4px;
+
+ white-space: nowrap;
}
&__vdisks-item {
- flex-basis: 3px;
- flex-shrink: 0;
+ flex: 0 0 var(--pdisk-vdisk-width);
+
+ 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 (
-
+
);
})}
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 {