diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.scss b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.scss new file mode 100644 index 0000000000..8d7c09407c --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.scss @@ -0,0 +1,32 @@ +.ydb-partitions-progress { + &__segment { + display: flex; + flex-basis: 0; + + &_additional { + min-width: 20px; + } + + &_main { + min-width: 70px; + } + } + + &__segment-bar { + &_additional { + --g-progress-filled-background-color: var(--g-color-base-danger-heavy); + } + + &_main { + --g-progress-filled-background-color: var(--g-color-base-info-heavy); + --g-progress-empty-background-color: var(--g-color-base-info-light); + } + } + + &__segment-touched { + padding: var(--g-spacing-2); + + font-size: var(--g-text-body-2-font-size); + line-height: var(--g-text-body-2-line-height); + } +} diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.tsx b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.tsx new file mode 100644 index 0000000000..366bf3705f --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/PartitionsProgress.tsx @@ -0,0 +1,144 @@ +import {Flex, Popover, Progress, Text} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; + +import {cn} from '../../../../../../utils/cn'; + +import {calcPartitionsProgress, getPartitionsLabel} from './helpers'; +import i18n from './i18n'; + +import './PartitionsProgress.scss'; + +const b = cn('ydb-partitions-progress'); + +interface PartitionsProgressProps { + minPartitions: number; + maxPartitions?: number; + partitionsCount: number; + className?: string; +} + +type SegmentPosition = 'main' | 'additional'; + +const SEGMENT_MODS: Record> = { + additional: {additional: true}, + main: {main: true}, +}; + +export const FULL_FILL_VALUE = 100; + +interface SegmentProgressBarProps { + position: SegmentPosition; + value: number; +} + +const SegmentProgressBar = ({position, value}: SegmentProgressBarProps) => ( +
+ +
+); + +export const PartitionsProgress = ({ + minPartitions, + maxPartitions, + partitionsCount, + className, +}: PartitionsProgressProps) => { + const { + min, + max, + partitionsBelowMin, + partitionsAboveMax, + isBelowMin, + isAboveMax, + leftSegmentUnits, + mainSegmentUnits, + rightSegmentUnits, + mainProgressValue, + } = calcPartitionsProgress(minPartitions, maxPartitions, partitionsCount); + + const partitionsLabel = getPartitionsLabel(partitionsCount); + + const belowLimitTooltip = i18n('tooltip_partitions-below-limit', { + count: partitionsCount, + diff: partitionsBelowMin, + partitions: partitionsLabel, + }); + + const aboveLimitTooltip = i18n('tooltip_partitions-above-limit', { + count: partitionsCount, + diff: partitionsAboveMax, + partitions: partitionsLabel, + }); + + const currentTooltip = i18n('tooltip_partitions-current', { + count: partitionsCount, + partitions: partitionsLabel, + }); + + const maxLabel = isNil(max) ? i18n('value_no-limit') : max; + + let tooltipContent = currentTooltip; + if (isBelowMin) { + tooltipContent = belowLimitTooltip; + } else if (isAboveMax) { + tooltipContent = aboveLimitTooltip; + } + + return ( + + + {isBelowMin && ( + + + + + + {partitionsCount} + + + + )} + + + + + + + {min} + + + {maxLabel} + + + + + {isAboveMax && ( + + + + + + {partitionsCount} + + + + )} + + + ); +}; diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/helpers.ts b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/helpers.ts new file mode 100644 index 0000000000..773eae7758 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/helpers.ts @@ -0,0 +1,109 @@ +import {isNil} from 'lodash'; + +import {FULL_FILL_VALUE} from './PartitionsProgress'; +import i18n from './i18n'; + +export interface PartitionsProgressCalcResult { + min: number; + max?: number; + + partitionsBelowMin: number; + partitionsAboveMax: number; + + isBelowMin: boolean; + isAboveMax: boolean; + + leftSegmentUnits: number; + mainSegmentUnits: number; + rightSegmentUnits: number; + + mainProgressValue: number; +} + +export function calcPartitionsProgress( + minPartitions: number, + maxPartitions: number | undefined, + partitionsCount: number, +): PartitionsProgressCalcResult { + const min = minPartitions; + + const hasMaxLimit = !isNil(maxPartitions); + + if (!hasMaxLimit) { + const partitionsBelowMin = Math.max(0, min - partitionsCount); + const partitionsAboveMax = 0; + const isBelowMin = partitionsBelowMin > 0; + const isAboveMax = false; + + // When max limit is not provided, reserve a fixed 20% of the total width + // for the "below min" segment (1:4 ratio between warning and main segments). + const leftSegmentUnits = isBelowMin ? 1 : 0; + const mainSegmentUnits = isBelowMin ? 4 : 1; + const rightSegmentUnits = 0; + + const mainProgressValue = partitionsCount < min ? 0 : FULL_FILL_VALUE; + + return { + min, + max: undefined, + + partitionsBelowMin, + partitionsAboveMax, + + isBelowMin, + isAboveMax, + + leftSegmentUnits, + mainSegmentUnits, + rightSegmentUnits, + + mainProgressValue, + }; + } + + const max = Math.max(maxPartitions as number, minPartitions); + + const range = Math.max(0, max - min); + + const partitionsBelowMin = Math.max(0, min - partitionsCount); + const partitionsAboveMax = Math.max(0, partitionsCount - max); + + const isBelowMin = partitionsBelowMin > 0; + const isAboveMax = partitionsAboveMax > 0; + + const mainSegmentUnits = range || 1; + + let mainProgressValue = 0; + if (range > 0) { + if (partitionsCount <= min) { + mainProgressValue = 0; + } else if (partitionsCount >= max) { + mainProgressValue = FULL_FILL_VALUE; + } else { + mainProgressValue = ((partitionsCount - min) / range) * 100; + } + } + + const leftSegmentUnits = isBelowMin ? partitionsBelowMin : 0; + const rightSegmentUnits = isAboveMax ? partitionsAboveMax : 0; + + return { + min, + max, + + partitionsBelowMin, + partitionsAboveMax, + + isBelowMin, + isAboveMax, + + leftSegmentUnits, + mainSegmentUnits, + rightSegmentUnits, + + mainProgressValue, + }; +} + +export const getPartitionsLabel = (count: number) => + count === 1 ? i18n('value_partition-one') : i18n('value_partition-many'); diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/en.json b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/en.json new file mode 100644 index 0000000000..ab452650af --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/en.json @@ -0,0 +1,10 @@ +{ + "tooltip_partitions-current": "{{count}} {{partitions}}", + "tooltip_partitions-below-limit": "{{count}} {{partitions}}. {{diff}} less than the limit", + "tooltip_partitions-above-limit": "{{count}} {{partitions}}. {{diff}} over the limit", + + "value_partition-one": "partition", + "value_partition-many": "partitions", + + "value_no-limit": "No limit" +} diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/index.ts b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/index.ts new file mode 100644 index 0000000000..0062f51ab4 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/PartitionsProgress/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'partitions-progress'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.scss b/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.scss index 4f787845b4..5774ba225e 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.scss +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.scss @@ -2,7 +2,15 @@ .ydb-diagnostics-table-info { &__title { - @include mixins.info-viewer-title(); + margin-bottom: 10px; + + font-weight: 600; + @include mixins.body-2-typography(); + } + + &__progress-bar { + max-width: 656px; + padding: var(--g-spacing-3) 0 var(--g-spacing-4); } &__row { diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.tsx b/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.tsx index 74eaf96f84..994d703cd6 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.tsx +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/TableInfo.tsx @@ -3,8 +3,8 @@ import React from 'react'; import {InfoViewer} from '../../../../../components/InfoViewer'; import type {EPathType, TEvDescribeSchemeResult} from '../../../../../types/api/schema'; import {cn} from '../../../../../utils/cn'; -import {EntityTitle} from '../../../EntityTitle/EntityTitle'; +import {PartitionsProgress} from './PartitionsProgress/PartitionsProgress'; import i18n from './i18n'; import {prepareTableInfo} from './prepareTableInfo'; @@ -18,22 +18,33 @@ interface TableInfoProps { } export const TableInfo = ({data, type}: TableInfoProps) => { - const title = ; - const { generalInfo, tableStatsInfo, tabletMetricsInfo = [], partitionConfigInfo = [], + partitionProgressConfig, } = React.useMemo(() => prepareTableInfo(data, type), [data, type]); + // Feature flag: show partitions progress only if WINDOW_SHOW_TABLE_SETTINGS is truthy + const isPartitionsProgressEnabled = Boolean(window.WINDOW_SHOW_TABLE_SETTINGS); + return (
+
{i18n('title')}
+ {isPartitionsProgressEnabled && partitionProgressConfig && ( +
+ +
+ )}
{title}
} + renderEmptyState={() => null} />
{tableStatsInfo ? ( diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/i18n/en.json b/src/containers/Tenant/Diagnostics/Overview/TableInfo/i18n/en.json index e449db46b7..8de7d5119f 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TableInfo/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/i18n/en.json @@ -1,6 +1,7 @@ { - "tableStats": "Table Stats", - "tabletMetrics": "Tablet Metrics", + "title": "Partitioning", + "tableStats": "Stats", + "tabletMetrics": "Metrics", "partitionConfig": "Partition Config", "label.ttl": "TTL for rows", diff --git a/src/containers/Tenant/Diagnostics/Overview/TableInfo/prepareTableInfo.tsx b/src/containers/Tenant/Diagnostics/Overview/TableInfo/prepareTableInfo.tsx index b96e78fef9..7740f17b42 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TableInfo/prepareTableInfo.tsx +++ b/src/containers/Tenant/Diagnostics/Overview/TableInfo/prepareTableInfo.tsx @@ -1,4 +1,5 @@ import {Text} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; import omit from 'lodash/omit'; import {toFormattedSize} from '../../../../../components/FormattedBytes/utils'; @@ -16,9 +17,9 @@ import type { TEvDescribeSchemeResult, TPartitionConfig, TTTLSettings, + TTablePartition, } from '../../../../../types/api/schema'; import {EPathType} from '../../../../../types/api/schema'; -import {valueIsDefined} from '../../../../../utils'; import {formatBytes, formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; import {formatDurationToShortTimeFormat} from '../../../../../utils/timeParsers'; import {isNumeric} from '../../../../../utils/utils'; @@ -129,7 +130,7 @@ const prepareTableGeneralInfo = (PartitionConfig: TPartitionConfig, TTLSettings? } } - if (valueIsDefined(EnableFilterByKey)) { + if (!isNil(EnableFilterByKey)) { generalTableInfo.push({ label: i18n('label.bloom-filter'), value: EnableFilterByKey ? i18n('enabled') : i18n('disabled'), @@ -139,6 +140,31 @@ const prepareTableGeneralInfo = (PartitionConfig: TPartitionConfig, TTLSettings? return generalTableInfo; }; +type PartitionProgressConfig = { + minPartitions: number; + maxPartitions?: number; + partitionsCount: number; +}; + +const preparePartitionProgressConfig = ( + PartitionConfig: TPartitionConfig, + TablePartitions?: TTablePartition[], +): PartitionProgressConfig => { + const {PartitioningPolicy} = PartitionConfig; + + // We are convinced, there is always at least one partition; + // fallback and clamp to 1 if value is missing. + const minPartitions = Math.max(1, PartitioningPolicy?.MinPartitionsCount ?? 1); + const maxPartitions = PartitioningPolicy?.MaxPartitionsCount; + const partitionsCount = TablePartitions?.length ?? 1; + + return { + minPartitions, + maxPartitions, + partitionsCount, + }; +}; + /** Prepares data for Table, ColumnTable and ColumnStore */ export const prepareTableInfo = (data?: TEvDescribeSchemeResult, type?: EPathType) => { if (!data) { @@ -148,6 +174,7 @@ export const prepareTableInfo = (data?: TEvDescribeSchemeResult, type?: EPathTyp const {PathDescription = {}} = data; const { + TablePartitions, TableStats = {}, TabletMetrics = {}, Table: {PartitionConfig = {}, TTLSettings} = {}, @@ -181,10 +208,15 @@ export const prepareTableInfo = (data?: TEvDescribeSchemeResult, type?: EPathTyp const {FollowerGroups, FollowerCount, CrossDataCenterFollowerCount} = PartitionConfig; let generalInfo: InfoViewerItem[] = []; + let partitionProgressConfig: PartitionProgressConfig | undefined; switch (type) { case EPathType.EPathTypeTable: { generalInfo = prepareTableGeneralInfo(PartitionConfig, TTLSettings); + partitionProgressConfig = preparePartitionProgressConfig( + PartitionConfig, + TablePartitions, + ); break; } case EPathType.EPathTypeColumnTable: { @@ -252,5 +284,11 @@ export const prepareTableInfo = (data?: TEvDescribeSchemeResult, type?: EPathTyp ); } - return {generalInfo, tableStatsInfo, tabletMetricsInfo, partitionConfigInfo}; + return { + generalInfo, + tableStatsInfo, + tabletMetricsInfo, + partitionConfigInfo, + partitionProgressConfig, + }; }; diff --git a/src/types/api/schema/schema.ts b/src/types/api/schema/schema.ts index 319268cc69..0ba521918b 100644 --- a/src/types/api/schema/schema.ts +++ b/src/types/api/schema/schema.ts @@ -334,7 +334,7 @@ interface TPathVersion { GeneralVersion?: string; } -interface TTablePartition { +export interface TTablePartition { /** bytes */ EndOfRangeKeyPrefix?: unknown; IsPoint?: boolean; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index cb10abf0a8..abd9bfa486 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -45,5 +45,7 @@ interface Window { api: import('../services/api/index').YdbEmbeddedAPI; + WINDOW_SHOW_TABLE_SETTINGS?: boolean; + [key: `yaCounter${number}`]: any; }