-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add partitions progress bar to table info #3206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c91b573
332209b
f121ed5
ae31b8a
b3414be
9f486b6
205afb9
5ff0661
dd87209
57bb8c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SegmentPosition, Record<string, boolean>> = { | ||
| additional: {additional: true}, | ||
| main: {main: true}, | ||
| }; | ||
|
|
||
| export const FULL_FILL_VALUE = 100; | ||
|
|
||
| interface SegmentProgressBarProps { | ||
| position: SegmentPosition; | ||
| value: number; | ||
| } | ||
|
|
||
| const SegmentProgressBar = ({position, value}: SegmentProgressBarProps) => ( | ||
| <div className={b('segment-bar', SEGMENT_MODS[position])}> | ||
| <Progress value={value} size="s" /> | ||
| </div> | ||
| ); | ||
|
|
||
| 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 ( | ||
| <Popover hasArrow placement="top" content={tooltipContent} className={b('segment-touched')}> | ||
| <Flex alignItems="center" gap="0.5" className={b(null, className)}> | ||
| {isBelowMin && ( | ||
| <Flex | ||
| style={{flexGrow: leftSegmentUnits}} | ||
| direction="column" | ||
| gap="2" | ||
| className={b('segment', SEGMENT_MODS.additional)} | ||
| > | ||
| <SegmentProgressBar position="additional" value={FULL_FILL_VALUE} /> | ||
|
|
||
| <Flex justifyContent="flex-start"> | ||
| <Text variant="body-2" color="secondary"> | ||
| {partitionsCount} | ||
| </Text> | ||
| </Flex> | ||
| </Flex> | ||
| )} | ||
|
|
||
| <Flex | ||
| direction="column" | ||
| className={b('segment', SEGMENT_MODS.main)} | ||
| style={{flexGrow: mainSegmentUnits}} | ||
| gap="2" | ||
| > | ||
| <SegmentProgressBar position="main" value={mainProgressValue} /> | ||
|
|
||
| <Flex justifyContent="space-between"> | ||
| <Text variant="body-2" color="secondary"> | ||
| {min} | ||
| </Text> | ||
| <Text variant="body-2" color="secondary"> | ||
| {maxLabel} | ||
| </Text> | ||
| </Flex> | ||
| </Flex> | ||
|
|
||
| {isAboveMax && ( | ||
| <Flex | ||
| direction="column" | ||
| gap="2" | ||
| className={b('segment', SEGMENT_MODS.additional)} | ||
| style={{flexGrow: rightSegmentUnits}} | ||
| > | ||
| <SegmentProgressBar position="additional" value={FULL_FILL_VALUE} /> | ||
|
|
||
| <Flex justifyContent="flex-end"> | ||
| <Text variant="body-2" color="secondary"> | ||
| {partitionsCount} | ||
| </Text> | ||
| </Flex> | ||
| </Flex> | ||
| )} | ||
| </Flex> | ||
| </Popover> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to move calculations to some function (maybe utils.ts in the same folder) and to cover it with unit tests |
||
| 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'); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import {registerKeysets} from '../../../../../../../utils/i18n'; | ||
|
|
||
| import en from './en.json'; | ||
|
|
||
| const COMPONENT = 'partitions-progress'; | ||
|
|
||
| export default registerKeysets(COMPONENT, {en}); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = <EntityTitle data={data?.PathDescription} />; | ||
|
|
||
| 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 ( | ||
| <div className={b()}> | ||
| <div className={b('title')}>{i18n('title')}</div> | ||
| {isPartitionsProgressEnabled && partitionProgressConfig && ( | ||
| <div className={b('progress-bar')}> | ||
| <PartitionsProgress | ||
| minPartitions={partitionProgressConfig.minPartitions} | ||
| partitionsCount={partitionProgressConfig.partitionsCount} | ||
| maxPartitions={partitionProgressConfig.maxPartitions} | ||
| /> | ||
| </div> | ||
| )} | ||
| <InfoViewer | ||
| info={generalInfo} | ||
| title={title} | ||
| className={b('info-block')} | ||
| renderEmptyState={() => <div className={b('title')}>{title}</div>} | ||
| renderEmptyState={() => null} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is returning null instead of message intentional?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it is intentional) |
||
| /> | ||
| <div className={b('row')}> | ||
| {tableStatsInfo ? ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| { | ||
| "tableStats": "Table Stats", | ||
| "tabletMetrics": "Tablet Metrics", | ||
| "title": "Partitioning", | ||
| "tableStats": "Stats", | ||
| "tabletMetrics": "Metrics", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be great to create new keys in i18n-naming-ruleset.md format (and very super best to change old keys to use it =)) ) |
||
| "partitionConfig": "Partition Config", | ||
|
|
||
| "label.ttl": "TTL for rows", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.