import React from 'react'; import {DiskStateProgressBar} from '../../../components/DiskStateProgressBar/DiskStateProgressBar'; import {HoverPopup} from '../../../components/HoverPopup/HoverPopup'; import {InternalLink} from '../../../components/InternalLink'; import {PDiskLogSize} from '../../../components/PDiskInfo/PDiskLogSize'; import {VDiskPopup} from '../../../components/VDiskPopup/VDiskPopup'; import type {YDBDefinitionListItem} from '../../../components/YDBDefinitionList/YDBDefinitionList'; import {YDBDefinitionList} from '../../../components/YDBDefinitionList/YDBDefinitionList'; import {useVDiskPagePath} from '../../../routes'; import type { EmptySlotData, LogSlotData, PDiskData, SlotItem, SlotItemType, } from '../../../store/reducers/pdisk/types'; import type {NodeMetadata} from '../../../types/store/nodesList'; import {valueIsDefined} from '../../../utils'; import {formatBytes} from '../../../utils/bytesParsers'; import {cn} from '../../../utils/cn'; import {formatStorageValuesToGb} from '../../../utils/dataFormatters/dataFormatters'; import {DISK_COLOR_STATE_TO_NUMERIC_SEVERITY} from '../../../utils/disks/constants'; import {getDiskBarTone} from '../../../utils/disks/getDiskBarTone'; import {pDiskPageKeyset} from '../i18n'; import {isEmptySlot, isLogSlot, isVDiskSlot} from './utils'; import './PDiskSpaceDistribution.scss'; const b = cn('ydb-pdisk-space-distribution'); const BASE_SLOT_HEIGHT = 40; // Upper bound for how much a single slot can be scaled relative to the smallest one. // Protects layout from degenerate cases where a VDisk reports an implausibly small Total // (e.g. a mostly empty disk where Total falls back to AllocatedSize), which would otherwise // blow up other slots' heights to thousands of pixels. const MAX_SLOT_HEIGHT_MULTIPLIER = 10; interface PDiskSpaceDistributionProps { data: PDiskData; } export function PDiskSpaceDistribution({data}: PDiskSpaceDistributionProps) { const getVDiskPagePath = useVDiskPagePath(); const {SlotItems} = data; const {PDiskId, NodeId} = data; // Find the minimum Total among non-log slots to use as the base unit for height scaling. // Slots with missing/zero Total are skipped so that they don't collapse the base unit to 1 // and inflate every other slot's computed height to an unrenderable value. const minNonLogTotal = React.useMemo(() => { if (!SlotItems?.length) { return 1; } let minTotal = Infinity; for (const item of SlotItems) { if (item.SlotType === 'log') { continue; } const value = Number(item.Total); if (!value || value <= 0) { continue; } if (value < minTotal) { minTotal = value; } } return minTotal === Infinity ? 1 : minTotal; }, [SlotItems]); const renderSlots = () => { return SlotItems?.map((item, index) => { return ( ); }); }; if (!SlotItems?.length) { return pDiskPageKeyset('no-slots-data'); } return (
); } interface SlotProps { item: SlotItem; minNonLogTotal: number; pDiskId?: string | number; nodeId?: string | number; nodeData?: NodeMetadata; getVDiskPagePath?: ( params: {nodeId: string | number | undefined; vDiskId: string | undefined}, query?: {activeTab?: string}, ) => string | undefined; } function getSlotHeight(item: SlotItem, minNonLogTotal: number) { // Log slots get a fixed half-height. if (item.SlotType === 'log') { return BASE_SLOT_HEIGHT / 2; } // Slots with a missing/non-numeric/zero Total fall back to BASE_SLOT_HEIGHT so they // stay visible and never produce NaN in the inline style. // Same falsy-check pattern as in minNonLogTotal: catches NaN, 0, undefined, null. const totalValue = Number(item.Total); if (!totalValue || totalValue < 0) { return BASE_SLOT_HEIGHT; } // Others scale proportionally to the smallest non-log slot, clamped from above so an // implausibly small Total can't blow up other slots' heights. const multiplier = Math.min(totalValue / minNonLogTotal, MAX_SLOT_HEIGHT_MULTIPLIER); return multiplier * BASE_SLOT_HEIGHT; } function Slot({ item, minNonLogTotal, nodeId, nodeData, getVDiskPagePath, }: SlotProps) { const renderContent = () => { if (isVDiskSlot(item)) { const vDiskPagePath = getVDiskPagePath?.({ nodeId, vDiskId: item.SlotData.StringifiedId, }); const isReplicatingColor = item.Severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; const isDonor = item.SlotData.DonorMode; return ( ( )} contentClassName={b('vdisk-popup')} placement={['right', 'top']} > } /> ); } if (isLogSlot(item)) { return ( } contentClassName={b('vdisk-popup')} placement={['right', 'top']} > } /> ); } if (isEmptySlot(item)) { return ( } contentClassName={b('vdisk-popup')} placement={['right', 'top']} > } /> ); } return null; }; const slotHeight = getSlotHeight(item, minNonLogTotal); return (
{renderContent()}
); } interface SlotContentProps { id?: string | number; title?: string; used?: number; total?: number; } function SlotContent({id, title, used, total}: SlotContentProps) { const renderSize = () => { const [formattedUsed, formattedTotal] = formatStorageValuesToGb(used, total); if (!total) { return formattedUsed; } return `${formattedUsed} / ${formattedTotal}`; }; return (
{valueIsDefined(id) ? {id} : null} {title} {renderSize()}
); } interface LogInfoProps { data: LogSlotData; } function LogInfo({data}: LogInfoProps) { const {LogTotalSize, LogUsedSize, SystemSize} = data; const items: YDBDefinitionListItem[] = [ { name: pDiskPageKeyset('label.log-size'), content: , }, ]; if (valueIsDefined(SystemSize)) { items.push({ name: pDiskPageKeyset('label.system-size'), content: formatBytes({value: SystemSize, fixedDecimalPlaces: 2}), }); } return ; } interface EmptySlotInfoProps { data: EmptySlotData; } function EmptySlotInfo({data}: EmptySlotInfoProps) { const {Size} = data; const items: YDBDefinitionListItem[] = [ { name: pDiskPageKeyset('label.slot-size'), content: formatBytes({value: Size}), }, ]; return ; }