diff --git a/src/components/InfoViewer/InfoViewer.scss b/src/components/InfoViewer/InfoViewer.scss index a31c77ce0d..7b8a1cabd6 100644 --- a/src/components/InfoViewer/InfoViewer.scss +++ b/src/components/InfoViewer/InfoViewer.scss @@ -1,3 +1,5 @@ +@use '../../styles/mixins.scss'; + .info-viewer { --ydb-info-viewer-font-size: var(--g-text-body-2-font-size); --ydb-info-viewer-line-height: var(--g-text-body-2-line-height); @@ -28,6 +30,10 @@ max-width: 100%; padding-top: 4px; + + &:first-child { + padding-top: 0; + } } &__label { @@ -88,4 +94,18 @@ } } } + + &_variant_small { + .info-viewer__title { + margin: 0 0 var(--g-spacing-3); + + color: var(--g-color-text-primary); + @include mixins.subheader-1-typography(); + } + + .info-viewer__label { + color: var(--g-color-text-secondary); + @include mixins.body-1-typography(); + } + } } diff --git a/src/components/InfoViewer/InfoViewer.tsx b/src/components/InfoViewer/InfoViewer.tsx index d3b421c056..248b8037b3 100644 --- a/src/components/InfoViewer/InfoViewer.tsx +++ b/src/components/InfoViewer/InfoViewer.tsx @@ -16,6 +16,7 @@ export interface InfoViewerProps { info?: InfoViewerItem[]; dots?: boolean; size?: 's'; + variant?: 'default' | 'small'; className?: string; multilineLabels?: boolean; renderEmptyState?: (props?: Pick) => React.ReactNode; @@ -28,6 +29,7 @@ export const InfoViewer = ({ info, dots = true, size, + variant = 'default', className, multilineLabels, renderEmptyState, @@ -37,7 +39,7 @@ export const InfoViewer = ({ } return ( -
+
{title &&
{title}
} {info && info.length > 0 ? (
diff --git a/src/components/MemoryViewer/MemoryViewer.scss b/src/components/MemoryViewer/MemoryViewer.scss index 57e52380a6..c3f4d2ce3e 100644 --- a/src/components/MemoryViewer/MemoryViewer.scss +++ b/src/components/MemoryViewer/MemoryViewer.scss @@ -1,9 +1,9 @@ $memory-type-colors: ( - 'AllocatorCachesMemory': var(--g-color-base-utility-medium-hover), - 'SharedCacheConsumption': var(--g-color-base-info-medium-hover), - 'MemTableConsumption': var(--g-color-base-warning-medium-hover), - 'QueryExecutionConsumption': var(--g-color-base-positive-medium-hover), - 'Other': var(--g-color-base-generic-medium-hover), + 'SharedCacheConsumption': var(--g-color-base-info-medium), + 'QueryExecutionConsumption': var(--g-color-base-positive-medium), + 'MemTableConsumption': var(--g-color-base-warning-medium), + 'AllocatorCachesMemory': var(--g-color-base-danger-medium), + 'Other': var(--g-color-base-neutral-medium), ); @mixin memory-type-color($type) { diff --git a/src/components/MemoryViewer/MemoryViewer.tsx b/src/components/MemoryViewer/MemoryViewer.tsx index f0ccc9a686..93e537c637 100644 --- a/src/components/MemoryViewer/MemoryViewer.tsx +++ b/src/components/MemoryViewer/MemoryViewer.tsx @@ -4,10 +4,10 @@ import type {TMemoryStats} from '../../types/api/nodes'; import {formatBytes} from '../../utils/bytesParsers'; import {cn} from '../../utils/cn'; import {GIGABYTE} from '../../utils/constants'; +import type {FormatProgressViewerValues} from '../../utils/progress'; import {calculateProgressStatus} from '../../utils/progress'; import {isNumeric} from '../../utils/utils'; import {HoverPopup} from '../HoverPopup/HoverPopup'; -import type {FormatProgressViewerValues} from '../ProgressViewer/ProgressViewer'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {calculateAllocatedMemory, getMemorySegments} from './utils'; diff --git a/src/components/MemoryViewer/i18n/en.json b/src/components/MemoryViewer/i18n/en.json index 4f51efc6bc..8f0b554c36 100644 --- a/src/components/MemoryViewer/i18n/en.json +++ b/src/components/MemoryViewer/i18n/en.json @@ -7,5 +7,6 @@ "text_usage": "Usage", "text_soft-limit": "Soft Limit", "text_hard-limit": "Hard Limit", - "text_other": "Other" + "text_other": "Other", + "text_memory-details": "Memory Details" } diff --git a/src/components/MemoryViewer/utils.ts b/src/components/MemoryViewer/utils.ts index dc8bed1c39..48aa871c09 100644 --- a/src/components/MemoryViewer/utils.ts +++ b/src/components/MemoryViewer/utils.ts @@ -13,7 +13,7 @@ export function getMaybeNumber(value: string | number | undefined): number | und return isNumeric(value) ? parseFloat(String(value)) : undefined; } -interface MemorySegment { +export interface MemorySegment { label: string; key: string; value: number; @@ -21,6 +21,19 @@ interface MemorySegment { isInfo?: boolean; } +// Memory segment colors using CSS variables for theme support +export const MEMORY_SEGMENT_COLORS: Record = { + SharedCacheConsumption: 'var(--g-color-base-info-medium)', + QueryExecutionConsumption: 'var(--g-color-base-positive-medium)', + MemTableConsumption: 'var(--g-color-base-warning-medium)', + AllocatorCachesMemory: 'var(--g-color-base-danger-medium)', + Other: 'var(--g-color-base-neutral-medium)', +}; + +export function getMemorySegmentColor(key: string): string { + return MEMORY_SEGMENT_COLORS[key] || MEMORY_SEGMENT_COLORS['Other']; +} + export function getMemorySegments(stats: TMemoryStats, memoryUsage: number): MemorySegment[] { const segments = [ { diff --git a/src/components/ProgressViewer/ProgressViewer.tsx b/src/components/ProgressViewer/ProgressViewer.tsx index f7ef0001b2..3e4fdcb69c 100644 --- a/src/components/ProgressViewer/ProgressViewer.tsx +++ b/src/components/ProgressViewer/ProgressViewer.tsx @@ -1,8 +1,8 @@ import {useTheme} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; -import {formatNumber, roundToPrecision} from '../../utils/dataFormatters/dataFormatters'; -import {calculateProgressStatus} from '../../utils/progress'; +import {calculateProgressStatus, defaultFormatProgressValues} from '../../utils/progress'; +import type {FormatProgressViewerValues} from '../../utils/progress'; import {isNumeric} from '../../utils/utils'; import './ProgressViewer.scss'; @@ -11,19 +11,6 @@ const b = cn('progress-viewer'); type ProgressViewerSize = 'xs' | 's' | 'ns' | 'm' | 'n' | 'l' | 'head'; -export type FormatProgressViewerValues = ( - value?: number, - capacity?: number, -) => (string | number | undefined)[]; - -const formatValue = (value?: number) => { - return formatNumber(roundToPrecision(Number(value), 2)); -}; - -const defaultFormatValues: FormatProgressViewerValues = (value, total) => { - return [formatValue(value), formatValue(total)]; -}; - /* Props description: @@ -56,7 +43,7 @@ export interface ProgressViewerProps { export function ProgressViewer({ value, capacity, - formatValues = defaultFormatValues, + formatValues = defaultFormatProgressValues, percents, withOverflow, className, diff --git a/src/components/ProgressViewer/i18n/en.json b/src/components/ProgressViewer/i18n/en.json new file mode 100644 index 0000000000..3ce3ab8ec8 --- /dev/null +++ b/src/components/ProgressViewer/i18n/en.json @@ -0,0 +1,4 @@ +{ + "value_of_capacity": "{{value}} of {{capacity}}", + "no-data": "No data" +} diff --git a/src/components/ProgressViewer/i18n/index.ts b/src/components/ProgressViewer/i18n/index.ts new file mode 100644 index 0000000000..991b1002cb --- /dev/null +++ b/src/components/ProgressViewer/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-progress-viewer'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/ProgressWrapper/ProgressContainer.tsx b/src/components/ProgressWrapper/ProgressContainer.tsx new file mode 100644 index 0000000000..6eebfd689d --- /dev/null +++ b/src/components/ProgressWrapper/ProgressContainer.tsx @@ -0,0 +1,25 @@ +import {Flex, Text} from '@gravity-ui/uikit'; + +import {getProgressStyle} from './progressUtils'; +import type {ProgressContainerProps} from './types'; + +export function ProgressContainer({ + children, + displayText, + withValue = false, + className, + width, +}: ProgressContainerProps) { + const progressStyle = getProgressStyle(width); + + return ( + +
{children}
+ {withValue && displayText && ( + + {displayText} + + )} +
+ ); +} diff --git a/src/components/ProgressWrapper/ProgressWrapper.tsx b/src/components/ProgressWrapper/ProgressWrapper.tsx new file mode 100644 index 0000000000..f65dce9f9d --- /dev/null +++ b/src/components/ProgressWrapper/ProgressWrapper.tsx @@ -0,0 +1,10 @@ +import {SingleProgress} from './SingleProgress'; +import {StackProgress} from './StackProgress'; +import type {ProgressWrapperProps} from './types'; + +export function ProgressWrapper(props: ProgressWrapperProps) { + if ('stack' in props && props.stack) { + return ; + } + return ; +} diff --git a/src/components/ProgressWrapper/SingleProgress.tsx b/src/components/ProgressWrapper/SingleProgress.tsx new file mode 100644 index 0000000000..15e4f82c57 --- /dev/null +++ b/src/components/ProgressWrapper/SingleProgress.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import {Progress} from '@gravity-ui/uikit'; + +import {defaultFormatProgressValues} from '../../utils/progress'; +import {safeParseNumber} from '../../utils/utils'; + +import {ProgressContainer} from './ProgressContainer'; +import i18n from './i18n'; +import { + PROGRESS_SIZE, + calculateProgressWidth, + formatDisplayValues, + formatProgressText, + isValidValue, +} from './progressUtils'; +import type {ProgressWrapperSingleProps} from './types'; + +export function SingleProgress({ + value, + capacity, + formatValues = defaultFormatProgressValues, + className, + width, + size = PROGRESS_SIZE, + withValue = false, +}: ProgressWrapperSingleProps) { + if (!isValidValue(value)) { + return
{i18n('alert_no-data')}
; + } + + const numericValue = safeParseNumber(value); + const numericCapacity = safeParseNumber(capacity); + const clampedFillWidth = calculateProgressWidth(numericValue, numericCapacity); + + const [valueText, capacityText] = React.useMemo(() => { + return formatDisplayValues(value, capacity, formatValues); + }, [formatValues, value, capacity]); + + const displayText = React.useMemo(() => { + return formatProgressText(valueText, capacityText, numericCapacity); + }, [valueText, capacityText, numericCapacity]); + + return ( + + + + ); +} diff --git a/src/components/ProgressWrapper/StackProgress.tsx b/src/components/ProgressWrapper/StackProgress.tsx new file mode 100644 index 0000000000..581de3ce68 --- /dev/null +++ b/src/components/ProgressWrapper/StackProgress.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import {Progress} from '@gravity-ui/uikit'; + +import {defaultFormatProgressValues} from '../../utils/progress'; +import {safeParseNumber} from '../../utils/utils'; +import {getMemorySegmentColor} from '../MemoryViewer/utils'; + +import {ProgressContainer} from './ProgressContainer'; +import i18n from './i18n'; +import { + MAX_PERCENTAGE, + PROGRESS_SIZE, + formatDisplayValues, + formatProgressText, +} from './progressUtils'; +import type {ProgressWrapperStackProps} from './types'; + +export function StackProgress({ + stack, + totalCapacity, + formatValues = defaultFormatProgressValues, + className, + width, + size = PROGRESS_SIZE, + withValue = false, +}: ProgressWrapperStackProps) { + const displaySegments = React.useMemo(() => { + return stack.filter((segment) => !segment.isInfo && segment.value > 0); + }, [stack]); + + if (displaySegments.length === 0) { + return
{i18n('alert_no-data')}
; + } + + const totalValue = React.useMemo(() => { + return displaySegments.reduce((sum, segment) => sum + segment.value, 0); + }, [displaySegments]); + + const numericTotalCapacity = React.useMemo(() => { + return safeParseNumber(totalCapacity); + }, [totalCapacity]); + + const maxValue = numericTotalCapacity || totalValue; + + const stackElements = React.useMemo(() => { + return displaySegments.map((segment) => ({ + value: maxValue > 0 ? (segment.value / maxValue) * MAX_PERCENTAGE : 0, + color: getMemorySegmentColor(segment.key), + title: segment.label, + })); + }, [displaySegments, maxValue]); + + const [totalValueText, totalCapacityText] = React.useMemo(() => { + return formatDisplayValues(totalValue, numericTotalCapacity || totalValue, formatValues); + }, [formatValues, totalValue, numericTotalCapacity]); + + const displayText = React.useMemo(() => { + return formatProgressText(totalValueText, totalCapacityText, numericTotalCapacity || 0); + }, [totalValueText, totalCapacityText, numericTotalCapacity]); + + return ( + + + + ); +} diff --git a/src/components/ProgressWrapper/i18n/en.json b/src/components/ProgressWrapper/i18n/en.json new file mode 100644 index 0000000000..9fdb3002b5 --- /dev/null +++ b/src/components/ProgressWrapper/i18n/en.json @@ -0,0 +1,4 @@ +{ + "alert_no-data": "no data", + "context_capacity-usage": "{{value}} of {{capacity}}" +} diff --git a/src/components/ProgressWrapper/i18n/index.ts b/src/components/ProgressWrapper/i18n/index.ts new file mode 100644 index 0000000000..6851521722 --- /dev/null +++ b/src/components/ProgressWrapper/i18n/index.ts @@ -0,0 +1,11 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'progress-wrapper'; + +const keysets = { + en, +}; + +export default registerKeysets(COMPONENT, keysets); diff --git a/src/components/ProgressWrapper/index.ts b/src/components/ProgressWrapper/index.ts new file mode 100644 index 0000000000..fadd8192b6 --- /dev/null +++ b/src/components/ProgressWrapper/index.ts @@ -0,0 +1,18 @@ +// Main component - public API +export {ProgressWrapper} from './ProgressWrapper'; + +// Individual components - for direct usage if needed +export {SingleProgress} from './SingleProgress'; +export {StackProgress} from './StackProgress'; +export {ProgressContainer} from './ProgressContainer'; + +// Types - for consumers +export type { + ProgressWrapperProps, + ProgressWrapperSingleProps, + ProgressWrapperStackProps, + ProgressContainerProps, +} from './types'; + +// Utils - for advanced usage +export * from './progressUtils'; diff --git a/src/components/ProgressWrapper/progressUtils.ts b/src/components/ProgressWrapper/progressUtils.ts new file mode 100644 index 0000000000..c8bbdbf393 --- /dev/null +++ b/src/components/ProgressWrapper/progressUtils.ts @@ -0,0 +1,53 @@ +import type {FormatProgressViewerValues} from '../../utils/progress'; +import {isNumeric, safeParseNumber} from '../../utils/utils'; + +import i18n from './i18n'; + +// Constants that were previously in TenantStorage/constants +export const DEFAULT_PROGRESS_WIDTH = 400; +export const MAX_PERCENTAGE = 100; +export const MIN_PERCENTAGE = 0; +export const PROGRESS_SIZE = 's'; + +export const isValidValue = (val?: number | string): boolean => + isNumeric(val) && safeParseNumber(val) >= 0; + +export function calculateProgressWidth(value: number, capacity: number): number { + const rawPercentage = + capacity > 0 ? Math.floor((value / capacity) * MAX_PERCENTAGE) : MAX_PERCENTAGE; + const fillWidth = Math.max(MIN_PERCENTAGE, rawPercentage); + return Math.min(fillWidth, MAX_PERCENTAGE); +} + +export function getProgressStyle(width?: number | 'full') { + const isFullWidth = width === 'full'; + const validatedWidth = isFullWidth ? 0 : Math.max(0, width || DEFAULT_PROGRESS_WIDTH); + + return { + width: isFullWidth ? '100%' : `${validatedWidth}px`, + flex: isFullWidth ? '1' : 'none', + }; +} + +export function formatProgressText( + valueText: string | number | undefined, + capacityText: string | number | undefined, + numericCapacity: number, +): string { + if (numericCapacity <= 0) { + return String(valueText); + } + return i18n('context_capacity-usage', {value: valueText, capacity: capacityText}); +} + +export function formatDisplayValues( + value: number | string | undefined, + capacity: number | string | undefined, + formatValues?: FormatProgressViewerValues, +): [string | number | undefined, string | number | undefined] { + if (formatValues) { + const result = formatValues(Number(value), Number(capacity)); + return [result[0], result[1]] as [string | number | undefined, string | number | undefined]; + } + return [value, capacity]; +} diff --git a/src/components/ProgressWrapper/types.ts b/src/components/ProgressWrapper/types.ts new file mode 100644 index 0000000000..cb2fe01372 --- /dev/null +++ b/src/components/ProgressWrapper/types.ts @@ -0,0 +1,35 @@ +import type {ProgressSize} from '@gravity-ui/uikit'; + +import type {FormatProgressViewerValues} from '../../utils/progress'; +import type {MemorySegment} from '../MemoryViewer/utils'; + +export interface ProgressWrapperBaseProps { + formatValues?: FormatProgressViewerValues; + className?: string; + width?: number | 'full'; + size?: ProgressSize; + withValue?: boolean; +} + +export interface ProgressWrapperSingleProps extends ProgressWrapperBaseProps { + value?: number | string; + capacity?: number | string; + stack?: never; +} + +export interface ProgressWrapperStackProps extends ProgressWrapperBaseProps { + stack: MemorySegment[]; + totalCapacity?: number | string; + value?: never; + capacity?: never; +} + +export type ProgressWrapperProps = ProgressWrapperSingleProps | ProgressWrapperStackProps; + +export interface ProgressContainerProps { + children: React.ReactNode; + displayText?: string; + withValue?: boolean; + className?: string; + width?: number | 'full'; +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss index 2434c4c3e1..79c966a408 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss @@ -1,6 +1,6 @@ .ydb-tenant-dashboard { width: var(--diagnostics-section-table-width); - margin-bottom: var(--diagnostics-section-margin); + margin-bottom: var(--g-spacing-4); &__charts { display: flex; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.scss new file mode 100644 index 0000000000..a0f8360934 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.scss @@ -0,0 +1,93 @@ +@use '../../../../../styles/mixins.scss'; + +.memory-details { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + gap: var(--g-spacing-3); + + &__header { + align-self: stretch; + } + + &__title { + font-weight: 500; + @include mixins.subheader-1-typography(); + } + + &__content { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + gap: var(--g-spacing-4); + } + + &__main-progress { + align-self: stretch; + } + + &__main-progress-bar { + height: 20px; + } + + &__segments-container { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + gap: var(--g-spacing-2); + + margin-bottom: var(--g-spacing-4); + } + + &__segment-row { + display: flex; + align-items: center; + align-self: stretch; + gap: var(--g-spacing-1); + } + + &__segment-indicator { + flex-shrink: 0; + + width: 8px; + height: 8px; + + border-radius: 50%; + } + + &__segment-definition-list { + flex: 1; + } + + &__segment-progress { + flex-shrink: 0; + + width: 400px; + } + + &__progress-bar { + height: 10px; + } + + &__popup-container { + display: flex; + align-items: center; + gap: var(--g-spacing-2); + } + + &__popup-legend { + flex-shrink: 0; + + width: 12px; + height: 12px; + + border-radius: var(--g-border-radius-xs); + } + + &__popup-name { + color: var(--g-color-text-primary); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.tsx new file mode 100644 index 0000000000..9ca899fafc --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDetailsSection.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import {Text} from '@gravity-ui/uikit'; + +import i18n from '../../../../../components/MemoryViewer/i18n'; +import {getMemorySegments} from '../../../../../components/MemoryViewer/utils'; +import {ProgressWrapper} from '../../../../../components/ProgressWrapper'; +import type {TMemoryStats} from '../../../../../types/api/nodes'; +import {formatBytes} from '../../../../../utils/bytesParsers'; +import {cn} from '../../../../../utils/cn'; + +import {MemorySegmentItem} from './MemorySegmentItem'; + +import './MemoryDetailsSection.scss'; + +const b = cn('memory-details'); + +interface MemoryDetailsSectionProps { + memoryStats: TMemoryStats; +} + +export function MemoryDetailsSection({memoryStats}: MemoryDetailsSectionProps) { + const memoryUsage = React.useMemo(() => { + if (memoryStats.AnonRss === undefined) { + return ( + Number(memoryStats.AllocatedMemory || 0) + + Number(memoryStats.AllocatorCachesMemory || 0) + ); + } else { + return Number(memoryStats.AnonRss); + } + }, [memoryStats.AnonRss, memoryStats.AllocatedMemory, memoryStats.AllocatorCachesMemory]); + + const memorySegments = React.useMemo(() => { + return getMemorySegments(memoryStats, memoryUsage); + }, [memoryStats, memoryUsage]); + + const displaySegments = React.useMemo(() => { + return memorySegments.filter((segment) => !segment.isInfo && segment.value > 0); + }, [memorySegments]); + + const formatValues = React.useCallback((value?: number, total?: number): [string, string] => { + return [ + formatBytes({ + value: value || 0, + size: 'gb', + withSizeLabel: false, + precision: 2, + }), + formatBytes({ + value: total || 0, + size: 'gb', + withSizeLabel: true, + precision: 1, + }), + ]; + }, []); + + return ( +
+
+ + {i18n('text_memory-details')} + +
+
+
+ +
+
+ {displaySegments.map((segment) => ( + + ))} +
+
+
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.scss new file mode 100644 index 0000000000..5dc1f07f97 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.scss @@ -0,0 +1,50 @@ +@use '../../../../../styles/mixins.scss'; + +.memory-progress-bar { + &__main-progress-container { + margin-bottom: var(--g-spacing-4); + } + + &__main-progress-bar { + display: flex; + overflow: hidden; + + height: 20px; + + border-radius: var(--g-border-radius-xs); + background: var(--g-color-base-generic); + } + + &__main-segment { + &:first-child { + border-radius: var(--g-border-radius-xs) 0 0 var(--g-border-radius-xs); + } + + &:last-child { + border-radius: 0 var(--g-border-radius-xs) var(--g-border-radius-xs) 0; + } + + &:only-child { + border-radius: var(--g-border-radius-xs); + } + } + + &__popup-container { + display: flex; + align-items: center; + gap: var(--g-spacing-2); + } + + &__popup-legend { + flex-shrink: 0; + + width: 12px; + height: 12px; + + border-radius: var(--g-border-radius-xs); + } + + &__popup-name { + color: var(--g-color-text-primary); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.tsx new file mode 100644 index 0000000000..9c409dd2bc --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryProgressBar.tsx @@ -0,0 +1,36 @@ +import {MEMORY_SEGMENT_COLORS} from '../../../../../components/MemoryViewer/utils'; +import {cn} from '../../../../../utils/cn'; + +import './MemoryProgressBar.scss'; + +const b = cn('memory-progress-bar'); + +interface MemoryProgressBarProps { + memoryUsed?: string; + memoryLimit?: string; +} + +export function MemoryProgressBar({memoryUsed, memoryLimit}: MemoryProgressBarProps) { + // Simple case - single segment progress bar + if (!memoryUsed || !memoryLimit) { + return null; + } + + const usedValue = Number(memoryUsed); + const limitValue = Number(memoryLimit); + const usagePercentage = Math.min((usedValue / limitValue) * 100, 100); + + return ( +
+
+
+
+
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemorySegmentItem.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemorySegmentItem.tsx new file mode 100644 index 0000000000..3a37eee57c --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemorySegmentItem.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import {DefinitionList, Flex, Progress, Text} from '@gravity-ui/uikit'; + +import type {MemorySegment} from '../../../../../components/MemoryViewer/utils'; +import {getMemorySegmentColor} from '../../../../../components/MemoryViewer/utils'; +import {cn} from '../../../../../utils/cn'; +import {formatSegmentValue} from '../../../../../utils/progress'; + +const b = cn('memory-details'); + +interface MemorySegmentItemProps { + segment: MemorySegment; +} + +export function MemorySegmentItem({segment}: MemorySegmentItemProps) { + const segmentColor = React.useMemo(() => { + return getMemorySegmentColor(segment.key); + }, [segment.key]); + + const valueText = React.useMemo(() => { + return formatSegmentValue(segment.value, segment.capacity); + }, [segment.value, segment.capacity]); + + const progressValue = React.useMemo(() => { + return segment.capacity ? (segment.value / segment.capacity) * 100 : 100; + }, [segment.value, segment.capacity]); + + const progressStyle: React.CSSProperties & Record = React.useMemo(() => { + return { + '--g-progress-filled-background-color': segmentColor, + }; + }, [segmentColor]); + + return ( +
+
+ + + {segment.label} + + } + > + +
+
+ +
+
+ + {valueText} + +
+
+
+
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.scss new file mode 100644 index 0000000000..95416b1065 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.scss @@ -0,0 +1,16 @@ +@use '../../../../../styles/mixins.scss'; + +.tenant-memory { + &__value-text { + flex-shrink: 0; + + min-width: 84px; + margin-top: var(--g-spacing-2); + + text-align: right; + white-space: nowrap; + + color: var(--g-color-text-secondary); + @include mixins.body-1-typography(); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx index 8c602149e3..4e4f46ac30 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx @@ -1,15 +1,17 @@ -import React from 'react'; - -import {MemoryViewer} from '../../../../../components/MemoryViewer/MemoryViewer'; -import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer'; +import {InfoViewer} from '../../../../../components/InfoViewer/InfoViewer'; import type {TMemoryStats} from '../../../../../types/api/nodes'; +import {cn} from '../../../../../utils/cn'; import {formatStorageValuesToGb} from '../../../../../utils/dataFormatters/dataFormatters'; import {TenantDashboard} from '../TenantDashboard/TenantDashboard'; -import {b} from '../utils'; +import i18n from '../i18n'; +import {MemoryDetailsSection} from './MemoryDetailsSection'; +import {MemoryProgressBar} from './MemoryProgressBar'; import {TopNodesByMemory} from './TopNodesByMemory'; import {memoryDashboardConfig} from './memoryDashboardConfig'; +import './TenantMemory.scss'; + interface TenantMemoryProps { tenantName: string; memoryStats?: TMemoryStats; @@ -17,29 +19,56 @@ interface TenantMemoryProps { memoryLimit?: string; } +const b = cn('tenant-memory'); + export function TenantMemory({ tenantName, memoryStats, memoryUsed, memoryLimit, }: TenantMemoryProps) { + const renderMemoryDetails = () => { + if (memoryStats) { + return ; + } + + // Simple fallback view + return ( + + + {memoryUsed && memoryLimit && ( +
+ {i18n('context_capacity-usage', { + value: formatStorageValuesToGb(Number(memoryUsed))[0], + capacity: formatStorageValuesToGb( + Number(memoryLimit), + )[0], + })} +
+ )} +
+ ), + }, + ]} + /> + ); + }; + return ( - +
-
{'Memory details'}
-
- {memoryStats ? ( - - ) : ( - - )} -
+ {renderMemoryDetails()} - +
); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss index 5f843dd47c..1256fafc68 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss @@ -32,7 +32,9 @@ } &__title { - margin-bottom: 10px; + margin-bottom: var(--g-spacing-2); + + font-weight: 500; @include mixins.subheader-1-typography(); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.scss new file mode 100644 index 0000000000..d84d6f5f99 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.scss @@ -0,0 +1,9 @@ +.tenant-storage { + &__tabs-container { + margin-top: var(--g-spacing-3); + } + + &__tab-content { + margin-top: var(--g-spacing-3); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx index 3419c76e84..e0ecb4ea26 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx @@ -1,18 +1,29 @@ import React from 'react'; +import {Tab, TabList, TabProvider} from '@gravity-ui/uikit'; + import {InfoViewer} from '../../../../../components/InfoViewer/InfoViewer'; import {LabelWithPopover} from '../../../../../components/LabelWithPopover'; -import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer'; +import {ProgressWrapper} from '../../../../../components/ProgressWrapper'; +import {TENANT_STORAGE_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; +import {cn} from '../../../../../utils/cn'; import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters'; import {TenantDashboard} from '../TenantDashboard/TenantDashboard'; import i18n from '../i18n'; -import {b} from '../utils'; import {TopGroups} from './TopGroups'; import {TopTables} from './TopTables'; import {storageDashboardConfig} from './storageDashboardConfig'; +import {useTenantStorageQueryParams} from './useTenantStorageQueryParams'; + +import './TenantStorage.scss'; -import '../TenantOverview.scss'; +const tenantStorageCn = cn('tenant-storage'); + +const storageTabs = [ + {id: TENANT_STORAGE_TABS_IDS.tables, title: i18n('title_top-tables-by-size')}, + {id: TENANT_STORAGE_TABS_IDS.groups, title: i18n('title_top-groups-by-usage')}, +]; export interface TenantStorageMetrics { blobStorageUsed?: number; @@ -27,8 +38,21 @@ interface TenantStorageProps { } export function TenantStorage({tenantName, metrics}: TenantStorageProps) { + const {storageTab, handleStorageTabChange} = useTenantStorageQueryParams(); + const {blobStorageUsed, tabletStorageUsed, blobStorageLimit, tabletStorageLimit} = metrics; + const renderTabContent = () => { + switch (storageTab) { + case TENANT_STORAGE_TABS_IDS.tables: + return ; + case TENANT_STORAGE_TABS_IDS.groups: + return ; + default: + return null; + } + }; + const info = [ { label: ( @@ -38,11 +62,11 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) { /> ), value: ( - ), }, @@ -54,11 +78,11 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) { /> ), value: ( - ), }, @@ -67,9 +91,23 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) { return ( - - - + + +
+ + + {storageTabs.map(({id, title}) => { + return ( + handleStorageTabChange(id)}> + {title} + + ); + })} + + + +
{renderTabContent()}
+
); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx index 85e8aece8a..a02485cb75 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx @@ -11,8 +11,6 @@ import {TENANT_OVERVIEW_TABLES_SETTINGS} from '../../../../../utils/constants'; import {useAutoRefreshInterval} from '../../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; -import {getSectionTitle} from '../getSectionTitle'; -import i18n from '../i18n'; import '../TenantOverview.scss'; @@ -57,14 +55,8 @@ export function TopTables({database}: TopTablesProps) { ) : null, }, ]; - const title = getSectionTitle({ - entity: i18n('tables'), - postfix: i18n('by-size'), - }); - return ( { + if (!queryParams.storageTab) { + return TENANT_STORAGE_TABS_IDS.tables; + } + const validTabs = Object.values(TENANT_STORAGE_TABS_IDS) as string[]; + return validTabs.includes(queryParams.storageTab) + ? (queryParams.storageTab as TenantStorageTab) + : TENANT_STORAGE_TABS_IDS.tables; + })(); + + const handleStorageTabChange = (value: TenantStorageTab) => { + setQueryParams({storageTab: value}, 'replaceIn'); + }; + + return { + storageTab, + handleStorageTabChange, + }; +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index b613553a2a..8f6e250338 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -35,6 +35,7 @@ "charts.cpu-usage": "CPU usage by pool", "charts.storage-usage": "Tablet storage usage", "charts.memory-usage": "Memory usage", + "title_storage-details": "Storage Details", "storage.tablet-storage-title": "Tablet storage", "storage.tablet-storage-description": "Size of user data and indexes stored in schema objects (tables, topics, etc.)", "storage.db-storage-title": "Database storage", @@ -44,6 +45,11 @@ "title_top-nodes": "Top Nodes", "title_top-shards": "Top Shards", "title_top-queries": "Top Queries", + "title_top-tables-by-size": "Top Tables By Size", + "title_top-groups-by-usage": "Top Groups By Usage", "action_by-load": "By Load", - "action_by-pool-usage": "By Pool Usage" + "action_by-pool-usage": "By Pool Usage", + "title_memory-details": "Memory Details", + "field_memory-usage": "Memory usage", + "context_capacity-usage": "{{value}} of {{capacity}}" } diff --git a/src/containers/Tenant/TenantPages.tsx b/src/containers/Tenant/TenantPages.tsx index f67117b4dc..7b08a77390 100644 --- a/src/containers/Tenant/TenantPages.tsx +++ b/src/containers/Tenant/TenantPages.tsx @@ -26,7 +26,6 @@ export const TenantTabsGroups = { queryTab: 'queryTab', diagnosticsTab: 'diagnosticsTab', metricsTab: 'metricsTab', - cpuTab: 'cpuTab', } as const; export const TENANT_INFO_TABS = [ diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index 6d1b7fe0d9..267dfc3e12 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -54,3 +54,8 @@ export const TENANT_CPU_NODES_MODE_IDS = { load: 'load', pools: 'pools', } as const; + +export const TENANT_STORAGE_TABS_IDS = { + tables: 'tables', + groups: 'groups', +} as const; diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index b0457fb581..59585d7cd6 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -44,8 +44,8 @@ const slice = createSlice({ }, setMetricsTab: (state, action: PayloadAction) => { // Ensure we always have a valid metrics tab - fallback to CPU if empty/invalid - const validTabs = Object.values(TENANT_METRICS_TABS_IDS); - const isValidTab = action.payload && validTabs.includes(action.payload as any); + const validTabs = Object.values(TENANT_METRICS_TABS_IDS) as TenantMetricsTab[]; + const isValidTab = action.payload && validTabs.includes(action.payload); state.metricsTab = isValidTab ? action.payload : TENANT_METRICS_TABS_IDS.cpu; }, }, diff --git a/src/store/reducers/tenant/types.ts b/src/store/reducers/tenant/types.ts index 8915d1efa1..cb55145e61 100644 --- a/src/store/reducers/tenant/types.ts +++ b/src/store/reducers/tenant/types.ts @@ -9,6 +9,7 @@ import type { TENANT_DIAGNOSTICS_TABS_IDS, TENANT_METRICS_TABS_IDS, TENANT_QUERY_TABS_ID, + TENANT_STORAGE_TABS_IDS, TENANT_SUMMARY_TABS_IDS, } from './constants'; @@ -21,6 +22,7 @@ export type TenantSummaryTab = ValueOf; export type TenantMetricsTab = ValueOf; export type TenantCpuTab = ValueOf; export type TenantNodesMode = ValueOf; +export type TenantStorageTab = ValueOf; export interface TenantState { tenantPage: TenantPage; diff --git a/src/utils/progress.ts b/src/utils/progress.ts index 03bf016bbd..217c7b7756 100644 --- a/src/utils/progress.ts +++ b/src/utils/progress.ts @@ -1,7 +1,47 @@ +import {formatBytes} from './bytesParsers'; import {DEFAULT_DANGER_THRESHOLD, DEFAULT_WARNING_THRESHOLD} from './constants'; +import {formatNumber, roundToPrecision} from './dataFormatters/dataFormatters'; export type ProgressStatus = 'good' | 'warning' | 'danger'; +export type FormatProgressViewerValues = ( + value?: number, + capacity?: number, +) => (string | number | undefined)[]; + +const formatValue = (value?: number) => { + return formatNumber(roundToPrecision(value || 0, 2)); +}; + +export const defaultFormatProgressValues: FormatProgressViewerValues = (value, total) => { + return [formatValue(value), formatValue(total)]; +}; + +export function formatSegmentValue(value: number, capacity?: number): string { + if (capacity) { + const usedValue = formatBytes({ + value, + size: 'tb', + withSizeLabel: false, + precision: 2, + }); + const totalValue = formatBytes({ + value: capacity, + size: 'tb', + withSizeLabel: true, + precision: 0, + }); + return `${usedValue} of ${totalValue}`; + } + + return formatBytes({ + value, + size: 'gb', + withSizeLabel: true, + precision: 1, + }); +} + interface CalculateProgressStatusProps { inverseColorize?: boolean; dangerThreshold?: number;