From b4a0dc53258e62c1b60ced675a0515df9ba72597 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 8 Apr 2026 19:20:34 +0300 Subject: [PATCH 1/9] feat: vdisk storage --- src/containers/VDiskPage/VDiskPage.scss | 4 + src/containers/VDiskPage/VDiskPage.tsx | 56 +-- .../VDiskPage/VDiskStorageDetails.scss | 134 +++++++ .../VDiskPage/VDiskStorageDetails.tsx | 222 +++++++++++ src/containers/VDiskPage/i18n/en.json | 10 + .../prepareVDiskDataResponse.test.ts | 46 +++ src/store/reducers/vdisk/types.ts | 1 + src/store/reducers/vdisk/utils.ts | 2 + tests/suites/storage/vdiskPage.test.ts | 371 ++++++++++++++++++ ...-storage-details-medium-chromium-linux.png | Bin 0 -> 17253 bytes ...sk-storage-details-medium-safari-linux.png | Bin 0 -> 24918 bytes ...-storage-details-narrow-chromium-linux.png | Bin 0 -> 17376 bytes ...sk-storage-details-narrow-safari-linux.png | Bin 0 -> 25307 bytes ...sk-storage-details-wide-chromium-linux.png | Bin 0 -> 15714 bytes ...disk-storage-details-wide-safari-linux.png | Bin 0 -> 22436 bytes 15 files changed, 824 insertions(+), 22 deletions(-) create mode 100644 src/containers/VDiskPage/VDiskStorageDetails.scss create mode 100644 src/containers/VDiskPage/VDiskStorageDetails.tsx create mode 100644 src/store/reducers/vdisk/__tests__/prepareVDiskDataResponse.test.ts create mode 100644 tests/suites/storage/vdiskPage.test.ts create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-chromium-linux.png create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-safari-linux.png create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-narrow-chromium-linux.png create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-narrow-safari-linux.png create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-wide-chromium-linux.png create mode 100644 tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-wide-safari-linux.png diff --git a/src/containers/VDiskPage/VDiskPage.scss b/src/containers/VDiskPage/VDiskPage.scss index ccb8eaa58b..6ae730a266 100644 --- a/src/containers/VDiskPage/VDiskPage.scss +++ b/src/containers/VDiskPage/VDiskPage.scss @@ -34,6 +34,10 @@ gap: var(--g-spacing-2); } + &__storage-details { + margin-bottom: var(--g-spacing-5); + } + &__tablets-content { margin-top: var(--g-spacing-4); } diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index 81b3b748f0..217dbd49b9 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -19,6 +19,7 @@ import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo'; import {useVDiskPagePath} from '../../routes'; import {api} from '../../store/reducers/api'; +import {useNewStorageViewEnabled} from '../../store/reducers/capabilities/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {vDiskApi} from '../../store/reducers/vdisk/vdisk'; import type {TVDiskID} from '../../types/api/vdisk'; @@ -29,6 +30,7 @@ import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks'; import {useAppTitle} from '../App/AppTitleContext'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; +import {VDiskStorageDetails} from './VDiskStorageDetails'; import {VDiskTablets} from './VDiskTablets'; import {vDiskPageKeyset} from './i18n'; @@ -73,6 +75,7 @@ export function VDiskPage() { const database = databaseParam ?? undefined; const vDiskTab = vDiskTabSchema.parse(activeTab); + const newStorageViewEnabled = useNewStorageViewEnabled(); const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -218,6 +221,14 @@ export function VDiskPage() { return ; }; + const renderStorageDetails = () => { + if (!newStorageViewEnabled) { + return null; + } + + return ; + }; + const renderTabs = () => { return (
@@ -246,28 +257,6 @@ export function VDiskPage() { ); }; - const renderTabsContent = () => { - switch (vDiskTab) { - case 'storage': { - return renderStorageInfo(); - } - case 'tablets': { - return ( - - ); - } - default: - return null; - } - }; - const renderStorageInfo = () => { if (!isNil(GroupID)) { return ( @@ -290,6 +279,28 @@ export function VDiskPage() { return null; }; + const renderTabsContent = () => { + switch (vDiskTab) { + case 'storage': { + return renderStorageInfo(); + } + case 'tablets': { + return ( + + ); + } + default: + return null; + } + }; + const renderContent = () => { if (loading) { return ; @@ -299,6 +310,7 @@ export function VDiskPage() { {error ? : null} {renderInfo()} + {renderStorageDetails()} {renderTabs()} {renderTabsContent()} diff --git a/src/containers/VDiskPage/VDiskStorageDetails.scss b/src/containers/VDiskPage/VDiskStorageDetails.scss new file mode 100644 index 0000000000..59dd4ff736 --- /dev/null +++ b/src/containers/VDiskPage/VDiskStorageDetails.scss @@ -0,0 +1,134 @@ +.ydb-vdisk-storage-details { + &__title { + margin-bottom: var(--g-spacing-4); + } + + &__cards { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: var(--g-spacing-1); + + width: fit-content; + max-width: 100%; + } + + &__card { + min-width: 0; + padding: var(--g-spacing-4) var(--g-spacing-5); + + border-radius: var(--g-border-radius-m) var(--g-border-radius-xs) var(--g-border-radius-xs) + var(--g-border-radius-m); + background-color: var(--g-color-base-generic); + + &_metrics { + display: flex; + flex: 0 0 auto; + align-items: stretch; + gap: var(--g-spacing-5); + + width: max-content; + max-width: 100%; + min-height: 74px; + } + + &_details { + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: var(--g-spacing-4); + + width: fit-content; + max-width: 100%; + min-height: 74px; + + border-radius: var(--g-border-radius-xs) var(--g-border-radius-m) + var(--g-border-radius-m) var(--g-border-radius-xs); + } + } + + &__details { + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + align-items: stretch; + gap: var(--g-spacing-4); + + min-width: 0; + max-width: 100%; + } + + &__metric, + &__detail { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: var(--g-spacing-1); + + min-width: 0; + padding-right: var(--g-spacing-4); + } + + &__metric_usage { + padding-left: var(--g-spacing-5); + + border-left: 1px solid var(--g-color-line-generic); + } + + &__detail { + flex: 0 1 auto; + + min-width: 100px; + max-width: 200px; + } + + &__value, + &__label { + display: block; + + white-space: nowrap; + } + + &__value-row &__value { + overflow: hidden; + + text-overflow: ellipsis; + } + + &__value-row { + flex-wrap: nowrap; + + width: 100%; + min-width: 0; + + &_copyable { + width: auto; + max-width: 100%; + } + } + + &__value-popover { + flex: 1 1 auto; + + min-width: 0; + + &_copyable { + flex: 0 1 auto; + + max-width: 100%; + } + } + + &__value-popover-content { + width: 100%; + min-width: 0; + + &_copyable { + width: auto; + max-width: 100%; + } + } +} diff --git a/src/containers/VDiskPage/VDiskStorageDetails.tsx b/src/containers/VDiskPage/VDiskStorageDetails.tsx new file mode 100644 index 0000000000..15e20613c4 --- /dev/null +++ b/src/containers/VDiskPage/VDiskStorageDetails.tsx @@ -0,0 +1,222 @@ +import {Button, Flex, Text} from '@gravity-ui/uikit'; + +import {CellWithPopover} from '../../components/CellWithPopover/CellWithPopover'; +import {ClipboardButton} from '../../components/ClipboardButton/ClipboardButton'; +import {getPDiskPagePath} from '../../routes'; +import type {VDiskData} from '../../store/reducers/vdisk/types'; +import {cn} from '../../utils/cn'; +import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; +import {formatNumber, formatStorageValuesToGb} from '../../utils/dataFormatters/dataFormatters'; + +import {vDiskPageKeyset} from './i18n'; + +import './VDiskStorageDetails.scss'; + +const b = cn('ydb-vdisk-storage-details'); + +interface VDiskStorageDetailsProps { + className?: string; + data?: VDiskData; +} + +interface MetricItemProps { + title: string; + value: string; + isUsage?: boolean; +} + +interface CopyableDetailItemProps { + title: string; + value?: string | number; +} + +interface DetailItemProps { + title: string; + value?: string | number; +} + +interface TruncatedDetailValueProps { + value?: string | number; +} + +function formatStorageMetric(value?: number) { + const [formattedValue] = formatStorageValuesToGb(value); + + return formattedValue || EMPTY_DATA_PLACEHOLDER; +} + +function formatUsage(value?: number) { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) { + return EMPTY_DATA_PLACEHOLDER; + } + + const formattedValue = formatNumber(numericValue); + + return formattedValue ? `${formattedValue}%` : EMPTY_DATA_PLACEHOLDER; +} + +function getFreeSize(availableSize?: number, sizeLimit?: number, allocatedSize?: number) { + const normalizedAvailableSize = Number(availableSize); + + if (Number.isFinite(normalizedAvailableSize) && normalizedAvailableSize >= 0) { + return normalizedAvailableSize; + } + + const normalizedSizeLimit = Number(sizeLimit); + const normalizedAllocatedSize = Number(allocatedSize); + + if (!Number.isFinite(normalizedSizeLimit) || normalizedSizeLimit < 0) { + return undefined; + } + + if (!Number.isFinite(normalizedAllocatedSize) || normalizedAllocatedSize < 0) { + return normalizedSizeLimit; + } + + return Math.max(normalizedSizeLimit - normalizedAllocatedSize, 0); +} + +function MetricItem({title, value, isUsage}: MetricItemProps) { + return ( +
+ + {value} + + + {title} + +
+ ); +} + +function DetailItem({title, value}: DetailItemProps) { + return ( +
+ + + {title} + +
+ ); +} + +function TruncatedDetailValue({value}: TruncatedDetailValueProps) { + const normalizedValue = + value === undefined || value === null || value === '' ? undefined : String(value); + const displayValue = normalizedValue ?? EMPTY_DATA_PLACEHOLDER; + + return ( + + + {displayValue} + + + ); +} + +function CopyableDetailItem({title, value}: CopyableDetailItemProps) { + const normalizedValue = + value === undefined || value === null || value === '' ? undefined : String(value); + const displayValue = normalizedValue ?? EMPTY_DATA_PLACEHOLDER; + + return ( +
+ + + + {displayValue} + + + + + + {title} + +
+ ); +} + +export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps) { + const used = Number(data?.AllocatedSize); + const total = Number(data?.SizeLimit); + const usage = Number(data?.AllocatedPercent); + const free = getFreeSize(data?.AvailableSize, data?.SizeLimit, data?.AllocatedSize); + + const {NodeDC, NodeRack, NodeHost, NodeId, PDiskId} = data || {}; + + const pDiskPath = + NodeId !== undefined && PDiskId !== undefined + ? getPDiskPagePath(PDiskId, NodeId) + : undefined; + + return ( +
+ + {vDiskPageKeyset('title_storage-details')} + +
+
+ + + + +
+
+
+ + + + +
+ {pDiskPath ? ( + + ) : null} +
+
+
+ ); +} diff --git a/src/containers/VDiskPage/i18n/en.json b/src/containers/VDiskPage/i18n/en.json index edb7219ba4..b875f4e92f 100644 --- a/src/containers/VDiskPage/i18n/en.json +++ b/src/containers/VDiskPage/i18n/en.json @@ -5,6 +5,16 @@ "vdisk": "VDisk", "storage": "Storage", "tablets": "Tablets", + "title_storage-details": "Storage details", + "field_storage-details-used": "Used", + "field_storage-details-total": "Total", + "field_storage-details-free": "Free", + "field_storage-details-usage": "Usage", + "field_datacenter": "Datacenter", + "field_rack": "Rack", + "field_node": "Node", + "field_pdisk-id": "PDisk ID", + "action_go-to-pdisk": "Go to PDisk", "tablet-id": "Tablet ID", "channel-id": "Channel ID", diff --git a/src/store/reducers/vdisk/__tests__/prepareVDiskDataResponse.test.ts b/src/store/reducers/vdisk/__tests__/prepareVDiskDataResponse.test.ts new file mode 100644 index 0000000000..a7866ed494 --- /dev/null +++ b/src/store/reducers/vdisk/__tests__/prepareVDiskDataResponse.test.ts @@ -0,0 +1,46 @@ +import type {StorageGroupsResponse} from '../../../../types/api/storage'; +import type {TEvSystemStateResponse} from '../../../../types/api/systemState'; +import {prepareVDiskDataResponse} from '../utils'; + +describe('prepareVDiskDataResponse', () => { + test('adds NodeRack from prepared node system state', () => { + const storageGroupResponse = { + StorageGroups: [ + { + StorageGroups: undefined, + VDisks: [ + { + VDiskId: '2181038080-1-0-0-0', + NodeId: 42, + PDisk: { + PDiskId: '42-7', + }, + }, + ], + }, + ], + } as unknown as StorageGroupsResponse; + + const nodeResponse = { + SystemStateInfo: [ + { + Host: 'storage-node-07.ydb', + Roles: ['Storage'], + Location: { + Rack: 'Rack-A-12', + DataCenter: 'KLG', + }, + }, + ], + } as TEvSystemStateResponse; + + const result = prepareVDiskDataResponse( + [storageGroupResponse, nodeResponse], + '2181038080-1-0-0-0', + ); + + expect(result.NodeRack).toBe('Rack-A-12'); + expect(result.NodeDC).toBe('KLG'); + expect(result.NodeHost).toBe('storage-node-07.ydb'); + }); +}); diff --git a/src/store/reducers/vdisk/types.ts b/src/store/reducers/vdisk/types.ts index 4a75abced7..50b8140f33 100644 --- a/src/store/reducers/vdisk/types.ts +++ b/src/store/reducers/vdisk/types.ts @@ -5,6 +5,7 @@ export interface VDiskData extends PreparedVDisk { NodeHost?: string; NodeType?: string; NodeDC?: string; + NodeRack?: string; PDiskId?: number; PDiskType?: string; diff --git a/src/store/reducers/vdisk/utils.ts b/src/store/reducers/vdisk/utils.ts index 54dc99ce9e..cc9bfc4fd4 100644 --- a/src/store/reducers/vdisk/utils.ts +++ b/src/store/reducers/vdisk/utils.ts @@ -42,6 +42,7 @@ export function prepareVDiskDataResponse( const NodeHost = preparedNode.Host; const NodeType = preparedNode.Roles?.[0]; const NodeDC = preparedNode.DC; + const NodeRack = preparedNode.Rack; const PDiskId = currentVDisk.PDiskId ?? preparedPDisk?.PDiskId; const PDiskType = preparedPDisk?.Type; @@ -53,6 +54,7 @@ export function prepareVDiskDataResponse( NodeHost, NodeType, NodeDC, + NodeRack, PDiskId, PDiskType, diff --git a/tests/suites/storage/vdiskPage.test.ts b/tests/suites/storage/vdiskPage.test.ts new file mode 100644 index 0000000000..aaffc2ca21 --- /dev/null +++ b/tests/suites/storage/vdiskPage.test.ts @@ -0,0 +1,371 @@ +import {expect, test} from '@playwright/test'; +import type {Page} from '@playwright/test'; + +import {ClusterStorageTable} from '../paginatedTable/paginatedTable'; + +const VDISK_ID = '2181038080-1-0-0-0'; +const SIBLING_VDISK_ID = '2181038080-1-0-0-1'; +const ANOTHER_PDISK_VDISK_ID = '2181038080-1-0-0-2'; +const NODE_ID = '42'; +const PDISK_ID = '1000'; +const SECOND_PDISK_ID = '1001'; +const GROUP_ID = '2181038080'; +const DATABASE = '/local'; +const VDISK_PAGE_PATH = `/vDisk?database=${DATABASE}&nodeId=${NODE_ID}&vDiskId=${VDISK_ID}&activeTab=storage&type=groups`; +const STORAGE_POOL_NAME = 'dynamic_storage_pool:1'; +const LONG_DATACENTER = 'KALUGA Central DC 12 City Region DC 2026'; +const LONG_RACK = 'Rack-A-12-2026A-12-2026'; +const LONG_HOST = 'storage-node-1273683y-1273683y-1273683y.ydb'; +const LONG_PDISK_ID = '1000-1012'; + +interface SetupVDiskPageMocksOptions { + datacenter?: string; + rack?: string; + host?: string; + pDiskId?: string; +} + +function createStorageGroupsResponse({ + pDiskId = PDISK_ID, +}: Pick = {}) { + return { + StorageGroups: [ + { + GroupId: GROUP_ID, + PoolName: STORAGE_POOL_NAME, + MediaType: 'SSD', + ErasureSpecies: 'mirror-3-dc', + Used: '10000000000', + Limit: '196000000000', + Available: '186000000000', + State: 'ok', + VDisks: [ + { + VDiskId: VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1001, + AllocatedSize: '10000000000', + AvailableSize: '186000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${pDiskId}`, + Type: 'ROT', + }, + }, + { + VDiskId: SIBLING_VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1002, + AllocatedSize: '9000000000', + AvailableSize: '187000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${pDiskId}`, + Type: 'ROT', + }, + }, + { + VDiskId: ANOTHER_PDISK_VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1003, + AllocatedSize: '8000000000', + AvailableSize: '188000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${SECOND_PDISK_ID}`, + Type: 'ROT', + }, + }, + ], + }, + ], + }; +} + +async function setupCapabilitiesMock(page: Page) { + await page.route('**/viewer/capabilities*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + Database: DATABASE, + Capabilities: { + '/storage/groups': 10, + '/viewer/nodes': 20, + '/pdisk/info': 10, + }, + }), + }); + }); +} + +async function setupNodeInfoMock( + page: Page, + { + datacenter = 'KLG', + rack = 'Rack-A-12', + host = 'storage-node-07.ydb', + }: Omit = {}, +) { + await page.route('**/viewer/json/sysinfo?*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + SystemStateInfo: [ + { + NodeId: Number(NODE_ID), + Host: host, + Roles: ['Storage'], + Location: { + Rack: rack, + DataCenter: datacenter, + }, + }, + ], + }), + }); + }); +} + +async function setupStorageGroupsMock( + page: Page, + {pDiskId = PDISK_ID}: Pick = {}, +) { + await page.route('**/storage/groups?*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(createStorageGroupsResponse({pDiskId})), + }); + }); +} + +async function setupPDiskInfoMock(page: Page) { + await page.route('**/pdisk/info*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + Whiteboard: { + PDisk: { + PDiskId: Number(PDISK_ID), + NodeId: Number(NODE_ID), + Path: '/dev/pdisk0', + Guid: '123456789', + Category: '1', + AvailableSize: '180000000000', + TotalSize: '200000000000', + State: 'Normal', + Device: 'Green', + Realtime: 'Green', + SystemSize: '1000000000', + LogUsedSize: '1000000000', + LogTotalSize: '5000000000', + EnforcedDynamicSlotSize: '20000000000', + ExpectedSlotCount: 4, + NumActiveSlots: 2, + }, + VDisks: [ + { + VDiskId: { + GroupID: Number(GROUP_ID), + GroupGeneration: 1, + Ring: 0, + Domain: 0, + VDisk: 0, + }, + NodeId: Number(NODE_ID), + PDiskId: Number(PDISK_ID), + AllocatedSize: '10000000000', + AvailableSize: '10000000000', + DiskSpace: 'Green', + FrontQueues: 'Green', + StoragePoolName: STORAGE_POOL_NAME, + }, + ], + }, + BSC: { + PDisk: { + Type: 'ROT', + Path: '/dev/pdisk0', + Guid: '123456789', + AvailableSize: '180000000000', + TotalSize: '200000000000', + StatusV2: 'ACTIVE', + EnforcedDynamicSlotSize: '20000000000', + ExpectedSlotCount: 4, + NumActiveSlots: 2, + }, + }, + }), + }); + }); +} + +async function setupVDiskPageMocks(page: Page, options: SetupVDiskPageMocksOptions = {}) { + await setupCapabilitiesMock(page); + await setupNodeInfoMock(page, options); + await setupStorageGroupsMock(page, options); +} + +async function enableNewStorageView(page: Page) { + await page.addInitScript(() => { + localStorage.setItem('enableNewStorageView', JSON.stringify(true)); + }); +} + +test.describe.only('VDisk page storage details', () => { + test('does not render storage details when experiment is disabled', async ({page}) => { + await setupVDiskPageMocks(page); + + await page.goto(VDISK_PAGE_PATH); + + await expect(page.locator('.ydb-vdisk-storage-details')).toHaveCount(0); + await expect(page.locator('.ydb-paginated-table__table')).toBeVisible(); + }); + + test('renders experimental storage details summary', async ({page}) => { + await enableNewStorageView(page); + await setupVDiskPageMocks(page, { + datacenter: LONG_DATACENTER, + rack: LONG_RACK, + host: LONG_HOST, + pDiskId: LONG_PDISK_ID, + }); + await page.goto(VDISK_PAGE_PATH); + + const storageDetails = page.locator('.ydb-vdisk-storage-details'); + + await expect(storageDetails).toBeVisible(); + await expect(storageDetails.getByText('Storage details')).toBeVisible(); + await expect(storageDetails.getByText('Go to PDisk')).toBeVisible(); + await expect( + storageDetails.locator('.ydb-vdisk-storage-details__value-row button'), + ).toHaveCount(3); + await expect(page.locator('.ydb-paginated-table__table')).toBeVisible(); + + await page.setViewportSize({width: 1500, height: 1000}); + await expect(storageDetails).toHaveScreenshot('vdisk-storage-details-wide.png'); + + await storageDetails + .locator('.ydb-vdisk-storage-details__detail') + .first() + .locator('.ydb-cell-with-popover__children-wrapper') + .hover(); + await expect(page.getByText(LONG_DATACENTER, {exact: true})).toBeVisible(); + + await page.setViewportSize({width: 900, height: 1000}); + await expect(storageDetails).toHaveScreenshot('vdisk-storage-details-medium.png'); + + await page.setViewportSize({width: 560, height: 1000}); + await expect(storageDetails).toHaveScreenshot('vdisk-storage-details-narrow.png'); + }); +}); + +test.describe('VDisk page storage tab', () => { + test.beforeEach(async ({page}) => { + await enableNewStorageView(page); + }); + + test('renders storage table in VDisk context', async ({page}) => { + await setupVDiskPageMocks(page); + await page.goto(VDISK_PAGE_PATH); + + const storageDetails = page.locator('.ydb-vdisk-storage-details'); + const storageTable = new ClusterStorageTable(page); + + await expect(storageDetails).toBeVisible(); + await storageTable.waitForTableToLoad(); + await storageTable.waitForTableData(); + + expect(await storageTable.getRowCount()).toBe(1); + + const rowData = await storageTable.getRowData(0); + expect(rowData['Group ID']).toBe(GROUP_ID); + expect(rowData['Pool Name']).toBe(STORAGE_POOL_NAME); + + const disksColumn = page + .locator('.ydb-paginated-table__row') + .first() + .locator('.ydb-storage-vdisks__wrapper'); + await expect(disksColumn).toBeVisible(); + + const activeDisks = disksColumn.locator( + '.storage-disk-progress-bar:not(.storage-disk-progress-bar_inactive)', + ); + const inactiveDisks = disksColumn.locator( + '.storage-disk-progress-bar.storage-disk-progress-bar_inactive', + ); + + await expect(activeDisks).toHaveCount(1); + await expect(inactiveDisks).toHaveCount(2); + }); + + test('Go to PDisk navigates to PDisk page', async ({page}) => { + await setupVDiskPageMocks(page); + await setupPDiskInfoMock(page); + await page.goto(VDISK_PAGE_PATH); + + const storageDetails = page.locator('.ydb-vdisk-storage-details'); + const goToPDiskLink = storageDetails.getByRole('link', {name: 'Go to PDisk'}); + + await expect(goToPDiskLink).toBeVisible(); + await expect(goToPDiskLink).toHaveAttribute( + 'href', + new RegExp(`/pDisk\\?nodeId=${NODE_ID}&pDiskId=${PDISK_ID}$`), + ); + + await goToPDiskLink.click(); + + await expect(page).toHaveURL(new RegExp(`/pDisk\\?nodeId=${NODE_ID}&pDiskId=${PDISK_ID}$`)); + await expect(page.locator('.ydb-pdisk-page')).toBeVisible(); + await expect(page.locator('.ydb-pdisk-page__info')).toBeVisible(); + await expect(page.locator('.ydb-pdisk-space-distribution')).toBeVisible(); + await expect(page.getByText('Space distribution', {exact: true})).toBeVisible(); + }); + + test('renders storage table screenshot in VDisk context', async ({page}) => { + await setupVDiskPageMocks(page); + await page.setViewportSize({width: 1600, height: 1100}); + await page.goto(VDISK_PAGE_PATH); + + const storageTable = new ClusterStorageTable(page); + await storageTable.waitForTableToLoad(); + await storageTable.waitForTableData(); + + await expect(storageTable.getTableContainerLocator()).toHaveScreenshot( + 'vdisk-storage-table-context.png', + ); + }); +}); diff --git a/tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-chromium-linux.png b/tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e6764b6703482fec58b25ccc99c341dd60e8f54e GIT binary patch literal 17253 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 KCCbF~{r?A@RRa3} literal 0 HcmV?d00001 diff --git a/tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-safari-linux.png b/tests/suites/storage/vdiskPage.test.ts-snapshots/vdisk-storage-details-medium-safari-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..3f19062189eb075318e8c85590d06f83ccdc25ef GIT binary patch literal 24918 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≧unVX+xXwb%87 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 s7jWw-2+Rg Date: Wed, 8 Apr 2026 19:27:55 +0300 Subject: [PATCH 2/9] fix: remove only; --- tests/suites/storage/vdiskPage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/suites/storage/vdiskPage.test.ts b/tests/suites/storage/vdiskPage.test.ts index aaffc2ca21..ccffdcd621 100644 --- a/tests/suites/storage/vdiskPage.test.ts +++ b/tests/suites/storage/vdiskPage.test.ts @@ -245,7 +245,7 @@ async function enableNewStorageView(page: Page) { }); } -test.describe.only('VDisk page storage details', () => { +test.describe('VDisk page storage details', () => { test('does not render storage details when experiment is disabled', async ({page}) => { await setupVDiskPageMocks(page); From f29b25d33a7b711fe31b408d714b49171bbfe573 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 8 Apr 2026 20:00:55 +0300 Subject: [PATCH 3/9] fix: tests --- tests/suites/storage/vdiskPage.test.ts | 264 ++----------------------- tests/suites/storage/vdiskPageMocks.ts | 237 ++++++++++++++++++++++ 2 files changed, 251 insertions(+), 250 deletions(-) create mode 100644 tests/suites/storage/vdiskPageMocks.ts diff --git a/tests/suites/storage/vdiskPage.test.ts b/tests/suites/storage/vdiskPage.test.ts index ccffdcd621..e4a413ca8d 100644 --- a/tests/suites/storage/vdiskPage.test.ts +++ b/tests/suites/storage/vdiskPage.test.ts @@ -1,243 +1,21 @@ -import {expect, test} from '@playwright/test'; import type {Page} from '@playwright/test'; +import {expect, test} from '@playwright/test'; import {ClusterStorageTable} from '../paginatedTable/paginatedTable'; -const VDISK_ID = '2181038080-1-0-0-0'; -const SIBLING_VDISK_ID = '2181038080-1-0-0-1'; -const ANOTHER_PDISK_VDISK_ID = '2181038080-1-0-0-2'; -const NODE_ID = '42'; -const PDISK_ID = '1000'; -const SECOND_PDISK_ID = '1001'; -const GROUP_ID = '2181038080'; -const DATABASE = '/local'; -const VDISK_PAGE_PATH = `/vDisk?database=${DATABASE}&nodeId=${NODE_ID}&vDiskId=${VDISK_ID}&activeTab=storage&type=groups`; -const STORAGE_POOL_NAME = 'dynamic_storage_pool:1'; -const LONG_DATACENTER = 'KALUGA Central DC 12 City Region DC 2026'; -const LONG_RACK = 'Rack-A-12-2026A-12-2026'; -const LONG_HOST = 'storage-node-1273683y-1273683y-1273683y.ydb'; -const LONG_PDISK_ID = '1000-1012'; - -interface SetupVDiskPageMocksOptions { - datacenter?: string; - rack?: string; - host?: string; - pDiskId?: string; -} - -function createStorageGroupsResponse({ - pDiskId = PDISK_ID, -}: Pick = {}) { - return { - StorageGroups: [ - { - GroupId: GROUP_ID, - PoolName: STORAGE_POOL_NAME, - MediaType: 'SSD', - ErasureSpecies: 'mirror-3-dc', - Used: '10000000000', - Limit: '196000000000', - Available: '186000000000', - State: 'ok', - VDisks: [ - { - VDiskId: VDISK_ID, - NodeId: Number(NODE_ID), - VDiskSlotId: 1001, - AllocatedSize: '10000000000', - AvailableSize: '186000000000', - StoragePoolName: STORAGE_POOL_NAME, - DiskSpace: 'Green', - FrontQueues: 'Green', - SatisfactionRank: { - FreshRank: { - Flag: 'Green', - }, - LevelRank: { - Flag: 'Green', - }, - }, - PDisk: { - PDiskId: `${NODE_ID}-${pDiskId}`, - Type: 'ROT', - }, - }, - { - VDiskId: SIBLING_VDISK_ID, - NodeId: Number(NODE_ID), - VDiskSlotId: 1002, - AllocatedSize: '9000000000', - AvailableSize: '187000000000', - StoragePoolName: STORAGE_POOL_NAME, - DiskSpace: 'Green', - FrontQueues: 'Green', - SatisfactionRank: { - FreshRank: { - Flag: 'Green', - }, - LevelRank: { - Flag: 'Green', - }, - }, - PDisk: { - PDiskId: `${NODE_ID}-${pDiskId}`, - Type: 'ROT', - }, - }, - { - VDiskId: ANOTHER_PDISK_VDISK_ID, - NodeId: Number(NODE_ID), - VDiskSlotId: 1003, - AllocatedSize: '8000000000', - AvailableSize: '188000000000', - StoragePoolName: STORAGE_POOL_NAME, - DiskSpace: 'Green', - FrontQueues: 'Green', - SatisfactionRank: { - FreshRank: { - Flag: 'Green', - }, - LevelRank: { - Flag: 'Green', - }, - }, - PDisk: { - PDiskId: `${NODE_ID}-${SECOND_PDISK_ID}`, - Type: 'ROT', - }, - }, - ], - }, - ], - }; -} - -async function setupCapabilitiesMock(page: Page) { - await page.route('**/viewer/capabilities*', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - Database: DATABASE, - Capabilities: { - '/storage/groups': 10, - '/viewer/nodes': 20, - '/pdisk/info': 10, - }, - }), - }); - }); -} - -async function setupNodeInfoMock( - page: Page, - { - datacenter = 'KLG', - rack = 'Rack-A-12', - host = 'storage-node-07.ydb', - }: Omit = {}, -) { - await page.route('**/viewer/json/sysinfo?*', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - SystemStateInfo: [ - { - NodeId: Number(NODE_ID), - Host: host, - Roles: ['Storage'], - Location: { - Rack: rack, - DataCenter: datacenter, - }, - }, - ], - }), - }); - }); -} - -async function setupStorageGroupsMock( - page: Page, - {pDiskId = PDISK_ID}: Pick = {}, -) { - await page.route('**/storage/groups?*', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(createStorageGroupsResponse({pDiskId})), - }); - }); -} - -async function setupPDiskInfoMock(page: Page) { - await page.route('**/pdisk/info*', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - Whiteboard: { - PDisk: { - PDiskId: Number(PDISK_ID), - NodeId: Number(NODE_ID), - Path: '/dev/pdisk0', - Guid: '123456789', - Category: '1', - AvailableSize: '180000000000', - TotalSize: '200000000000', - State: 'Normal', - Device: 'Green', - Realtime: 'Green', - SystemSize: '1000000000', - LogUsedSize: '1000000000', - LogTotalSize: '5000000000', - EnforcedDynamicSlotSize: '20000000000', - ExpectedSlotCount: 4, - NumActiveSlots: 2, - }, - VDisks: [ - { - VDiskId: { - GroupID: Number(GROUP_ID), - GroupGeneration: 1, - Ring: 0, - Domain: 0, - VDisk: 0, - }, - NodeId: Number(NODE_ID), - PDiskId: Number(PDISK_ID), - AllocatedSize: '10000000000', - AvailableSize: '10000000000', - DiskSpace: 'Green', - FrontQueues: 'Green', - StoragePoolName: STORAGE_POOL_NAME, - }, - ], - }, - BSC: { - PDisk: { - Type: 'ROT', - Path: '/dev/pdisk0', - Guid: '123456789', - AvailableSize: '180000000000', - TotalSize: '200000000000', - StatusV2: 'ACTIVE', - EnforcedDynamicSlotSize: '20000000000', - ExpectedSlotCount: 4, - NumActiveSlots: 2, - }, - }, - }), - }); - }); -} - -async function setupVDiskPageMocks(page: Page, options: SetupVDiskPageMocksOptions = {}) { - await setupCapabilitiesMock(page); - await setupNodeInfoMock(page, options); - await setupStorageGroupsMock(page, options); -} +import { + GROUP_ID, + LONG_DATACENTER, + LONG_HOST, + LONG_PDISK_ID, + LONG_RACK, + NODE_ID, + PDISK_ID, + STORAGE_POOL_NAME, + VDISK_PAGE_PATH, + setupPDiskInfoMock, + setupVDiskPageMocks, +} from './vdiskPageMocks'; async function enableNewStorageView(page: Page) { await page.addInitScript(() => { @@ -354,18 +132,4 @@ test.describe('VDisk page storage tab', () => { await expect(page.locator('.ydb-pdisk-space-distribution')).toBeVisible(); await expect(page.getByText('Space distribution', {exact: true})).toBeVisible(); }); - - test('renders storage table screenshot in VDisk context', async ({page}) => { - await setupVDiskPageMocks(page); - await page.setViewportSize({width: 1600, height: 1100}); - await page.goto(VDISK_PAGE_PATH); - - const storageTable = new ClusterStorageTable(page); - await storageTable.waitForTableToLoad(); - await storageTable.waitForTableData(); - - await expect(storageTable.getTableContainerLocator()).toHaveScreenshot( - 'vdisk-storage-table-context.png', - ); - }); }); diff --git a/tests/suites/storage/vdiskPageMocks.ts b/tests/suites/storage/vdiskPageMocks.ts new file mode 100644 index 0000000000..25b8838383 --- /dev/null +++ b/tests/suites/storage/vdiskPageMocks.ts @@ -0,0 +1,237 @@ +import type {Page} from '@playwright/test'; + +export const VDISK_ID = '2181038080-1-0-0-0'; +export const SIBLING_VDISK_ID = '2181038080-1-0-0-1'; +export const ANOTHER_PDISK_VDISK_ID = '2181038080-1-0-0-2'; +export const NODE_ID = '42'; +export const PDISK_ID = '1000'; +export const SECOND_PDISK_ID = '1001'; +export const GROUP_ID = '2181038080'; +export const DATABASE = '/local'; +export const VDISK_PAGE_PATH = `/vDisk?database=${DATABASE}&nodeId=${NODE_ID}&vDiskId=${VDISK_ID}&activeTab=storage&type=groups`; +export const STORAGE_POOL_NAME = 'dynamic_storage_pool:1'; +export const LONG_DATACENTER = 'KALUGA Central DC 12 City Region DC 2026'; +export const LONG_RACK = 'Rack-A-12-2026A-12-2026'; +export const LONG_HOST = 'storage-node-1273683y-1273683y-1273683y.ydb'; +export const LONG_PDISK_ID = '1000-1012'; + +export interface SetupVDiskPageMocksOptions { + datacenter?: string; + rack?: string; + host?: string; + pDiskId?: string; +} + +function createStorageGroupsResponse({ + pDiskId = PDISK_ID, +}: Pick = {}) { + return { + StorageGroups: [ + { + GroupId: GROUP_ID, + PoolName: STORAGE_POOL_NAME, + MediaType: 'SSD', + ErasureSpecies: 'mirror-3-dc', + Used: '10000000000', + Limit: '196000000000', + Available: '186000000000', + State: 'ok', + VDisks: [ + { + VDiskId: VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1001, + AllocatedSize: '10000000000', + AvailableSize: '186000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${pDiskId}`, + Type: 'ROT', + }, + }, + { + VDiskId: SIBLING_VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1002, + AllocatedSize: '9000000000', + AvailableSize: '187000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${pDiskId}`, + Type: 'ROT', + }, + }, + { + VDiskId: ANOTHER_PDISK_VDISK_ID, + NodeId: Number(NODE_ID), + VDiskSlotId: 1003, + AllocatedSize: '8000000000', + AvailableSize: '188000000000', + StoragePoolName: STORAGE_POOL_NAME, + DiskSpace: 'Green', + FrontQueues: 'Green', + SatisfactionRank: { + FreshRank: { + Flag: 'Green', + }, + LevelRank: { + Flag: 'Green', + }, + }, + PDisk: { + PDiskId: `${NODE_ID}-${SECOND_PDISK_ID}`, + Type: 'ROT', + }, + }, + ], + }, + ], + }; +} + +async function setupCapabilitiesMock(page: Page) { + await page.route('**/viewer/capabilities*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + Database: DATABASE, + Capabilities: { + '/storage/groups': 10, + '/viewer/nodes': 20, + '/pdisk/info': 10, + }, + }), + }); + }); +} + +async function setupNodeInfoMock( + page: Page, + { + datacenter = 'KLG', + rack = 'Rack-A-12', + host = 'storage-node-07.ydb', + }: Omit = {}, +) { + await page.route('**/viewer/json/sysinfo?*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + SystemStateInfo: [ + { + NodeId: Number(NODE_ID), + Host: host, + Roles: ['Storage'], + Location: { + Rack: rack, + DataCenter: datacenter, + }, + }, + ], + }), + }); + }); +} + +async function setupStorageGroupsMock( + page: Page, + {pDiskId = PDISK_ID}: Pick = {}, +) { + await page.route('**/storage/groups?*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(createStorageGroupsResponse({pDiskId})), + }); + }); +} + +export async function setupPDiskInfoMock(page: Page) { + await page.route('**/pdisk/info*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + Whiteboard: { + PDisk: { + PDiskId: Number(PDISK_ID), + NodeId: Number(NODE_ID), + Path: '/dev/pdisk0', + Guid: '123456789', + Category: '1', + AvailableSize: '180000000000', + TotalSize: '200000000000', + State: 'Normal', + Device: 'Green', + Realtime: 'Green', + SystemSize: '1000000000', + LogUsedSize: '1000000000', + LogTotalSize: '5000000000', + EnforcedDynamicSlotSize: '20000000000', + ExpectedSlotCount: 4, + NumActiveSlots: 2, + }, + VDisks: [ + { + VDiskId: { + GroupID: Number(GROUP_ID), + GroupGeneration: 1, + Ring: 0, + Domain: 0, + VDisk: 0, + }, + NodeId: Number(NODE_ID), + PDiskId: Number(PDISK_ID), + AllocatedSize: '10000000000', + AvailableSize: '10000000000', + DiskSpace: 'Green', + FrontQueues: 'Green', + StoragePoolName: STORAGE_POOL_NAME, + }, + ], + }, + BSC: { + PDisk: { + Type: 'ROT', + Path: '/dev/pdisk0', + Guid: '123456789', + AvailableSize: '180000000000', + TotalSize: '200000000000', + StatusV2: 'ACTIVE', + EnforcedDynamicSlotSize: '20000000000', + ExpectedSlotCount: 4, + NumActiveSlots: 2, + }, + }, + }), + }); + }); +} + +export async function setupVDiskPageMocks(page: Page, options: SetupVDiskPageMocksOptions = {}) { + await setupCapabilitiesMock(page); + await setupNodeInfoMock(page, options); + await setupStorageGroupsMock(page, options); +} From 5756072823208f517b3397b1e1aef703409a232d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 8 Apr 2026 20:22:49 +0300 Subject: [PATCH 4/9] fix: link --- .../VDiskPage/VDiskStorageDetails.tsx | 2 +- src/routes.test.ts | 31 +++++++++++++++++++ src/routes.ts | 3 +- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/routes.test.ts diff --git a/src/containers/VDiskPage/VDiskStorageDetails.tsx b/src/containers/VDiskPage/VDiskStorageDetails.tsx index 15e20613c4..0f1b4a7511 100644 --- a/src/containers/VDiskPage/VDiskStorageDetails.tsx +++ b/src/containers/VDiskPage/VDiskStorageDetails.tsx @@ -166,7 +166,7 @@ export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps) const pDiskPath = NodeId !== undefined && PDiskId !== undefined - ? getPDiskPagePath(PDiskId, NodeId) + ? getPDiskPagePath(PDiskId, NodeId, undefined, {withBasename: true}) : undefined; return ( diff --git a/src/routes.test.ts b/src/routes.test.ts new file mode 100644 index 0000000000..715141087c --- /dev/null +++ b/src/routes.test.ts @@ -0,0 +1,31 @@ +jest.mock('./store', () => ({ + backend: undefined, + basename: '/monitoring', + clusterName: undefined, + environment: undefined, + webVersion: false, +})); + +jest.mock('./uiFactory/uiFactory', () => ({ + uiFactory: {clustersDomain: undefined}, +})); + +jest.mock('./utils/hooks/useDatabaseFromQuery', () => ({ + useDatabaseFromQuery: jest.fn(), +})); + +import {getPDiskPagePath} from './routes'; + +describe('routes', () => { + describe('getPDiskPagePath', () => { + test('should create pDisk path without basename by default', () => { + expect(getPDiskPagePath(1001, 4)).toBe('/pDisk?nodeId=4&pDiskId=1001'); + }); + + test('should prepend basename when requested for href-based links', () => { + expect(getPDiskPagePath(1001, 4, undefined, {withBasename: true})).toBe( + '/monitoring/pDisk?nodeId=4&pDiskId=1001', + ); + }); + }); +}); diff --git a/src/routes.ts b/src/routes.ts index af6dc05226..c539569624 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -148,8 +148,9 @@ export function getPDiskPagePath( pDiskId: string | number, nodeId: string | number, query: Query = {}, + options?: CreateHrefOptions, ) { - return createHref(routes.pDisk, undefined, {...query, nodeId, pDiskId}); + return createHref(routes.pDisk, undefined, {...query, nodeId, pDiskId}, options); } export function getStorageGroupPath(groupId: string | number, query: Query = {}) { From 10e82109fdb77f729e2d40890a6ac3a792a3e6db Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 8 Apr 2026 22:25:27 +0300 Subject: [PATCH 5/9] fix: review --- .../VDiskPage/VDiskStorageDetails.tsx | 23 +---------- .../__tests__/prepareGroupsDisks.test.ts | 21 ++++++++++ src/utils/disks/__test__/prepareDisks.test.ts | 23 +++++++++++ src/utils/disks/prepareDisks.ts | 40 ++++++++++++++++++- src/utils/disks/types.ts | 1 + 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/containers/VDiskPage/VDiskStorageDetails.tsx b/src/containers/VDiskPage/VDiskStorageDetails.tsx index 0f1b4a7511..234f2be466 100644 --- a/src/containers/VDiskPage/VDiskStorageDetails.tsx +++ b/src/containers/VDiskPage/VDiskStorageDetails.tsx @@ -57,27 +57,6 @@ function formatUsage(value?: number) { return formattedValue ? `${formattedValue}%` : EMPTY_DATA_PLACEHOLDER; } -function getFreeSize(availableSize?: number, sizeLimit?: number, allocatedSize?: number) { - const normalizedAvailableSize = Number(availableSize); - - if (Number.isFinite(normalizedAvailableSize) && normalizedAvailableSize >= 0) { - return normalizedAvailableSize; - } - - const normalizedSizeLimit = Number(sizeLimit); - const normalizedAllocatedSize = Number(allocatedSize); - - if (!Number.isFinite(normalizedSizeLimit) || normalizedSizeLimit < 0) { - return undefined; - } - - if (!Number.isFinite(normalizedAllocatedSize) || normalizedAllocatedSize < 0) { - return normalizedSizeLimit; - } - - return Math.max(normalizedSizeLimit - normalizedAllocatedSize, 0); -} - function MetricItem({title, value, isUsage}: MetricItemProps) { return (
@@ -160,7 +139,7 @@ export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps) const used = Number(data?.AllocatedSize); const total = Number(data?.SizeLimit); const usage = Number(data?.AllocatedPercent); - const free = getFreeSize(data?.AvailableSize, data?.SizeLimit, data?.AllocatedSize); + const free = Number(data?.FreeSize); const {NodeDC, NodeRack, NodeHost, NodeId, PDiskId} = data || {}; diff --git a/src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts b/src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts index 315fee55e5..2016cbd490 100644 --- a/src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts +++ b/src/store/reducers/storage/__tests__/prepareGroupsDisks.test.ts @@ -92,6 +92,7 @@ describe('prepareGroupsVDisk', () => { AllocatedSize: 30943477760, AvailableSize: 234461593600, SizeLimit: 265405071360, + FreeSize: 234461593600, AllocatedPercent: 11, Donors: undefined, @@ -135,6 +136,7 @@ describe('prepareGroupsVDisk', () => { AllocatedSize: 30943477760, AvailableSize: 234461593600, SizeLimit: 265405071360, + FreeSize: 234461593600, AllocatedPercent: 11, Donors: undefined, @@ -241,6 +243,7 @@ describe('prepareGroupsVDisk', () => { AllocatedSize: 30943477760, AvailableSize: 234461593600, SizeLimit: 265405071360, + FreeSize: 234461593600, AllocatedPercent: 11, Donors: undefined, @@ -263,6 +266,24 @@ describe('prepareGroupsVDisk', () => { expect(preparedData).toEqual(expectedResult); }); + + test('Should derive FreeSize from PDisk slot size fallback when AvailableSize is 0', () => { + const vDiskData = { + VDiskId: '2181038134-22-0-0-0', + NodeId: 224, + AllocatedSize: '300', + AvailableSize: '0', + PDisk: { + SlotSize: '500', + }, + } as const as TStorageVDisk; + + const preparedData = prepareGroupsVDisk(vDiskData); + + expect(preparedData.SizeLimit).toBe(500); + expect(preparedData.FreeSize).toBe(200); + expect(preparedData.AllocatedPercent).toBe(60); + }); }); describe('prepareGroupsPDisk', () => { diff --git a/src/utils/disks/__test__/prepareDisks.test.ts b/src/utils/disks/__test__/prepareDisks.test.ts index f58afcd9c3..d93eb30f7f 100644 --- a/src/utils/disks/__test__/prepareDisks.test.ts +++ b/src/utils/disks/__test__/prepareDisks.test.ts @@ -91,6 +91,7 @@ describe('prepareWhiteboardVDiskData', () => { AvailableSize: 188523479040, AllocatedSize: 8996782080, SizeLimit: 197520261120, + FreeSize: 188523479040, AllocatedPercent: 4, }; @@ -191,6 +192,7 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 400, AllocatedSize: 100, SizeLimit: 500, // allocated (100) + available (400) = 500 + FreeSize: 400, AllocatedPercent: 20, // 100 / 500 * 100 = 20% }); }); @@ -206,6 +208,7 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 0, AllocatedSize: 500, SizeLimit: 500, // SlotSize is used when available is 0 + FreeSize: 0, AllocatedPercent: 100, // 500 / 500 * 100 = 100% }); }); @@ -221,10 +224,27 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 0, AllocatedSize: 300, SizeLimit: 500, // SlotSize is used when available is undefined + FreeSize: 200, AllocatedPercent: 60, // 300 / 500 * 100 = 60% }); }); + test('Should derive free size from slot size fallback when AvailableSize is 0', () => { + expect( + prepareVDiskSizeFields({ + AvailableSize: '0', + AllocatedSize: '300', + SlotSize: '500', + }), + ).toEqual({ + AvailableSize: 0, + AllocatedSize: 300, + SizeLimit: 500, + FreeSize: 200, + AllocatedPercent: 60, + }); + }); + test('Should use allocated when SlotSize is undefined and available is 0', () => { expect( prepareVDiskSizeFields({ @@ -236,6 +256,7 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 0, AllocatedSize: 500, SizeLimit: 500, // allocated (500) + FreeSize: 0, AllocatedPercent: 100, // 500 / 500 * 100 = 100% }); }); @@ -251,6 +272,7 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 0, AllocatedSize: 800, SizeLimit: 500, // SlotSize is used as limit + FreeSize: 0, AllocatedPercent: 160, // 800 / 500 * 100 = 160% }); }); @@ -266,6 +288,7 @@ describe('prepareVDiskSizeFields', () => { AvailableSize: 0, AllocatedSize: NaN, SizeLimit: NaN, + FreeSize: NaN, AllocatedPercent: NaN, }); }); diff --git a/src/utils/disks/prepareDisks.ts b/src/utils/disks/prepareDisks.ts index 63d059613c..9c35d7fff2 100644 --- a/src/utils/disks/prepareDisks.ts +++ b/src/utils/disks/prepareDisks.ts @@ -155,24 +155,62 @@ export function prepareVDiskSizeFields({ // Unlike available, allocated is displayed in UI, it is incorrect to fallback it to 0 const allocated = Number(AllocatedSize); const slotSize = Number(SlotSize); + const hasSizeLimitFallback = !available && Boolean(slotSize); let sizeLimit = allocated + available; // If no available size or available size is 0, slot size should be used as limit - if (!available && slotSize) { + if (hasSizeLimitFallback) { sizeLimit = slotSize; } + const freeSize = getVDiskFreeSize({ + available, + allocated, + sizeLimit, + hasSizeLimitFallback, + }); const allocatedPercent = sizeLimit > 0 ? Math.floor((allocated * 100) / sizeLimit) : NaN; return { AvailableSize: available, AllocatedSize: allocated, SizeLimit: sizeLimit, + FreeSize: freeSize, AllocatedPercent: allocatedPercent, }; } +function getVDiskFreeSize({ + available, + allocated, + sizeLimit, + hasSizeLimitFallback, +}: { + available: number; + allocated: number; + sizeLimit: number; + hasSizeLimitFallback: boolean; +}) { + if (!Number.isFinite(sizeLimit) || sizeLimit < 0) { + return NaN; + } + + if (!Number.isFinite(allocated) || allocated < 0) { + return sizeLimit; + } + + if (hasSizeLimitFallback) { + return Math.max(sizeLimit - allocated, 0); + } + + if (Number.isFinite(available) && available >= 0) { + return available; + } + + return Math.max(sizeLimit - allocated, 0); +} + export function preparePDiskSizeFields({ AvailableSize, TotalSize, diff --git a/src/utils/disks/types.ts b/src/utils/disks/types.ts index 841414700a..76e9275a8c 100644 --- a/src/utils/disks/types.ts +++ b/src/utils/disks/types.ts @@ -36,6 +36,7 @@ export interface PreparedVDisk AllocatedSize?: number; AllocatedPercent?: number; SizeLimit?: number; + FreeSize?: number; Donors?: PreparedVDisk[]; From b64a13b003debdb3910eb16f9a1a6438e8761408 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 9 Apr 2026 00:04:17 +0300 Subject: [PATCH 6/9] fix: use common formatters --- .../StorageUsage/storageUsageFormatters.ts | 58 ++--------------- .../VDiskPage/VDiskStorageDetails.tsx | 33 +++------- src/utils/__test__/storageMetrics.test.ts | 28 ++++++++ src/utils/storageMetrics.ts | 64 +++++++++++++++++++ 4 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 src/utils/__test__/storageMetrics.test.ts create mode 100644 src/utils/storageMetrics.ts diff --git a/src/containers/Tenant/Diagnostics/StorageUsage/storageUsageFormatters.ts b/src/containers/Tenant/Diagnostics/StorageUsage/storageUsageFormatters.ts index 89808b9c45..6cbb8dabf2 100644 --- a/src/containers/Tenant/Diagnostics/StorageUsage/storageUsageFormatters.ts +++ b/src/containers/Tenant/Diagnostics/StorageUsage/storageUsageFormatters.ts @@ -1,7 +1,9 @@ -import type {BytesSizes} from '../../../../utils/bytesParsers'; -import {getBytesSizeUnit, sizes} from '../../../../utils/bytesParsers'; -import {EMPTY_DATA_PLACEHOLDER, UNBREAKABLE_GAP} from '../../../../utils/constants'; +import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants'; import {formatNumber, formatPercent} from '../../../../utils/dataFormatters/dataFormatters'; +import { + formatMetricBytes as formatMetricBytesShared, + getConsistentMetricBytesSize as getConsistentMetricBytesSizeShared, +} from '../../../../utils/storageMetrics'; export const STORAGE_USAGE_INITIAL_ROWS_COUNT = 3; @@ -10,54 +12,8 @@ const MIN_SHARE_PERCENT_WITH_FRACTION = 1; const PERCENT_MULTIPLIER = 100; const MAX_PROGRESS_PERCENT = 100; -function getMetricBytesDecimalPlaces(size: BytesSizes, convertedValue: number) { - if (size === 'b' || size === 'kb' || size === 'mb') { - return convertedValue % 1 === 0 ? 0 : 1; - } - if (size === 'gb') { - if (convertedValue < 1) { - return 2; - } - return convertedValue % 1 === 0 ? 0 : 1; - } - // TB and PB: <10 → 2 decimal places, 10+ → 1 decimal place - if (convertedValue < 10) { - return 2; - } - return 1; -} - -export function getConsistentMetricBytesSize(values: Array) { - const maxValue = values.reduce((currentMaxValue, value) => { - const numericValue = Number(value); - - if (!Number.isFinite(numericValue) || numericValue < 0) { - return currentMaxValue; - } - - return Math.max(currentMaxValue, numericValue); - }, 0); - - return getBytesSizeUnit(maxValue); -} - -export function formatMetricBytes(value?: string | number, size?: BytesSizes) { - const numericValue = Number(value); - - if (!Number.isFinite(numericValue)) { - return EMPTY_DATA_PLACEHOLDER; - } - - const resolvedSize = size ?? getBytesSizeUnit(numericValue); - const convertedValue = numericValue / sizes[resolvedSize].value; - const decimalPlaces = getMetricBytesDecimalPlaces(resolvedSize, convertedValue); - const rounded = Number(convertedValue.toFixed(decimalPlaces)); - const formatted = formatNumber(rounded); - - return formatted - ? `${formatted}${UNBREAKABLE_GAP}${sizes[resolvedSize].label}` - : EMPTY_DATA_PLACEHOLDER; -} +export const getConsistentMetricBytesSize = getConsistentMetricBytesSizeShared; +export const formatMetricBytes = formatMetricBytesShared; export function formatOverhead( diskUsage: number | undefined, diff --git a/src/containers/VDiskPage/VDiskStorageDetails.tsx b/src/containers/VDiskPage/VDiskStorageDetails.tsx index 234f2be466..1797e3bccb 100644 --- a/src/containers/VDiskPage/VDiskStorageDetails.tsx +++ b/src/containers/VDiskPage/VDiskStorageDetails.tsx @@ -6,7 +6,11 @@ import {getPDiskPagePath} from '../../routes'; import type {VDiskData} from '../../store/reducers/vdisk/types'; import {cn} from '../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; -import {formatNumber, formatStorageValuesToGb} from '../../utils/dataFormatters/dataFormatters'; +import { + formatMetricBytes, + formatMetricPercent, + getConsistentMetricBytesSize, +} from '../../utils/storageMetrics'; import {vDiskPageKeyset} from './i18n'; @@ -39,24 +43,6 @@ interface TruncatedDetailValueProps { value?: string | number; } -function formatStorageMetric(value?: number) { - const [formattedValue] = formatStorageValuesToGb(value); - - return formattedValue || EMPTY_DATA_PLACEHOLDER; -} - -function formatUsage(value?: number) { - const numericValue = Number(value); - - if (!Number.isFinite(numericValue)) { - return EMPTY_DATA_PLACEHOLDER; - } - - const formattedValue = formatNumber(numericValue); - - return formattedValue ? `${formattedValue}%` : EMPTY_DATA_PLACEHOLDER; -} - function MetricItem({title, value, isUsage}: MetricItemProps) { return (
@@ -140,6 +126,7 @@ export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps) const total = Number(data?.SizeLimit); const usage = Number(data?.AllocatedPercent); const free = Number(data?.FreeSize); + const metricsSize = getConsistentMetricBytesSize([used, total, free]); const {NodeDC, NodeRack, NodeHost, NodeId, PDiskId} = data || {}; @@ -157,19 +144,19 @@ export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps)
diff --git a/src/utils/__test__/storageMetrics.test.ts b/src/utils/__test__/storageMetrics.test.ts new file mode 100644 index 0000000000..f91682b218 --- /dev/null +++ b/src/utils/__test__/storageMetrics.test.ts @@ -0,0 +1,28 @@ +import {EMPTY_DATA_PLACEHOLDER, UNBREAKABLE_GAP} from '../constants'; +import { + formatMetricBytes, + formatMetricPercent, + getConsistentMetricBytesSize, +} from '../storageMetrics'; + +describe('storageMetrics', () => { + test('getConsistentMetricBytesSize chooses a shared unit from the largest metric', () => { + expect(getConsistentMetricBytesSize([521_000_000, 1_230_000_000, 410_000_000])).toBe('gb'); + }); + + test('formatMetricBytes keeps related values in the provided shared unit', () => { + expect(formatMetricBytes(521_000_000, 'gb')).toBe(`0.52${UNBREAKABLE_GAP}GB`); + }); + + test('formatMetricPercent keeps integer usage values without decimals', () => { + expect(formatMetricPercent(50)).toBe('50%'); + }); + + test('formatMetricPercent keeps one decimal for non-integer values', () => { + expect(formatMetricPercent(64.2)).toBe('64.2%'); + }); + + test('formatMetricPercent returns placeholder for invalid values', () => { + expect(formatMetricPercent(undefined)).toBe(EMPTY_DATA_PLACEHOLDER); + }); +}); diff --git a/src/utils/storageMetrics.ts b/src/utils/storageMetrics.ts new file mode 100644 index 0000000000..6cf6a3141f --- /dev/null +++ b/src/utils/storageMetrics.ts @@ -0,0 +1,64 @@ +import type {BytesSizes} from './bytesParsers'; +import {getBytesSizeUnit, sizes} from './bytesParsers'; +import {EMPTY_DATA_PLACEHOLDER, UNBREAKABLE_GAP} from './constants'; +import {formatNumber, formatPercent} from './dataFormatters/dataFormatters'; + +function getMetricBytesDecimalPlaces(size: BytesSizes, convertedValue: number) { + if (size === 'b' || size === 'kb' || size === 'mb') { + return convertedValue % 1 === 0 ? 0 : 1; + } + if (size === 'gb') { + if (convertedValue < 1) { + return 2; + } + return convertedValue % 1 === 0 ? 0 : 1; + } + if (convertedValue < 10) { + return 2; + } + return 1; +} + +export function getConsistentMetricBytesSize(values: Array) { + const maxValue = values.reduce((currentMaxValue, value) => { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue) || numericValue < 0) { + return currentMaxValue; + } + + return Math.max(currentMaxValue, numericValue); + }, 0); + + return getBytesSizeUnit(maxValue); +} + +export function formatMetricBytes(value?: string | number, size?: BytesSizes) { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) { + return EMPTY_DATA_PLACEHOLDER; + } + + const resolvedSize = size ?? getBytesSizeUnit(numericValue); + const convertedValue = numericValue / sizes[resolvedSize].value; + const decimalPlaces = getMetricBytesDecimalPlaces(resolvedSize, convertedValue); + const rounded = Number(convertedValue.toFixed(decimalPlaces)); + const formatted = formatNumber(rounded); + + return formatted + ? `${formatted}${UNBREAKABLE_GAP}${sizes[resolvedSize].label}` + : EMPTY_DATA_PLACEHOLDER; +} + +export function formatMetricPercent(value?: number) { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue) || numericValue < 0) { + return EMPTY_DATA_PLACEHOLDER; + } + + const precision = Number.isInteger(numericValue) ? 0 : 1; + + return formatPercent(numericValue / 100, precision); +} From 09e60bb96f5e795cc1cf23d75be483d5331f5d6f Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sat, 11 Apr 2026 11:22:48 +0300 Subject: [PATCH 7/9] fix: use Divider --- src/containers/VDiskPage/VDiskStorageDetails.scss | 7 +------ src/containers/VDiskPage/VDiskStorageDetails.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/containers/VDiskPage/VDiskStorageDetails.scss b/src/containers/VDiskPage/VDiskStorageDetails.scss index 59dd4ff736..766b70f996 100644 --- a/src/containers/VDiskPage/VDiskStorageDetails.scss +++ b/src/containers/VDiskPage/VDiskStorageDetails.scss @@ -72,12 +72,6 @@ padding-right: var(--g-spacing-4); } - &__metric_usage { - padding-left: var(--g-spacing-5); - - border-left: 1px solid var(--g-color-line-generic); - } - &__detail { flex: 0 1 auto; @@ -103,6 +97,7 @@ width: 100%; min-width: 0; + height: 20px; &_copyable { width: auto; diff --git a/src/containers/VDiskPage/VDiskStorageDetails.tsx b/src/containers/VDiskPage/VDiskStorageDetails.tsx index 1797e3bccb..a6eb93e662 100644 --- a/src/containers/VDiskPage/VDiskStorageDetails.tsx +++ b/src/containers/VDiskPage/VDiskStorageDetails.tsx @@ -1,4 +1,4 @@ -import {Button, Flex, Text} from '@gravity-ui/uikit'; +import {Button, Divider, Flex, Text} from '@gravity-ui/uikit'; import {CellWithPopover} from '../../components/CellWithPopover/CellWithPopover'; import {ClipboardButton} from '../../components/ClipboardButton/ClipboardButton'; @@ -26,7 +26,6 @@ interface VDiskStorageDetailsProps { interface MetricItemProps { title: string; value: string; - isUsage?: boolean; } interface CopyableDetailItemProps { @@ -43,9 +42,9 @@ interface TruncatedDetailValueProps { value?: string | number; } -function MetricItem({title, value, isUsage}: MetricItemProps) { +function MetricItem({title, value}: MetricItemProps) { return ( -
+
{value} @@ -112,6 +111,7 @@ function CopyableDetailItem({title, value}: CopyableDetailItemProps) { copyText={normalizedValue} withLabel={false} view="flat-secondary" + size="s" /> @@ -154,10 +154,10 @@ export function VDiskStorageDetails({className, data}: VDiskStorageDetailsProps) title={vDiskPageKeyset('field_storage-details-free')} value={formatMetricBytes(free, metricsSize)} /> +
From 2f90b0d213d879068bea30bee773c29987d4e646 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sat, 11 Apr 2026 11:43:53 +0300 Subject: [PATCH 8/9] fix: review --- src/utils/disks/__test__/prepareDisks.test.ts | 16 ++++++++++++++++ src/utils/disks/prepareDisks.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utils/disks/__test__/prepareDisks.test.ts b/src/utils/disks/__test__/prepareDisks.test.ts index d93eb30f7f..bdcfbcb262 100644 --- a/src/utils/disks/__test__/prepareDisks.test.ts +++ b/src/utils/disks/__test__/prepareDisks.test.ts @@ -245,6 +245,22 @@ describe('prepareVDiskSizeFields', () => { }); }); + test('Should return unknown free size when SlotSize fallback is used with invalid AllocatedSize', () => { + expect( + prepareVDiskSizeFields({ + AvailableSize: '0', + AllocatedSize: undefined, + SlotSize: '500', + }), + ).toEqual({ + AvailableSize: 0, + AllocatedSize: NaN, + SizeLimit: 500, + FreeSize: NaN, + AllocatedPercent: NaN, + }); + }); + test('Should use allocated when SlotSize is undefined and available is 0', () => { expect( prepareVDiskSizeFields({ diff --git a/src/utils/disks/prepareDisks.ts b/src/utils/disks/prepareDisks.ts index 9c35d7fff2..23fc3711c1 100644 --- a/src/utils/disks/prepareDisks.ts +++ b/src/utils/disks/prepareDisks.ts @@ -197,7 +197,7 @@ function getVDiskFreeSize({ } if (!Number.isFinite(allocated) || allocated < 0) { - return sizeLimit; + return hasSizeLimitFallback ? NaN : sizeLimit; } if (hasSizeLimitFallback) { From 5fb5aba0ccc09d0993a0bf621e2f985d92b278ad Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sat, 11 Apr 2026 12:13:29 +0300 Subject: [PATCH 9/9] fix: review --- src/utils/disks/__test__/prepareDisks.test.ts | 16 ++++++++++++++++ src/utils/disks/prepareDisks.ts | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/utils/disks/__test__/prepareDisks.test.ts b/src/utils/disks/__test__/prepareDisks.test.ts index bdcfbcb262..b7e74e2c41 100644 --- a/src/utils/disks/__test__/prepareDisks.test.ts +++ b/src/utils/disks/__test__/prepareDisks.test.ts @@ -261,6 +261,22 @@ describe('prepareVDiskSizeFields', () => { }); }); + test('Should preserve known free size when AllocatedSize is invalid outside SlotSize fallback', () => { + expect( + prepareVDiskSizeFields({ + AvailableSize: '400', + AllocatedSize: undefined, + SlotSize: '500', + }), + ).toEqual({ + AvailableSize: 400, + AllocatedSize: NaN, + SizeLimit: NaN, + FreeSize: 400, + AllocatedPercent: NaN, + }); + }); + test('Should use allocated when SlotSize is undefined and available is 0', () => { expect( prepareVDiskSizeFields({ diff --git a/src/utils/disks/prepareDisks.ts b/src/utils/disks/prepareDisks.ts index 23fc3711c1..39cecac7a9 100644 --- a/src/utils/disks/prepareDisks.ts +++ b/src/utils/disks/prepareDisks.ts @@ -3,6 +3,7 @@ import {isNil} from 'lodash'; import type {TPDiskStateInfo} from '../../types/api/pdisk'; import type {TVDiskStateInfo, TVSlotId} from '../../types/api/vdisk'; import {stringifyVdiskId} from '../dataFormatters/dataFormatters'; +import {isNumeric} from '../utils'; import {calculatePDiskSeverity} from './calculatePDiskSeverity'; import {calculateVDiskSeverity} from './calculateVDiskSeverity'; @@ -151,7 +152,9 @@ export function prepareVDiskSizeFields({ AllocatedSize: string | number | undefined; SlotSize: string | number | undefined; }) { - const available = Number(AvailableSize ?? 0); + const parsedAvailable = Number(AvailableSize); + const hasKnownAvailableSize = isNumeric(AvailableSize) && parsedAvailable >= 0; + const available = hasKnownAvailableSize ? parsedAvailable : 0; // Unlike available, allocated is displayed in UI, it is incorrect to fallback it to 0 const allocated = Number(AllocatedSize); const slotSize = Number(SlotSize); @@ -168,6 +171,7 @@ export function prepareVDiskSizeFields({ available, allocated, sizeLimit, + hasKnownAvailableSize, hasSizeLimitFallback, }); const allocatedPercent = sizeLimit > 0 ? Math.floor((allocated * 100) / sizeLimit) : NaN; @@ -185,13 +189,19 @@ function getVDiskFreeSize({ available, allocated, sizeLimit, + hasKnownAvailableSize, hasSizeLimitFallback, }: { available: number; allocated: number; sizeLimit: number; + hasKnownAvailableSize: boolean; hasSizeLimitFallback: boolean; }) { + if (!hasSizeLimitFallback && hasKnownAvailableSize) { + return available; + } + if (!Number.isFinite(sizeLimit) || sizeLimit < 0) { return NaN; }