import React from 'react'; import {Flex, HelpMark, Progress, Text, Tooltip} from '@gravity-ui/uikit'; import type {PopupPlacement} from '@gravity-ui/uikit'; import {cn} from '../../../../../utils/cn'; import {formatPercent} from '../../../../../utils/dataFormatters/dataFormatters'; import i18n from './i18n'; import type { TenantStorageSegment, TenantStorageSegmentKey, TenantStorageSystemDetail, TenantStorageSystemDetailKey, } from './utils'; import { TENANT_STORAGE_SEGMENT_KEYS, TENANT_STORAGE_SYSTEM_DETAIL_KEYS, getTenantStorageSegmentDisplayValue, } from './utils'; import './TenantStorageSegments.scss'; const b = cn('ydb-tenant-storage-segments'); const SEGMENT_TOOLTIP_OPEN_DELAY = 100; const SEGMENT_TOOLTIP_CLOSE_DELAY = 100; const SEGMENT_TOOLTIP_WITH_HELP_MARK_CLOSE_DELAY = 250; const PROGRESS_TOOLTIP_PLACEMENT: PopupPlacement = ['top', 'bottom']; const LEGEND_TOOLTIP_PLACEMENT: PopupPlacement = ['bottom', 'top']; export type TenantStorageSegmentOpenSource = 'progress' | 'legend' | 'system-details'; export type TenantStorageSegmentOpenChange = ( segmentKey: TenantStorageSegmentKey, open: boolean, source: TenantStorageSegmentOpenSource, ) => void; const SEGMENT_COLORS: Record = { [TENANT_STORAGE_SEGMENT_KEYS.rowTables]: 'var(--ydb-storage-segment-row-tables)', [TENANT_STORAGE_SEGMENT_KEYS.columnTables]: 'var(--ydb-storage-segment-column-tables)', [TENANT_STORAGE_SEGMENT_KEYS.topics]: 'var(--ydb-storage-segment-topics)', [TENANT_STORAGE_SEGMENT_KEYS.system]: 'var(--ydb-storage-segment-system)', [TENANT_STORAGE_SEGMENT_KEYS.unknown]: 'var(--ydb-storage-segment-unknown)', }; const SEGMENT_LABELS: Record = { [TENANT_STORAGE_SEGMENT_KEYS.rowTables]: i18n('value_row-tables'), [TENANT_STORAGE_SEGMENT_KEYS.columnTables]: i18n('value_column-tables'), [TENANT_STORAGE_SEGMENT_KEYS.topics]: i18n('value_topics'), [TENANT_STORAGE_SEGMENT_KEYS.system]: i18n('value_system'), [TENANT_STORAGE_SEGMENT_KEYS.unknown]: i18n('value_unknown'), }; const SEGMENT_ORDER_INDEX: Record = { [TENANT_STORAGE_SEGMENT_KEYS.system]: 0, [TENANT_STORAGE_SEGMENT_KEYS.rowTables]: 1, [TENANT_STORAGE_SEGMENT_KEYS.columnTables]: 2, [TENANT_STORAGE_SEGMENT_KEYS.topics]: 3, [TENANT_STORAGE_SEGMENT_KEYS.unknown]: 4, }; const SYSTEM_DETAIL_LABELS: Record = { [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.hive]: i18n('value_system-detail-hive'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.coordinator]: i18n('value_system-detail-coordinator'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.mediator]: i18n('value_system-detail-mediator'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.schemeShard]: i18n('value_system-detail-scheme-shard'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.sysViewProcessor]: i18n( 'value_system-detail-sys-view-processor', ), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.graphShard]: i18n('value_system-detail-graph-shard'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.statisticsAggregator]: i18n( 'value_system-detail-statistics-aggregator', ), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.bsController]: i18n('value_system-detail-bs-controller'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.cms]: i18n('value_system-detail-cms'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.nodeBroker]: i18n('value_system-detail-node-broker'), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.tenantSlotBroker]: i18n( 'value_system-detail-tenant-slot-broker', ), [TENANT_STORAGE_SYSTEM_DETAIL_KEYS.console]: i18n('value_system-detail-console'), }; function getSegmentProgressValue(segment: TenantStorageSegment) { return segment.progressValue ?? segment.value; } function getActiveDisplaySegments(segments: TenantStorageSegment[]) { return segments .filter((segment) => getSegmentProgressValue(segment) > 0) .sort((left, right) => SEGMENT_ORDER_INDEX[left.key] - SEGMENT_ORDER_INDEX[right.key]); } function formatSegmentPercent(value: number, total: number) { if (!Number.isFinite(total) || total <= 0 || value <= 0) { return ''; } const percent = value / total; return formatPercent(percent, 2); } export function SegmentTooltipContent({ formatValue, segment, total, totalLabel, }: { formatValue: (value: number) => string; segment: TenantStorageSegment; total: number; totalLabel: string; }) { const percent = formatSegmentPercent(getSegmentProgressValue(segment), total); return (
  • {formatValue(getTenantStorageSegmentDisplayValue(segment))}
  • {percent ? (
  • {i18n('context_segment-share', {value: percent, totalLabel})}
  • ) : null}
); } function SegmentTooltip({ children, closeDelay = SEGMENT_TOOLTIP_CLOSE_DELAY, formatValue, onOpenChange, placement, segment, source, total, totalLabel, }: { children: React.ReactElement; closeDelay?: number; formatValue: (value: number) => string; onOpenChange: TenantStorageSegmentOpenChange; placement: PopupPlacement; segment: TenantStorageSegment; source: TenantStorageSegmentOpenSource; total: number; totalLabel: string; }) { return ( } onOpenChange={(open) => onOpenChange(segment.key, open, source)} > {children} ); } export function SegmentedProgressBar({ activeSegmentKey, formatValue, formatTooltipValue, onSegmentOpenChange, segments, tooltipTotal, tooltipTotalLabel, total, }: { activeSegmentKey?: TenantStorageSegmentKey; formatValue: (value: number) => string; formatTooltipValue: (value: number) => string; onSegmentOpenChange: TenantStorageSegmentOpenChange; segments: TenantStorageSegment[]; tooltipTotal: number; tooltipTotalLabel: string; total?: number; }) { const activeSegments = getActiveDisplaySegments(segments); const segmentSum = activeSegments.reduce((sum, s) => sum + getSegmentProgressValue(s), 0); const effectiveTotal = total === undefined ? segmentSum : total; if (!Number.isFinite(effectiveTotal) || effectiveTotal <= 0 || segmentSum <= 0) { return ; } const cappedTotal = Math.max(effectiveTotal, segmentSum); return (
{activeSegments.map((segment) => { const inactive = activeSegmentKey !== undefined && activeSegmentKey !== segment.key; return (
event.preventDefault()} style={{ width: `${(getSegmentProgressValue(segment) / cappedTotal) * 100}%`, background: SEGMENT_COLORS[segment.key], }} tabIndex={0} /> ); })}
); } interface LegendItemContentProps extends React.HTMLAttributes { formatValue: (value: number) => string; segment: TenantStorageSegment; } const LegendItemContent = React.forwardRef( ({className, formatValue, onMouseDown, segment, ...props}, ref) => { const formattedValue = formatValue(getTenantStorageSegmentDisplayValue(segment)); const handleMouseDown = React.useCallback( (event: React.MouseEvent) => { onMouseDown?.(event); event.preventDefault(); }, [onMouseDown], ); return (
); }, ); LegendItemContent.displayName = 'LegendItemContent'; function SystemDetailsHelpMark({ describedBy, details, formatSystemDetailValue, formatValue, onOpenChange, segmentKey, }: { describedBy: string; details: TenantStorageSystemDetail[]; formatSystemDetailValue?: (value: number) => string; formatValue: (value: number) => string; onOpenChange: TenantStorageSegmentOpenChange; segmentKey: TenantStorageSegmentKey; }) { const handleOpenChange = React.useCallback( (open: boolean) => { onOpenChange(segmentKey, open, 'system-details'); }, [onOpenChange, segmentKey], ); return ( {details.map((detail) => ( {SYSTEM_DETAIL_LABELS[detail.key]} {formatSystemDetailValue?.(detail.value) ?? formatValue(detail.value)} ))} ); } function LegendItem({ activeSegmentKey, formatSystemDetailValue, formatTooltipValue, formatValue, onSegmentOpenChange, segment, showSystemDetails, systemDetails, tooltipTotal, tooltipTotalLabel, }: { activeSegmentKey?: TenantStorageSegmentKey; formatSystemDetailValue?: (value: number) => string; formatTooltipValue: (value: number) => string; formatValue: (value: number) => string; onSegmentOpenChange: TenantStorageSegmentOpenChange; segment: TenantStorageSegment; showSystemDetails: boolean; systemDetails: TenantStorageSystemDetail[]; tooltipTotal: number; tooltipTotalLabel: string; }) { const contentId = React.useId(); const inactive = activeSegmentKey !== undefined && activeSegmentKey !== segment.key; const closeDelay = showSystemDetails ? SEGMENT_TOOLTIP_WITH_HELP_MARK_CLOSE_DELAY : undefined; return (
{showSystemDetails ? ( ) : null}
); } export function LegendItems({ activeSegmentKey, segments, formatValue, formatTooltipValue, formatSystemDetailValue, onSegmentOpenChange, systemDetails, tooltipTotal, tooltipTotalLabel, }: { activeSegmentKey?: TenantStorageSegmentKey; segments: TenantStorageSegment[]; formatValue: (value: number) => string; formatTooltipValue: (value: number) => string; formatSystemDetailValue?: (value: number) => string; onSegmentOpenChange: TenantStorageSegmentOpenChange; systemDetails?: TenantStorageSystemDetail[]; tooltipTotal: number; tooltipTotalLabel: string; }) { const activeSegments = getActiveDisplaySegments(segments); const visibleSystemDetails = (systemDetails ?? []).filter((detail) => detail.value > 0); if (activeSegments.length === 0) { return null; } return (
{activeSegments.map((segment) => { const isSystemSegment = segment.key === TENANT_STORAGE_SEGMENT_KEYS.system; const showSystemDetails = isSystemSegment && visibleSystemDetails.length > 0; return ( ); })}
); }