From 5cbaec6ebb8c320e8917743e4cae604a6397933a Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 16 Oct 2024 13:36:33 +0300 Subject: [PATCH 1/4] feat(Cluster): rework cluster page --- .../DiagnosticCard/DiagnosticCard.scss | 21 ++- .../DiagnosticCard/DiagnosticCard.tsx | 14 +- .../DoughnutMetrics/DoughnutMetrics.scss | 58 ++++++ .../DoughnutMetrics/DoughnutMetrics.tsx | 64 +++++++ .../ProgressViewer/ProgressViewer.scss | 2 +- .../ProgressViewer/ProgressViewer.tsx | 22 +-- src/components/Tag/Tag.scss | 3 +- src/components/Tags/Tags.tsx | 15 +- src/containers/Cluster/Cluster.scss | 37 +++- src/containers/Cluster/Cluster.tsx | 11 +- .../ClusterDashboard/ClusterDashboard.scss | 46 +++++ .../ClusterDashboard/ClusterDashboard.tsx | 137 ++++++++++++++ .../components/ClusterMetricsCard.tsx | 61 +++++++ .../components/ClusterMetricsCores.tsx | 28 +++ .../components/ClusterMetricsMemory.tsx | 30 +++ .../components/ClusterMetricsStorage.tsx | 30 +++ .../Cluster/ClusterDashboard/shared.ts | 11 ++ .../Cluster/ClusterDashboard/utils.tsx | 97 ++++++++++ .../Cluster/ClusterInfo/ClusterInfo.scss | 19 +- .../Cluster/ClusterInfo/ClusterInfo.tsx | 24 +-- .../DiskGroupsStatsBars.scss | 7 +- .../DiskGroupsStatsBars.tsx | 56 +++--- .../components/NodesState/NodesState.scss | 5 +- src/containers/Cluster/ClusterInfo/utils.tsx | 171 ++---------------- src/containers/Cluster/i18n/en.json | 23 ++- src/types/api/cluster.ts | 3 + src/utils/bytesParsers/formatBytes.ts | 8 +- src/utils/dataFormatters/dataFormatters.ts | 41 ++++- src/utils/numeral.ts | 2 +- src/utils/progress.ts | 27 +++ 30 files changed, 780 insertions(+), 293 deletions(-) create mode 100644 src/components/DoughnutMetrics/DoughnutMetrics.scss create mode 100644 src/components/DoughnutMetrics/DoughnutMetrics.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss create mode 100644 src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx create mode 100644 src/containers/Cluster/ClusterDashboard/shared.ts create mode 100644 src/containers/Cluster/ClusterDashboard/utils.tsx create mode 100644 src/utils/progress.ts diff --git a/src/components/DiagnosticCard/DiagnosticCard.scss b/src/components/DiagnosticCard/DiagnosticCard.scss index bf5bf8197..b78262c02 100644 --- a/src/components/DiagnosticCard/DiagnosticCard.scss +++ b/src/components/DiagnosticCard/DiagnosticCard.scss @@ -1,7 +1,6 @@ .ydb-diagnostic-card { flex-shrink: 0; - width: 206px; padding: 16px; padding-bottom: 28px; @@ -13,10 +12,24 @@ border-color: var(--g-color-base-info-medium); background-color: var(--g-color-base-selection); } + &_interactive { + &:hover { + cursor: pointer; - &:hover { - cursor: pointer; + box-shadow: 0px 1px 5px var(--g-color-sfx-shadow); + } + } - box-shadow: 0px 1px 5px var(--g-color-sfx-shadow); + &_size_m { + width: 206px; + min-width: 206px; + } + &_size_l { + width: 289px; + min-width: 289px; + } + &_size_s { + width: 134px; + min-width: 134px; } } diff --git a/src/components/DiagnosticCard/DiagnosticCard.tsx b/src/components/DiagnosticCard/DiagnosticCard.tsx index 19a668196..834c96f8f 100644 --- a/src/components/DiagnosticCard/DiagnosticCard.tsx +++ b/src/components/DiagnosticCard/DiagnosticCard.tsx @@ -4,12 +4,20 @@ import './DiagnosticCard.scss'; const b = cn('ydb-diagnostic-card'); -interface DiagnosticCardProps { +export interface DiagnosticCardProps { children?: React.ReactNode; className?: string; active?: boolean; + size?: 'm' | 'l' | 's'; + interactive?: boolean; } -export function DiagnosticCard({children, className, active}: DiagnosticCardProps) { - return
{children}
; +export function DiagnosticCard({ + children, + className, + active, + size = 'm', + interactive = true, +}: DiagnosticCardProps) { + return
{children}
; } diff --git a/src/components/DoughnutMetrics/DoughnutMetrics.scss b/src/components/DoughnutMetrics/DoughnutMetrics.scss new file mode 100644 index 000000000..9087310f7 --- /dev/null +++ b/src/components/DoughnutMetrics/DoughnutMetrics.scss @@ -0,0 +1,58 @@ +.ydb-doughnut-metrics { + --doughnut-border: 11px; + --doughnut-color: var(--ydb-color-status-green); + &__doughnut { + position: relative; + + width: 172px; + aspect-ratio: 1; + + border-radius: 50%; + background-color: var(--doughnut-color); + &::before { + display: block; + + height: calc(100% - calc(var(--doughnut-border) * 2)); + + content: ''; + + border-radius: 50%; + background-color: var(--g-color-base-background); + + transform: translate(var(--doughnut-border), var(--doughnut-border)); + aspect-ratio: 1; + } + } + &__doughnut_status_warning { + --doughnut-color: var(--ydb-color-status-yellow); + } + &__doughnut_status_danger { + --doughnut-color: var(--ydb-color-status-red); + } + &__text-wrapper { + --wrapper-indent: calc(var(--doughnut-border) + 5px); + + position: absolute; + top: var(--wrapper-indent); + right: var(--wrapper-indent); + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: calc(100% - calc(var(--wrapper-indent) * 2)); + + text-align: center; + aspect-ratio: 1; + } + &__value { + position: absolute; + bottom: 20px; + } + &__legend { + height: 50%; + + white-space: pre-wrap; + } +} diff --git a/src/components/DoughnutMetrics/DoughnutMetrics.tsx b/src/components/DoughnutMetrics/DoughnutMetrics.tsx new file mode 100644 index 000000000..b596d8936 --- /dev/null +++ b/src/components/DoughnutMetrics/DoughnutMetrics.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import type {TextProps} from '@gravity-ui/uikit'; +import {Text} from '@gravity-ui/uikit'; + +import {cn} from '../../utils/cn'; +import type {ProgressStatus} from '../../utils/progress'; + +import './DoughnutMetrics.scss'; + +const b = cn('ydb-doughnut-metrics'); + +interface LegendProps { + children?: React.ReactNode; + variant?: TextProps['variant']; +} + +function Legend({children, variant = 'subheader-3'}: LegendProps) { + return ( + + {children} + + ); +} +function Value({children, variant = 'subheader-2'}: LegendProps) { + return ( + + {children} + + ); +} + +interface DoughnutProps { + status: ProgressStatus; + fillWidth: number; + children?: React.ReactNode; + className?: string; +} + +export function DoughnutMetrics({status, fillWidth, children, className}: DoughnutProps) { + let gradientFill = 'var(--g-color-line-generic-solid)'; + let filledDegrees = fillWidth * 3.6 - 90; + + if (fillWidth > 50) { + gradientFill = 'var(--doughnut-color)'; + filledDegrees = fillWidth * 3.6 + 90; + } + const gradientDegrees = filledDegrees; + return ( +
+
+
{children}
+
+
+ ); +} + +DoughnutMetrics.Legend = Legend; +DoughnutMetrics.Value = Value; diff --git a/src/components/ProgressViewer/ProgressViewer.scss b/src/components/ProgressViewer/ProgressViewer.scss index 4ca04704a..02f9d600c 100644 --- a/src/components/ProgressViewer/ProgressViewer.scss +++ b/src/components/ProgressViewer/ProgressViewer.scss @@ -11,7 +11,7 @@ justify-content: center; align-items: center; - min-width: 120px; + min-width: 150px; height: 23px; padding: 0 4px; diff --git a/src/components/ProgressViewer/ProgressViewer.tsx b/src/components/ProgressViewer/ProgressViewer.tsx index 81505cc37..6f33d6492 100644 --- a/src/components/ProgressViewer/ProgressViewer.tsx +++ b/src/components/ProgressViewer/ProgressViewer.tsx @@ -2,6 +2,7 @@ import {useTheme} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; import {formatNumber, roundToPrecision} from '../../utils/dataFormatters/dataFormatters'; +import {calculateProgressStatus} from '../../utils/progress'; import {isNumeric} from '../../utils/utils'; import './ProgressViewer.scss'; @@ -10,8 +11,6 @@ const b = cn('progress-viewer'); type ProgressViewerSize = 'xs' | 's' | 'ns' | 'm' | 'n' | 'l' | 'head'; -type ProgressViewerStatus = 'good' | 'warning' | 'danger'; - type FormatProgressViewerValues = ( value?: number, capacity?: number, @@ -79,16 +78,15 @@ export function ProgressViewer({ [valueText, capacityText] = formatValues(Number(value), Number(capacity)); } - let status: ProgressViewerStatus = inverseColorize ? 'danger' : 'good'; - if (colorizeProgress) { - if (fillWidth > warningThreshold && fillWidth <= dangerThreshold) { - status = 'warning'; - } else if (fillWidth > dangerThreshold) { - status = inverseColorize ? 'good' : 'danger'; - } - if (!isNumeric(capacity)) { - fillWidth = 100; - } + const status = calculateProgressStatus({ + fillWidth, + warningThreshold, + dangerThreshold, + colorizeProgress, + inverseColorize, + }); + if (colorizeProgress && !isNumeric(capacity)) { + fillWidth = 100; } const lineStyle = { diff --git a/src/components/Tag/Tag.scss b/src/components/Tag/Tag.scss index bc3747eaa..04d131492 100644 --- a/src/components/Tag/Tag.scss +++ b/src/components/Tag/Tag.scss @@ -1,9 +1,8 @@ .tag { - margin-right: 5px; padding: 2px 5px; font-size: 12px; - text-transform: uppercase; + white-space: nowrap; color: var(--g-color-text-primary); border-radius: 3px; diff --git a/src/components/Tags/Tags.tsx b/src/components/Tags/Tags.tsx index 9a0c61d2b..3030b45f3 100644 --- a/src/components/Tags/Tags.tsx +++ b/src/components/Tags/Tags.tsx @@ -1,24 +1,23 @@ import React from 'react'; -import {cn} from '../../utils/cn'; +import type {FlexProps} from '@gravity-ui/uikit'; +import {Flex} from '@gravity-ui/uikit'; + import type {TagType} from '../Tag'; import {Tag} from '../Tag'; -import './Tags.scss'; - -const b = cn('tags'); - interface TagsProps { tags: React.ReactNode[]; tagsType?: TagType; className?: string; + gap?: FlexProps['gap']; } -export const Tags = ({tags, tagsType, className = ''}: TagsProps) => { +export const Tags = ({tags, tagsType, className = '', gap = 1}: TagsProps) => { return ( -
+ {tags && tags.map((tag, tagIndex) => )} -
+ ); }; diff --git a/src/containers/Cluster/Cluster.scss b/src/containers/Cluster/Cluster.scss index b57528448..29c31f517 100644 --- a/src/containers/Cluster/Cluster.scss +++ b/src/containers/Cluster/Cluster.scss @@ -27,27 +27,50 @@ height: var(--g-text-header-1-line-height); } - &__tabs { - position: sticky; - left: 0; + &__tabs-sticky-wrapper { + z-index: 3; + margin-top: 20px; + margin-right: -20px; + padding-right: 20px; + @include sticky-top(); + } + &__tabs { display: flex; - justify-content: space-between; - align-items: center; @include tabs-wrapper-styles(); } &__sticky-wrapper { position: sticky; z-index: 4; - top: 56px; + top: 66px; left: 0; } &__auto-refresh-control { float: right; - margin-top: -40px; + margin-top: -46px; + + background-color: var(--g-color-base-background); + } + .ydb-table-with-controls-layout__controls-wrapper { + top: 40px; + } + + &__tablets { + .data-table__sticky_moving { + // Place table head right after controls + top: 60px !important; + } + } + + &__fake-block { + position: sticky; + z-index: 3; + top: 40px; + + height: 20px; background-color: var(--g-color-base-background); } diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index 5a31ae98a..0b1c353fa 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -31,6 +31,7 @@ import {TabletsTable} from '../Tablets/TabletsTable'; import {Tenants} from '../Tenants/Tenants'; import {Versions} from '../Versions/Versions'; +import {ClusterDashboard} from './ClusterDashboard/ClusterDashboard'; import {ClusterInfo} from './ClusterInfo/ClusterInfo'; import type {ClusterTab} from './utils'; import {clusterTabs, clusterTabsIds, getClusterPath, isClusterTab} from './utils'; @@ -119,7 +120,11 @@ export function Cluster({ {activeTab ? {activeTab.title} : null}
{getClusterTitle()}
-
+
+ +
+ +
-
- -
+ {formatNumber(value)} + + ); +} + +interface ClusterDashboardProps { + cluster: TClusterInfo; + groupStats?: ClusterGroupsStats; + loading?: boolean; +} + +export function ClusterDashboard({cluster, groupStats = {}, loading}: ClusterDashboardProps) { + const getDoughnuts = () => { + if (loading) { + return Array.from('123').map((el) => ); + } + const metricsCards = []; + if (isClusterInfoV2(cluster)) { + const {CoresUsed, NumberOfCpus} = cluster; + if (valueIsDefined(CoresUsed) && valueIsDefined(NumberOfCpus)) { + metricsCards.push( + , + ); + } + } + const {StorageTotal, StorageUsed} = cluster; + if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) { + metricsCards.push( + , + ); + } + const {MemoryTotal, MemoryUsed} = cluster; + if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) { + metricsCards.push( + , + ); + } + return metricsCards; + }; + + const getCards = () => { + if (loading) { + return null; + } + const cards = []; + + const nodesRoles = getNodesRolesInfo(cluster); + cards.push( + + + + + {nodesRoles?.length ? : null} + + , + ); + + if (Object.keys(groupStats).length) { + const tags = getStorageGroupStats(groupStats); + const total = getTotalStorageGroupsUsed(groupStats); + cards.push( + + + + + + , + ); + } + + const dataCenters = getDCInfo(cluster); + if (dataCenters?.length) { + cards.push( + + + + + + , + ); + } + + if (cluster.Tenants) { + cards.push( + + + , + ); + } + return cards; + }; + + return ( +
+ + + {getDoughnuts()} + +
{getCards()}
+
+
+ ); +} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx new file mode 100644 index 000000000..fc667ab81 --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx @@ -0,0 +1,61 @@ +import {Text} from '@gravity-ui/uikit'; + +import type {DiagnosticCardProps} from '../../../../components/DiagnosticCard/DiagnosticCard'; +import {DiagnosticCard} from '../../../../components/DiagnosticCard/DiagnosticCard'; +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {Skeleton} from '../../../../components/Skeleton/Skeleton'; +import type {ProgressStatus} from '../../../../utils/progress'; +import {b} from '../shared'; + +interface ClusterMetricsDougnutCardProps extends ClusterMetricsCommonCardProps { + status: ProgressStatus; + fillWidth: number; +} + +interface ClusterMetricsCommonCardProps { + children?: React.ReactNode; + title?: string; + size?: DiagnosticCardProps['size']; + className?: string; +} + +export function ClusterMetricsCard({ + children, + title, + size, + className, +}: ClusterMetricsCommonCardProps) { + return ( + + {title ? ( + + {title} + + ) : null} + {children} + + ); +} + +export function ClusterMetricsCardDoughnut({ + title, + children, + size, + ...rest +}: ClusterMetricsDougnutCardProps) { + return ( + + + {children} + + + ); +} + +export function ClusterMetricsCardSkeleton() { + return ( + + + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx new file mode 100644 index 000000000..5f80ba261 --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx @@ -0,0 +1,28 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatNumberCustom} from '../../../../utils/dataFormatters/dataFormatters'; +import i18n from '../../i18n'; +import type {ClusterMetricsCommonProps} from '../shared'; +import {useDiagramValues} from '../utils'; + +import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; + +interface ClusterMetricsCoresProps extends ClusterMetricsCommonProps {} + +function formatCoresLegend({value, capacity}: {value: number; capacity: number}) { + return `${formatNumberCustom(value)} / ${formatNumberCustom(capacity)}\n${i18n('context_cores')}`; +} + +export function ClusterMetricsCores({value, capacity, ...rest}: ClusterMetricsCoresProps) { + const {status, percents, legend, fill} = useDiagramValues({ + value, + capacity, + legendFormatter: formatCoresLegend, + ...rest, + }); + return ( + + {legend} + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx new file mode 100644 index 000000000..7959395bd --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx @@ -0,0 +1,30 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; +import i18n from '../../i18n'; +import type {ClusterMetricsCommonProps} from '../shared'; +import {useDiagramValues} from '../utils'; + +import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; + +interface ClusterMetricsMemoryProps extends ClusterMetricsCommonProps {} + +function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { + const formatted = formatStorageValues(value, capacity, undefined, '\n'); + return `${formatted[0]} / ${formatted[1]}`; +} + +export function ClusterMetricsMemory({value, capacity, ...rest}: ClusterMetricsMemoryProps) { + const {status, percents, legend, fill} = useDiagramValues({ + value, + capacity, + legendFormatter: formatStorageLegend, + ...rest, + }); + + return ( + + {legend} + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx new file mode 100644 index 000000000..07cb6e539 --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx @@ -0,0 +1,30 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; +import i18n from '../../i18n'; +import type {ClusterMetricsCommonProps} from '../shared'; +import {useDiagramValues} from '../utils'; + +import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; + +interface ClusterMetricsStorageProps extends ClusterMetricsCommonProps {} + +function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { + const formatted = formatStorageValues(value, capacity, undefined, '\n'); + return `${formatted[0]} / ${formatted[1]}`; +} + +export function ClusterMetricsStorage({value, capacity, ...rest}: ClusterMetricsStorageProps) { + const {status, percents, legend, fill} = useDiagramValues({ + value, + capacity, + legendFormatter: formatStorageLegend, + ...rest, + }); + + return ( + + {legend} + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/shared.ts b/src/containers/Cluster/ClusterDashboard/shared.ts new file mode 100644 index 000000000..23591c8fd --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/shared.ts @@ -0,0 +1,11 @@ +import {cn} from '../../../utils/cn'; +export const b = cn('ydb-cluster-dashboard'); + +export interface ClusterMetricsCommonProps { + value: number | string; + capacity: number | string; + colorizeProgress?: boolean; + inverseColorize?: boolean; + warningThreshold?: number; + dangerThreshold?: number; +} diff --git a/src/containers/Cluster/ClusterDashboard/utils.tsx b/src/containers/Cluster/ClusterDashboard/utils.tsx new file mode 100644 index 000000000..823e04287 --- /dev/null +++ b/src/containers/Cluster/ClusterDashboard/utils.tsx @@ -0,0 +1,97 @@ +import React from 'react'; + +import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; +import {isClusterInfoV2} from '../../../types/api/cluster'; +import type {TClusterInfo} from '../../../types/api/cluster'; +import {formatNumber, formatPercent} from '../../../utils/dataFormatters/dataFormatters'; +import {calculateProgressStatus} from '../../../utils/progress'; +import {DiskGroupsErasureStats} from '../ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars'; + +import type {ClusterMetricsCommonProps} from './shared'; + +export function useDiagramValues({ + value, + capacity, + colorizeProgress = true, + warningThreshold = 60, + dangerThreshold = 80, + inverseColorize = false, + legendFormatter, +}: ClusterMetricsCommonProps & { + legendFormatter: (params: {value: number; capacity: number}) => string; +}) { + const parsedValue = parseFloat(String(value)); + const parsedCapacity = parseFloat(String(capacity)); + let fillWidth = (parsedValue / parsedCapacity) * 100 || 0; + fillWidth = fillWidth > 100 ? 100 : fillWidth; + const normalizedFillWidth = fillWidth < 1 ? 0.5 : fillWidth; + const status = calculateProgressStatus({ + fillWidth, + warningThreshold, + dangerThreshold, + colorizeProgress, + inverseColorize, + }); + + const percents = formatPercent(fillWidth / 100); + const legend = legendFormatter({ + value: parsedValue, + capacity: parsedCapacity, + }); + + return {status, percents, legend, fill: normalizedFillWidth}; +} + +export function getDCInfo(cluster: TClusterInfo) { + if (isClusterInfoV2(cluster) && cluster.MapDataCenters) { + return Object.keys(cluster.MapDataCenters); + } + return cluster.DataCenters?.filter(Boolean); +} + +const rolesToShow = ['storage', 'tenant']; + +export function getNodesRolesInfo(cluster: TClusterInfo) { + const nodesRoles: React.ReactNode[] = []; + if (isClusterInfoV2(cluster) && cluster.MapNodeRoles) { + for (const [role, count] of Object.entries(cluster.MapNodeRoles)) { + if (rolesToShow.includes(role.toLowerCase())) { + nodesRoles.push( + + {role}: {formatNumber(count)} + , + ); + } + } + } + return nodesRoles; +} + +export function getStorageGroupStats(groupStats: ClusterGroupsStats) { + const result: React.ReactNode[] = []; + + Object.entries(groupStats).forEach(([storageType, stats]) => { + Object.values(stats).forEach((erasureStats) => { + result.push( + + {storageType}: {formatNumber(erasureStats.createdGroups)} /{' '} + {formatNumber(erasureStats.totalGroups)} + , + ); + }); + }); + return result; +} + +export const getTotalStorageGroupsUsed = (groupStats: ClusterGroupsStats) => { + return Object.values(groupStats).reduce((acc, data) => { + Object.values(data).forEach((erasureStats) => { + acc += erasureStats.createdGroups; + }); + + return acc; + }, 0); +}; diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss index e8e3d2944..8db9a954b 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss @@ -1,7 +1,7 @@ @import '../../../styles/mixins'; .cluster-info { - padding-top: 20px; + padding: 20px 0; &__skeleton { margin-top: 5px; @@ -11,16 +11,6 @@ @include body-2-typography(); } - &__system-tablets { - display: flex; - flex-wrap: wrap; - align-items: center; - - & .tablet { - margin-top: 2px; - } - } - &__metrics { margin: 0 -15px; padding: 0 15px !important; @@ -53,11 +43,4 @@ margin-left: 5px; } - &__dc-count { - text-transform: lowercase; - } - &__nodes-states { - display: flex; - gap: var(--g-spacing-half); - } } diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx index c777365e1..d640abfd2 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx @@ -1,24 +1,19 @@ import {ResponseError} from '../../../components/Errors/ResponseError'; import {InfoViewer} from '../../../components/InfoViewer/InfoViewer'; import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; -import {backend, customBackend} from '../../../store'; -import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; import type {AdditionalClusterProps} from '../../../types/additionalProps'; import type {TClusterInfo} from '../../../types/api/cluster'; import type {IResponseError} from '../../../types/api/error'; import type {VersionToColorMap} from '../../../types/versions'; -import {DEVELOPER_UI_TITLE} from '../../../utils/constants'; -import {useTypedSelector} from '../../../utils/hooks'; import {b} from './shared'; -import {getInfo, useGetVersionValues} from './utils'; +import {getInfo} from './utils'; import './ClusterInfo.scss'; interface ClusterInfoProps { cluster?: TClusterInfo; versionToColor?: VersionToColorMap; - groupsStats?: ClusterGroupsStats; loading?: boolean; error?: IResponseError; additionalClusterProps?: AdditionalClusterProps; @@ -26,28 +21,13 @@ interface ClusterInfoProps { export const ClusterInfo = ({ cluster, - versionToColor, - groupsStats = {}, loading, error, additionalClusterProps = {}, }: ClusterInfoProps) => { - const singleClusterMode = useTypedSelector((state) => state.singleClusterMode); - - const versionsValues = useGetVersionValues(cluster, versionToColor); - - let internalLink = backend + '/internal'; - - if (singleClusterMode && !customBackend) { - internalLink = `/internal`; - } - const {info = [], links = []} = additionalClusterProps; - const clusterInfo = getInfo(cluster ?? {}, versionsValues, groupsStats, info, [ - {title: DEVELOPER_UI_TITLE, url: internalLink}, - ...links, - ]); + const clusterInfo = getInfo(cluster ?? {}, info, links); const getContent = () => { if (loading) { diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss index 1e7378a20..92647ff85 100644 --- a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss +++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss @@ -1,10 +1,5 @@ .ydb-disk-groups-stats { - display: flex; - flex-direction: column; - gap: var(--g-spacing-3); - &__bar { - cursor: pointer; - } + cursor: pointer; &__popup-content { padding: var(--g-spacing-3); } diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx index d9013dced..f21d99528 100644 --- a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx +++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx @@ -1,10 +1,7 @@ +import {DefinitionList} from '@gravity-ui/components'; + import {ContentWithPopup} from '../../../../../components/ContentWithPopup/ContentWithPopup'; -import {InfoViewer} from '../../../../../components/InfoViewer'; -import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer'; -import type { - DiskErasureGroupsStats, - DiskGroupsStats, -} from '../../../../../store/reducers/cluster/types'; +import type {DiskErasureGroupsStats} from '../../../../../store/reducers/cluster/types'; import {formatBytes, getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers'; import {cn} from '../../../../../utils/cn'; import i18n from '../../../i18n'; @@ -14,25 +11,20 @@ import './DiskGroupsStatsBars.scss'; const b = cn('ydb-disk-groups-stats'); interface DiskGroupsStatsProps { - stats: DiskGroupsStats; + stats: DiskErasureGroupsStats; + children: React.ReactNode; } -export const DiskGroupsStatsBars = ({stats}: DiskGroupsStatsProps) => { +export const DiskGroupsErasureStats = ({stats, children}: DiskGroupsStatsProps) => { return (
- {Object.values(stats).map((erasureStats) => ( - } - > - - - ))} + } + > + {children} +
); }; @@ -53,26 +45,26 @@ function GroupsStatsPopupContent({stats}: GroupsStatsPopupContentProps) { const info = [ { - label: i18n('disk-type'), - value: diskType, + name: i18n('disk-type'), + content: diskType, }, { - label: i18n('erasure'), - value: erasure, + name: i18n('erasure'), + content: erasure, }, { - label: i18n('allocated'), - value: convertedAllocatedSize, + name: i18n('allocated'), + content: convertedAllocatedSize, }, { - label: i18n('available'), - value: convertedAvailableSize, + name: i18n('available'), + content: convertedAvailableSize, }, { - label: i18n('usage'), - value: usage + '%', + name: i18n('usage'), + content: usage + '%', }, ]; - return ; + return ; } diff --git a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss index e32f77726..be7cee3e1 100644 --- a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss +++ b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss @@ -5,11 +5,12 @@ justify-content: center; align-items: center; - min-width: 30px; + width: max-content; + min-width: 26px; + height: 20px; padding: 0 var(--g-spacing-1); color: var(--entity-state-font-color); - border: 1px solid var(--entity-state-border-color); border-radius: var(--g-spacing-1); background-color: var(--entity-state-background-color); @include entity-state-colors(); diff --git a/src/containers/Cluster/ClusterInfo/utils.tsx b/src/containers/Cluster/ClusterInfo/utils.tsx index f3f56352c..85fe01d5e 100644 --- a/src/containers/Cluster/ClusterInfo/utils.tsx +++ b/src/containers/Cluster/ClusterInfo/utils.tsx @@ -1,30 +1,22 @@ import React from 'react'; -import {skipToken} from '@reduxjs/toolkit/query'; +import {Flex} from '@gravity-ui/uikit'; import type {InfoViewerItem} from '../../../components/InfoViewer/InfoViewer'; import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer'; -import {Tablet} from '../../../components/Tablet'; import {Tags} from '../../../components/Tags'; -import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; -import {nodesApi} from '../../../store/reducers/nodes/nodes'; import type {ClusterLink} from '../../../types/additionalProps'; import {isClusterInfoV2} from '../../../types/api/cluster'; import type {TClusterInfo} from '../../../types/api/cluster'; import type {EFlag} from '../../../types/api/enums'; import type {TTabletStateInfo} from '../../../types/api/tablet'; import {EType} from '../../../types/api/tablet'; -import type {VersionToColorMap, VersionValue} from '../../../types/versions'; -import {formatStorageValues} from '../../../utils/dataFormatters/dataFormatters'; -import {parseNodeGroupsToVersionsValues, parseNodesToVersionsValues} from '../../../utils/versions'; -import {VersionsBar} from '../VersionsBar/VersionsBar'; +import {formatNumber} from '../../../utils/dataFormatters/dataFormatters'; import i18n from '../i18n'; -import {DiskGroupsStatsBars} from './components/DiskGroupsStatsBars/DiskGroupsStatsBars'; import {NodesState} from './components/NodesState/NodesState'; import {b} from './shared'; - const COLORS_PRIORITY: Record = { Green: 5, Blue: 4, @@ -46,92 +38,24 @@ export const compareTablets = (tablet1: TTabletStateInfo, tablet2: TTabletStateI return 0; }; -const getGroupsStatsFields = (groupsStats: ClusterGroupsStats) => { - return Object.keys(groupsStats).map((diskType) => { - return { - label: i18n('storage-groups', {diskType}), - value: , - }; - }); -}; - const getDCInfo = (cluster: TClusterInfo) => { if (isClusterInfoV2(cluster) && cluster.MapDataCenters) { return Object.entries(cluster.MapDataCenters).map(([dc, count]) => ( - {dc}: {i18n('quantity', {count})} + {dc}: {formatNumber(count)} )); } - return cluster.DataCenters?.filter(Boolean); -}; - -const getStorageStats = (cluster: TClusterInfo) => { - if (isClusterInfoV2(cluster) && cluster.MapDataCenters) { - const {MapStorageTotal, MapStorageUsed} = cluster; - const storageTypesSet = new Set( - Object.keys(MapStorageTotal ?? []).concat(Object.keys(MapStorageUsed ?? [])), - ); - if (storageTypesSet.size > 0) { - return Array.from(storageTypesSet).reduce( - (acc, storageType) => { - acc[storageType] = { - used: MapStorageUsed?.[storageType], - total: MapStorageTotal?.[storageType], - }; - return acc; - }, - {} as Record, - ); - } - } - return {_default: {used: cluster?.StorageUsed, total: cluster?.StorageTotal}}; + return undefined; }; export const getInfo = ( cluster: TClusterInfo, - versionsValues: VersionValue[], - groupsStats: ClusterGroupsStats, additionalInfo: InfoViewerItem[], links: ClusterLink[], ) => { const info: InfoViewerItem[] = []; - const dataCenters = getDCInfo(cluster); - - if (dataCenters?.length) { - info.push({ - label: i18n('dc'), - value: , - }); - } - - if (cluster.SystemTablets) { - const tablets = cluster.SystemTablets.slice(0).sort(compareTablets); - info.push({ - label: i18n('tablets'), - value: ( -
- {tablets.map((tablet, tabletIndex) => ( - - ))} -
- ), - }); - } - - if (cluster.Tenants) { - info.push({ - label: i18n('databases'), - value: cluster.Tenants, - }); - } - - info.push({ - label: i18n('nodes'), - value: , - }); - if (isClusterInfoV2(cluster) && cluster.MapNodeStates) { const arrayNodesStates = Object.entries(cluster.MapNodeStates) as [EFlag, number][]; // sort stack to achieve order "green, orange, yellow, red, blue, grey" @@ -144,42 +68,28 @@ export const getInfo = ( ); }); info.push({ - label: i18n('nodes-state'), - value:
{nodesStates}
, + label: i18n('label_nodes-state'), + value: {nodesStates}, + }); + } + + const dataCenters = getDCInfo(cluster); + if (dataCenters?.length) { + info.push({ + label: i18n('label_dc'), + value: , }); } info.push({ - label: i18n('load'), + label: i18n('label_load'), value: , }); - const storageStats = getStorageStats(cluster); + info.push(...additionalInfo); - Object.entries(storageStats).forEach(([type, stats]) => { - let label = i18n('storage-size'); - if (type !== '_default') { - label += `, ${type}`; - } + if (links.length) { info.push({ - label: label, - value: ( - - ), - }); - }); - - if (Object.keys(groupsStats).length) { - info.push(...getGroupsStatsFields(groupsStats)); - } - - info.push( - ...additionalInfo, - { label: i18n('links'), value: (
@@ -188,51 +98,8 @@ export const getInfo = ( ))}
), - }, - { - label: i18n('versions'), - value: ( - el.title !== 'unknown')} - /> - ), - }, - ); + }); + } return info; }; - -export const useGetVersionValues = (cluster?: TClusterInfo, versionToColor?: VersionToColorMap) => { - const {currentData} = nodesApi.useGetNodesQuery( - isClusterInfoV2(cluster) - ? skipToken - : { - tablets: false, - fieldsRequired: ['SystemState'], - group: 'Version', - }, - ); - - const versionsValues = React.useMemo(() => { - if (isClusterInfoV2(cluster) && cluster.MapVersions) { - const groups = Object.entries(cluster.MapVersions).map(([version, count]) => ({ - name: version, - count, - })); - return parseNodeGroupsToVersionsValues(groups, versionToColor, cluster.NodesTotal); - } - if (!currentData) { - return []; - } - if (Array.isArray(currentData.NodeGroups)) { - return parseNodeGroupsToVersionsValues( - currentData.NodeGroups, - versionToColor, - cluster?.NodesTotal, - ); - } - return parseNodesToVersionsValues(currentData.Nodes, versionToColor); - }, [currentData, versionToColor, cluster]); - - return versionsValues; -}; diff --git a/src/containers/Cluster/i18n/en.json b/src/containers/Cluster/i18n/en.json index cf0eba22d..8ad4b9446 100644 --- a/src/containers/Cluster/i18n/en.json +++ b/src/containers/Cluster/i18n/en.json @@ -4,19 +4,18 @@ "allocated": "Allocated", "available": "Available", "usage": "Usage", - "dc": "DC", - "tablets": "Tablets", - "databases": "Databases", - "nodes": "Nodes", - "nodes-state": "Nodes state", - "load": "Load", + "label_nodes-state": "Nodes state", + "label_dc": "Nodes data centers", "storage-size": "Storage size", "storage-groups": "Storage groups, {{diskType}}", "links": "Links", - "versions": "Versions", - "quantity": { - "one": "{{count}} node", - "other": "{{count}} nodes", - "zero": "no nodes" - } + "context_cores": "cores", + "title_cpu": "CPU", + "title_storage": "Storage", + "title_memory": "Memory", + "label_nodes": "Nodes", + "label_hosts": "Hosts", + "label_storage-groups": "Storage groups", + "label_databases": "Databases", + "label_load": "Load" } diff --git a/src/types/api/cluster.ts b/src/types/api/cluster.ts index 6a4c3e0ed..c30a25efe 100644 --- a/src/types/api/cluster.ts +++ b/src/types/api/cluster.ts @@ -57,6 +57,9 @@ export interface TClusterInfoV2 extends TClusterInfoV1 { MapDataCenters?: { [key: string]: number; }; + MapNodeRoles?: { + [key: string]: number; + }; MapNodeStates?: Partial>; /** value is uint64 */ MapStorageTotal?: { diff --git a/src/utils/bytesParsers/formatBytes.ts b/src/utils/bytesParsers/formatBytes.ts index c5d474598..ccd9355ba 100644 --- a/src/utils/bytesParsers/formatBytes.ts +++ b/src/utils/bytesParsers/formatBytes.ts @@ -84,8 +84,8 @@ const formatToSize = ({value, size = 'mb', precision = 0}: FormatToSizeArgs) => return formatNumber(result); }; -const addSizeLabel = (result: string, size: BytesSizes) => { - return result + ` ${sizes[size].label}`; +const addSizeLabel = (result: string, size: BytesSizes, delimiter = '\xa0') => { + return result + delimiter + sizes[size].label; }; const addSpeedLabel = (result: string, size: BytesSizes) => { @@ -97,6 +97,7 @@ export type FormatBytesArgs = Omit & { withSpeedLabel?: boolean; withSizeLabel?: boolean; significantDigits?: number; + delimiter?: string; }; /** @@ -108,6 +109,7 @@ export const formatBytes = ({ withSpeedLabel = false, withSizeLabel = true, significantDigits = 0, + delimiter, ...params }: FormatBytesArgs) => { if (!isNumeric(value)) { @@ -125,7 +127,7 @@ export const formatBytes = ({ } if (withSizeLabel) { - return addSizeLabel(result, sizeToConvert); + return addSizeLabel(result, sizeToConvert, delimiter); } return result; diff --git a/src/utils/dataFormatters/dataFormatters.ts b/src/utils/dataFormatters/dataFormatters.ts index cea9b2cc4..ab567744d 100644 --- a/src/utils/dataFormatters/dataFormatters.ts +++ b/src/utils/dataFormatters/dataFormatters.ts @@ -52,7 +52,12 @@ export const formatMsToUptime = (ms?: number) => { return ms && formatUptime(ms / 1000); }; -export const formatStorageValues = (value?: number, total?: number, size?: BytesSizes) => { +export const formatStorageValues = ( + value?: number, + total?: number, + size?: BytesSizes, + delimiter?: string, +) => { let calculatedSize = getSizeWithSignificantDigits(Number(value), 0); let valueWithSizeLabel = true; let valuePrecision = 0; @@ -63,13 +68,28 @@ export const formatStorageValues = (value?: number, total?: number, size?: Bytes valuePrecision = 1; } - const formattedValue = formatBytesCustom({ + let formattedValue = formatBytesCustom({ value, withSizeLabel: valueWithSizeLabel, size: size || calculatedSize, precision: valuePrecision, }); - const formattedTotal = formatBytesCustom({value: total, size: size || calculatedSize}); + if (value && value > 0) { + while (formattedValue === '0') { + valuePrecision += 1; + formattedValue = formatBytesCustom({ + value, + withSizeLabel: valueWithSizeLabel, + size: size || calculatedSize, + precision: valuePrecision, + }); + } + } + const formattedTotal = formatBytesCustom({ + value: total, + size: size || calculatedSize, + delimiter, + }); return [formattedValue, formattedTotal]; }; @@ -90,6 +110,21 @@ export const formatNumber = (number?: unknown) => { // "," in format is delimiter sign, not delimiter itself return configuredNumeral(number).format('0,0.[00000]'); }; +export const formatNumberCustom = (number?: number) => { + return configuredNumeral(number).format('0.[0]a'); +}; +export const formatPercent = (number?: unknown) => { + if (!isNumeric(number)) { + return ''; + } + const configuredNumber = configuredNumeral(number); + const numberValue = configuredNumber.value(); + let format = '0.[0]%'; + if (numberValue && numberValue < 0.001) { + format = '0.[00]%'; + } + return configuredNumber.format(format); +}; export const formatSecondsToHours = (seconds: number) => { const hours = (seconds / HOUR_IN_SECONDS).toFixed(2); diff --git a/src/utils/numeral.ts b/src/utils/numeral.ts index 2c2b74669..3bd963ddc 100644 --- a/src/utils/numeral.ts +++ b/src/utils/numeral.ts @@ -6,7 +6,7 @@ import {Lang, i18n} from './i18n'; // Set space delimiter for all locales possible in project Object.values(Lang).forEach((value) => { if (numeral.locales[value]) { - numeral.locales[value].delimiters.thousands = ' '; + numeral.locales[value].delimiters.thousands = '\xa0'; } }); diff --git a/src/utils/progress.ts b/src/utils/progress.ts new file mode 100644 index 000000000..9c2dfa45a --- /dev/null +++ b/src/utils/progress.ts @@ -0,0 +1,27 @@ +export type ProgressStatus = 'good' | 'warning' | 'danger'; + +interface CalculateProgressStatusProps { + inverseColorize?: boolean; + dangerThreshold?: number; + warningThreshold?: number; + colorizeProgress?: boolean; + fillWidth: number; +} + +export function calculateProgressStatus({ + inverseColorize, + warningThreshold = 60, + dangerThreshold = 80, + colorizeProgress, + fillWidth, +}: CalculateProgressStatusProps) { + let status: ProgressStatus = inverseColorize ? 'danger' : 'good'; + if (colorizeProgress) { + if (fillWidth > warningThreshold && fillWidth <= dangerThreshold) { + status = 'warning'; + } else if (fillWidth > dangerThreshold) { + status = inverseColorize ? 'good' : 'danger'; + } + } + return status; +} From e1fc54c25a0ddf8d31d0280bafaf76d823f20c41 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 16 Oct 2024 17:43:01 +0300 Subject: [PATCH 2/4] fix: tests --- .../bytesParsers/__test__/formatBytes.test.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/utils/bytesParsers/__test__/formatBytes.test.ts b/src/utils/bytesParsers/__test__/formatBytes.test.ts index e5cf024fb..f9175c746 100644 --- a/src/utils/bytesParsers/__test__/formatBytes.test.ts +++ b/src/utils/bytesParsers/__test__/formatBytes.test.ts @@ -2,31 +2,37 @@ import {formatBytes} from '../formatBytes'; describe('formatBytes', () => { it('should work with only value', () => { - expect(formatBytes({value: 100})).toBe('100 B'); - expect(formatBytes({value: 100_000})).toBe('100 KB'); - expect(formatBytes({value: 100_000_000})).toBe('100 MB'); - expect(formatBytes({value: 100_000_000_000})).toBe('100 GB'); - expect(formatBytes({value: 100_000_000_000_000})).toBe('100 TB'); + expect(formatBytes({value: 100})).toBe('100\xa0B'); + expect(formatBytes({value: 100_000})).toBe('100\xa0KB'); + expect(formatBytes({value: 100_000_000})).toBe('100\xa0MB'); + expect(formatBytes({value: 100_000_000_000})).toBe('100\xa0GB'); + expect(formatBytes({value: 100_000_000_000_000})).toBe('100\xa0TB'); }); it('should convert to size', () => { - expect(formatBytes({value: 100_000, size: 'b'})).toBe('100 000 B'); - expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe('100 000 GB'); + expect(formatBytes({value: 100_000, size: 'b'})).toBe('100\xa0000\xa0B'); + expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe('100\xa0000\xa0GB'); }); it('should convert without labels', () => { - expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe('100 000'); + expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe('100\xa0000'); expect(formatBytes({value: 100_000_000_000_000, size: 'gb', withSizeLabel: false})).toBe( - '100 000', + '100\xa0000', ); }); it('should convert to speed', () => { - expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe('100 KB/s'); - expect(formatBytes({value: 100_000, size: 'b', withSpeedLabel: true})).toBe('100 000 B/s'); + expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe('100\xa0KB/s'); + expect(formatBytes({value: 100_000, size: 'b', withSpeedLabel: true})).toBe( + '100\xa0000\xa0B/s', + ); }); it('should return fixed amount of significant digits', () => { - expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual('99 000 B'); - expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual('100 KB'); - expect(formatBytes({value: 99_000_000_000_000, significantDigits: 2})).toEqual('99 000 GB'); - expect(formatBytes({value: 100_000_000_000_000, significantDigits: 2})).toEqual('100 TB'); + expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual('99\xa0000\xa0B'); + expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual('100\xa0KB'); + expect(formatBytes({value: 99_000_000_000_000, significantDigits: 2})).toEqual( + '99\xa0000\xa0GB', + ); + expect(formatBytes({value: 100_000_000_000_000, significantDigits: 2})).toEqual( + '100\xa0TB', + ); }); it('should return empty string on invalid data', () => { expect(formatBytes({value: undefined})).toEqual(''); @@ -36,12 +42,12 @@ describe('formatBytes', () => { expect(formatBytes({value: '123qwe'})).toEqual(''); }); it('should work with precision', () => { - expect(formatBytes({value: 123.123, precision: 2})).toBe('123 B'); - expect(formatBytes({value: 12.123, precision: 2})).toBe('12 B'); - expect(formatBytes({value: 1.123, precision: 2})).toBe('1.1 B'); - expect(formatBytes({value: 0.123, precision: 2})).toBe('0.12 B'); - expect(formatBytes({value: 0.012, precision: 2})).toBe('0.01 B'); - expect(formatBytes({value: 0.001, precision: 2})).toBe('0 B'); - expect(formatBytes({value: 0, precision: 2})).toBe('0 B'); + expect(formatBytes({value: 123.123, precision: 2})).toBe('123\xa0B'); + expect(formatBytes({value: 12.123, precision: 2})).toBe('12\xa0B'); + expect(formatBytes({value: 1.123, precision: 2})).toBe('1.1\xa0B'); + expect(formatBytes({value: 0.123, precision: 2})).toBe('0.12\xa0B'); + expect(formatBytes({value: 0.012, precision: 2})).toBe('0.01\xa0B'); + expect(formatBytes({value: 0.001, precision: 2})).toBe('0\xa0B'); + expect(formatBytes({value: 0, precision: 2})).toBe('0\xa0B'); }); }); From f42551f0188345e260a60f53f93379ec80b73e8d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 17 Oct 2024 18:02:09 +0300 Subject: [PATCH 3/4] fix: review --- .../ClusterDashboard/ClusterDashboard.scss | 2 +- .../ClusterDashboard/ClusterDashboard.tsx | 162 +++++++++--------- .../components/ClusterMetricsCard.tsx | 16 +- .../bytesParsers/__test__/formatBytes.test.ts | 57 +++--- src/utils/bytesParsers/formatBytes.ts | 4 +- .../__test__/formatStorageValues.test.ts | 44 +++++ src/utils/numeral.ts | 3 +- src/utils/utils.ts | 2 + 8 files changed, 182 insertions(+), 108 deletions(-) create mode 100644 src/utils/dataFormatters/__test__/formatStorageValues.test.ts diff --git a/src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss b/src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss index 82080d635..86444b149 100644 --- a/src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss +++ b/src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss @@ -21,7 +21,7 @@ border: unset; } - &__doughtnut { + &__doughnut { margin-top: auto; } &__cards { diff --git a/src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx b/src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx index 3490b1950..f4922f404 100644 --- a/src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx +++ b/src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx @@ -8,7 +8,7 @@ import {valueIsDefined} from '../../../utils'; import {formatNumber} from '../../../utils/dataFormatters/dataFormatters'; import i18n from '../i18n'; -import {ClusterMetricsCard, ClusterMetricsCardSkeleton} from './components/ClusterMetricsCard'; +import {ClusterDashboardSkeleton, ClusterMetricsCard} from './components/ClusterMetricsCard'; import {ClusterMetricsCores} from './components/ClusterMetricsCores'; import {ClusterMetricsMemory} from './components/ClusterMetricsMemory'; import {ClusterMetricsStorage} from './components/ClusterMetricsStorage'; @@ -43,95 +43,97 @@ interface ClusterDashboardProps { loading?: boolean; } -export function ClusterDashboard({cluster, groupStats = {}, loading}: ClusterDashboardProps) { - const getDoughnuts = () => { - if (loading) { - return Array.from('123').map((el) => ); - } - const metricsCards = []; - if (isClusterInfoV2(cluster)) { - const {CoresUsed, NumberOfCpus} = cluster; - if (valueIsDefined(CoresUsed) && valueIsDefined(NumberOfCpus)) { - metricsCards.push( - , - ); - } - } - const {StorageTotal, StorageUsed} = cluster; - if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) { - metricsCards.push( - , - ); - } - const {MemoryTotal, MemoryUsed} = cluster; - if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) { +export function ClusterDashboard(props: ClusterDashboardProps) { + return ( +
+ + + + +
+ +
+
+
+ ); +} + +function ClusterDoughnuts({cluster, loading}: ClusterDashboardProps) { + if (loading) { + return ; + } + const metricsCards = []; + if (isClusterInfoV2(cluster)) { + const {CoresUsed, NumberOfCpus} = cluster; + if (valueIsDefined(CoresUsed) && valueIsDefined(NumberOfCpus)) { metricsCards.push( - , + , ); } - return metricsCards; - }; + } + const {StorageTotal, StorageUsed} = cluster; + if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) { + metricsCards.push( + , + ); + } + const {MemoryTotal, MemoryUsed} = cluster; + if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) { + metricsCards.push( + , + ); + } + return metricsCards; +} - const getCards = () => { - if (loading) { - return null; - } - const cards = []; +function ClusterDashboardCards({cluster, groupStats = {}, loading}: ClusterDashboardProps) { + if (loading) { + return null; + } + const cards = []; + + const nodesRoles = getNodesRolesInfo(cluster); + cards.push( + + + + + {nodesRoles?.length ? : null} + + , + ); - const nodesRoles = getNodesRolesInfo(cluster); + if (Object.keys(groupStats).length) { + const tags = getStorageGroupStats(groupStats); + const total = getTotalStorageGroupsUsed(groupStats); cards.push( - + - - - {nodesRoles?.length ? : null} + + , ); + } - if (Object.keys(groupStats).length) { - const tags = getStorageGroupStats(groupStats); - const total = getTotalStorageGroupsUsed(groupStats); - cards.push( - - - - - - , - ); - } - - const dataCenters = getDCInfo(cluster); - if (dataCenters?.length) { - cards.push( - - - - - - , - ); - } - - if (cluster.Tenants) { - cards.push( - - - , - ); - } - return cards; - }; - - return ( -
- - - {getDoughnuts()} + const dataCenters = getDCInfo(cluster); + if (dataCenters?.length) { + cards.push( + + + + -
{getCards()}
-
-
- ); +
, + ); + } + + if (cluster.Tenants) { + cards.push( + + + , + ); + } + return cards; } diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx index fc667ab81..60fe1bd6b 100644 --- a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx +++ b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import {Text} from '@gravity-ui/uikit'; import type {DiagnosticCardProps} from '../../../../components/DiagnosticCard/DiagnosticCard'; @@ -45,17 +47,27 @@ export function ClusterMetricsCardDoughnut({ }: ClusterMetricsDougnutCardProps) { return ( - + {children} ); } -export function ClusterMetricsCardSkeleton() { +function ClusterMetricsCardSkeleton() { return ( ); } + +export function ClusterDashboardSkeleton() { + return ( + + + + + + ); +} diff --git a/src/utils/bytesParsers/__test__/formatBytes.test.ts b/src/utils/bytesParsers/__test__/formatBytes.test.ts index f9175c746..491168c5b 100644 --- a/src/utils/bytesParsers/__test__/formatBytes.test.ts +++ b/src/utils/bytesParsers/__test__/formatBytes.test.ts @@ -1,37 +1,50 @@ +import {unbreakableGap} from '../../utils'; import {formatBytes} from '../formatBytes'; describe('formatBytes', () => { it('should work with only value', () => { - expect(formatBytes({value: 100})).toBe('100\xa0B'); - expect(formatBytes({value: 100_000})).toBe('100\xa0KB'); - expect(formatBytes({value: 100_000_000})).toBe('100\xa0MB'); - expect(formatBytes({value: 100_000_000_000})).toBe('100\xa0GB'); - expect(formatBytes({value: 100_000_000_000_000})).toBe('100\xa0TB'); + expect(formatBytes({value: 100})).toBe(`100${unbreakableGap}B`); + expect(formatBytes({value: 100_000})).toBe(`100${unbreakableGap}KB`); + expect(formatBytes({value: 100_000_000})).toBe(`100${unbreakableGap}MB`); + expect(formatBytes({value: 100_000_000_000})).toBe(`100${unbreakableGap}GB`); + expect(formatBytes({value: 100_000_000_000_000})).toBe(`100${unbreakableGap}TB`); }); it('should convert to size', () => { - expect(formatBytes({value: 100_000, size: 'b'})).toBe('100\xa0000\xa0B'); - expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe('100\xa0000\xa0GB'); + expect(formatBytes({value: 100_000, size: 'b'})).toBe( + `100${unbreakableGap}000${unbreakableGap}B`, + ); + expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe( + `100${unbreakableGap}000${unbreakableGap}GB`, + ); }); it('should convert without labels', () => { - expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe('100\xa0000'); + expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe( + `100${unbreakableGap}000`, + ); expect(formatBytes({value: 100_000_000_000_000, size: 'gb', withSizeLabel: false})).toBe( - '100\xa0000', + `100${unbreakableGap}000`, ); }); it('should convert to speed', () => { - expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe('100\xa0KB/s'); + expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe( + `100${unbreakableGap}KB/s`, + ); expect(formatBytes({value: 100_000, size: 'b', withSpeedLabel: true})).toBe( - '100\xa0000\xa0B/s', + `100${unbreakableGap}000${unbreakableGap}B/s`, ); }); it('should return fixed amount of significant digits', () => { - expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual('99\xa0000\xa0B'); - expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual('100\xa0KB'); + expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual( + `99${unbreakableGap}000${unbreakableGap}B`, + ); + expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual( + `100${unbreakableGap}KB`, + ); expect(formatBytes({value: 99_000_000_000_000, significantDigits: 2})).toEqual( - '99\xa0000\xa0GB', + `99${unbreakableGap}000${unbreakableGap}GB`, ); expect(formatBytes({value: 100_000_000_000_000, significantDigits: 2})).toEqual( - '100\xa0TB', + `100${unbreakableGap}TB`, ); }); it('should return empty string on invalid data', () => { @@ -42,12 +55,12 @@ describe('formatBytes', () => { expect(formatBytes({value: '123qwe'})).toEqual(''); }); it('should work with precision', () => { - expect(formatBytes({value: 123.123, precision: 2})).toBe('123\xa0B'); - expect(formatBytes({value: 12.123, precision: 2})).toBe('12\xa0B'); - expect(formatBytes({value: 1.123, precision: 2})).toBe('1.1\xa0B'); - expect(formatBytes({value: 0.123, precision: 2})).toBe('0.12\xa0B'); - expect(formatBytes({value: 0.012, precision: 2})).toBe('0.01\xa0B'); - expect(formatBytes({value: 0.001, precision: 2})).toBe('0\xa0B'); - expect(formatBytes({value: 0, precision: 2})).toBe('0\xa0B'); + expect(formatBytes({value: 123.123, precision: 2})).toBe(`123${unbreakableGap}B`); + expect(formatBytes({value: 12.123, precision: 2})).toBe(`12${unbreakableGap}B`); + expect(formatBytes({value: 1.123, precision: 2})).toBe(`1.1${unbreakableGap}B`); + expect(formatBytes({value: 0.123, precision: 2})).toBe(`0.12${unbreakableGap}B`); + expect(formatBytes({value: 0.012, precision: 2})).toBe(`0.01${unbreakableGap}B`); + expect(formatBytes({value: 0.001, precision: 2})).toBe(`0${unbreakableGap}B`); + expect(formatBytes({value: 0, precision: 2})).toBe(`0${unbreakableGap}B`); }); }); diff --git a/src/utils/bytesParsers/formatBytes.ts b/src/utils/bytesParsers/formatBytes.ts index ccd9355ba..ea0678517 100644 --- a/src/utils/bytesParsers/formatBytes.ts +++ b/src/utils/bytesParsers/formatBytes.ts @@ -1,6 +1,6 @@ import {GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE} from '../constants'; import {formatNumber, roundToPrecision} from '../dataFormatters/dataFormatters'; -import {isNumeric} from '../utils'; +import {isNumeric, unbreakableGap} from '../utils'; import i18n from './i18n'; @@ -84,7 +84,7 @@ const formatToSize = ({value, size = 'mb', precision = 0}: FormatToSizeArgs) => return formatNumber(result); }; -const addSizeLabel = (result: string, size: BytesSizes, delimiter = '\xa0') => { +const addSizeLabel = (result: string, size: BytesSizes, delimiter = unbreakableGap) => { return result + delimiter + sizes[size].label; }; diff --git a/src/utils/dataFormatters/__test__/formatStorageValues.test.ts b/src/utils/dataFormatters/__test__/formatStorageValues.test.ts new file mode 100644 index 000000000..3d533246c --- /dev/null +++ b/src/utils/dataFormatters/__test__/formatStorageValues.test.ts @@ -0,0 +1,44 @@ +import {unbreakableGap} from '../../utils'; +import {formatStorageValues} from '../dataFormatters'; + +describe('formatStorageValues', () => { + it('should return ["", ""] when both value and total are undefined', () => { + const result = formatStorageValues(); + expect(result).toEqual(['', '']); + }); + + it('should format value correctly when total is undefined', () => { + const result = formatStorageValues(1024); + expect(result).toEqual([`1${unbreakableGap}KB`, '']); + }); + + it('should format total correctly when value is undefined', () => { + const result = formatStorageValues(undefined, 2048); + expect(result).toEqual(['', `2${unbreakableGap}KB`]); + }); + + it('should format both value and total correctly', () => { + const result = formatStorageValues(1024, 2048); + expect(result).toEqual(['1', `2${unbreakableGap}KB`]); + }); + + it('should handle small value compared to total and increase precision', () => { + const result = formatStorageValues(1, 1024); + expect(result).toEqual(['0.001', `1${unbreakableGap}KB`]); + }); + + it('should return ["0", formattedTotal] when value is 0', () => { + const result = formatStorageValues(0, 2048); + expect(result).toEqual(['0', `2${unbreakableGap}KB`]); + }); + + it('should use provided size and delimiter', () => { + const result = formatStorageValues(5120, 10240, 'mb', '/'); + expect(result).toEqual(['0.01', '0/MB']); + }); + + it('should handle non-numeric total gracefully', () => { + const result = formatStorageValues(2048, 'Not a number' as any); + expect(result).toEqual([`2${unbreakableGap}KB`, '']); + }); +}); diff --git a/src/utils/numeral.ts b/src/utils/numeral.ts index 3bd963ddc..19b9daafe 100644 --- a/src/utils/numeral.ts +++ b/src/utils/numeral.ts @@ -2,11 +2,12 @@ import numeral from 'numeral'; import 'numeral/locales'; // Without this numeral will throw an error when using not 'en' locale import {Lang, i18n} from './i18n'; +import {unbreakableGap} from './utils'; // Set space delimiter for all locales possible in project Object.values(Lang).forEach((value) => { if (numeral.locales[value]) { - numeral.locales[value].delimiters.thousands = '\xa0'; + numeral.locales[value].delimiters.thousands = unbreakableGap; } }); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f9d038b1e..d02017417 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -104,3 +104,5 @@ export function isNumeric(value?: unknown): value is number | string { export function toExponential(value: number, precision?: number) { return Number(value).toExponential(precision); } + +export const unbreakableGap = '\xa0'; From 7eb74addea914bbfebd6f27ab61d8f319d8ce460 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 21 Oct 2024 13:08:17 +0300 Subject: [PATCH 4/4] fix: review --- .../bytesParsers/__test__/formatBytes.test.ts | 46 +++++++++---------- src/utils/bytesParsers/formatBytes.ts | 4 +- .../__test__/formatStorageValues.test.ts | 14 +++--- src/utils/numeral.ts | 4 +- src/utils/utils.ts | 2 +- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/utils/bytesParsers/__test__/formatBytes.test.ts b/src/utils/bytesParsers/__test__/formatBytes.test.ts index 491168c5b..1a64ad3f0 100644 --- a/src/utils/bytesParsers/__test__/formatBytes.test.ts +++ b/src/utils/bytesParsers/__test__/formatBytes.test.ts @@ -1,50 +1,50 @@ -import {unbreakableGap} from '../../utils'; +import {UNBREAKABLE_GAP} from '../../utils'; import {formatBytes} from '../formatBytes'; describe('formatBytes', () => { it('should work with only value', () => { - expect(formatBytes({value: 100})).toBe(`100${unbreakableGap}B`); - expect(formatBytes({value: 100_000})).toBe(`100${unbreakableGap}KB`); - expect(formatBytes({value: 100_000_000})).toBe(`100${unbreakableGap}MB`); - expect(formatBytes({value: 100_000_000_000})).toBe(`100${unbreakableGap}GB`); - expect(formatBytes({value: 100_000_000_000_000})).toBe(`100${unbreakableGap}TB`); + expect(formatBytes({value: 100})).toBe(`100${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 100_000})).toBe(`100${UNBREAKABLE_GAP}KB`); + expect(formatBytes({value: 100_000_000})).toBe(`100${UNBREAKABLE_GAP}MB`); + expect(formatBytes({value: 100_000_000_000})).toBe(`100${UNBREAKABLE_GAP}GB`); + expect(formatBytes({value: 100_000_000_000_000})).toBe(`100${UNBREAKABLE_GAP}TB`); }); it('should convert to size', () => { expect(formatBytes({value: 100_000, size: 'b'})).toBe( - `100${unbreakableGap}000${unbreakableGap}B`, + `100${UNBREAKABLE_GAP}000${UNBREAKABLE_GAP}B`, ); expect(formatBytes({value: 100_000_000_000_000, size: 'gb'})).toBe( - `100${unbreakableGap}000${unbreakableGap}GB`, + `100${UNBREAKABLE_GAP}000${UNBREAKABLE_GAP}GB`, ); }); it('should convert without labels', () => { expect(formatBytes({value: 100_000, size: 'b', withSizeLabel: false})).toBe( - `100${unbreakableGap}000`, + `100${UNBREAKABLE_GAP}000`, ); expect(formatBytes({value: 100_000_000_000_000, size: 'gb', withSizeLabel: false})).toBe( - `100${unbreakableGap}000`, + `100${UNBREAKABLE_GAP}000`, ); }); it('should convert to speed', () => { expect(formatBytes({value: 100_000, withSpeedLabel: true})).toBe( - `100${unbreakableGap}KB/s`, + `100${UNBREAKABLE_GAP}KB/s`, ); expect(formatBytes({value: 100_000, size: 'b', withSpeedLabel: true})).toBe( - `100${unbreakableGap}000${unbreakableGap}B/s`, + `100${UNBREAKABLE_GAP}000${UNBREAKABLE_GAP}B/s`, ); }); it('should return fixed amount of significant digits', () => { expect(formatBytes({value: 99_000, significantDigits: 2})).toEqual( - `99${unbreakableGap}000${unbreakableGap}B`, + `99${UNBREAKABLE_GAP}000${UNBREAKABLE_GAP}B`, ); expect(formatBytes({value: 100_000, significantDigits: 2})).toEqual( - `100${unbreakableGap}KB`, + `100${UNBREAKABLE_GAP}KB`, ); expect(formatBytes({value: 99_000_000_000_000, significantDigits: 2})).toEqual( - `99${unbreakableGap}000${unbreakableGap}GB`, + `99${UNBREAKABLE_GAP}000${UNBREAKABLE_GAP}GB`, ); expect(formatBytes({value: 100_000_000_000_000, significantDigits: 2})).toEqual( - `100${unbreakableGap}TB`, + `100${UNBREAKABLE_GAP}TB`, ); }); it('should return empty string on invalid data', () => { @@ -55,12 +55,12 @@ describe('formatBytes', () => { expect(formatBytes({value: '123qwe'})).toEqual(''); }); it('should work with precision', () => { - expect(formatBytes({value: 123.123, precision: 2})).toBe(`123${unbreakableGap}B`); - expect(formatBytes({value: 12.123, precision: 2})).toBe(`12${unbreakableGap}B`); - expect(formatBytes({value: 1.123, precision: 2})).toBe(`1.1${unbreakableGap}B`); - expect(formatBytes({value: 0.123, precision: 2})).toBe(`0.12${unbreakableGap}B`); - expect(formatBytes({value: 0.012, precision: 2})).toBe(`0.01${unbreakableGap}B`); - expect(formatBytes({value: 0.001, precision: 2})).toBe(`0${unbreakableGap}B`); - expect(formatBytes({value: 0, precision: 2})).toBe(`0${unbreakableGap}B`); + expect(formatBytes({value: 123.123, precision: 2})).toBe(`123${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 12.123, precision: 2})).toBe(`12${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 1.123, precision: 2})).toBe(`1.1${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 0.123, precision: 2})).toBe(`0.12${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 0.012, precision: 2})).toBe(`0.01${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 0.001, precision: 2})).toBe(`0${UNBREAKABLE_GAP}B`); + expect(formatBytes({value: 0, precision: 2})).toBe(`0${UNBREAKABLE_GAP}B`); }); }); diff --git a/src/utils/bytesParsers/formatBytes.ts b/src/utils/bytesParsers/formatBytes.ts index ea0678517..f6f9eca15 100644 --- a/src/utils/bytesParsers/formatBytes.ts +++ b/src/utils/bytesParsers/formatBytes.ts @@ -1,6 +1,6 @@ import {GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE} from '../constants'; import {formatNumber, roundToPrecision} from '../dataFormatters/dataFormatters'; -import {isNumeric, unbreakableGap} from '../utils'; +import {UNBREAKABLE_GAP, isNumeric} from '../utils'; import i18n from './i18n'; @@ -84,7 +84,7 @@ const formatToSize = ({value, size = 'mb', precision = 0}: FormatToSizeArgs) => return formatNumber(result); }; -const addSizeLabel = (result: string, size: BytesSizes, delimiter = unbreakableGap) => { +const addSizeLabel = (result: string, size: BytesSizes, delimiter = UNBREAKABLE_GAP) => { return result + delimiter + sizes[size].label; }; diff --git a/src/utils/dataFormatters/__test__/formatStorageValues.test.ts b/src/utils/dataFormatters/__test__/formatStorageValues.test.ts index 3d533246c..6d5a6ebf4 100644 --- a/src/utils/dataFormatters/__test__/formatStorageValues.test.ts +++ b/src/utils/dataFormatters/__test__/formatStorageValues.test.ts @@ -1,4 +1,4 @@ -import {unbreakableGap} from '../../utils'; +import {UNBREAKABLE_GAP} from '../../utils'; import {formatStorageValues} from '../dataFormatters'; describe('formatStorageValues', () => { @@ -9,27 +9,27 @@ describe('formatStorageValues', () => { it('should format value correctly when total is undefined', () => { const result = formatStorageValues(1024); - expect(result).toEqual([`1${unbreakableGap}KB`, '']); + expect(result).toEqual([`1${UNBREAKABLE_GAP}KB`, '']); }); it('should format total correctly when value is undefined', () => { const result = formatStorageValues(undefined, 2048); - expect(result).toEqual(['', `2${unbreakableGap}KB`]); + expect(result).toEqual(['', `2${UNBREAKABLE_GAP}KB`]); }); it('should format both value and total correctly', () => { const result = formatStorageValues(1024, 2048); - expect(result).toEqual(['1', `2${unbreakableGap}KB`]); + expect(result).toEqual(['1', `2${UNBREAKABLE_GAP}KB`]); }); it('should handle small value compared to total and increase precision', () => { const result = formatStorageValues(1, 1024); - expect(result).toEqual(['0.001', `1${unbreakableGap}KB`]); + expect(result).toEqual(['0.001', `1${UNBREAKABLE_GAP}KB`]); }); it('should return ["0", formattedTotal] when value is 0', () => { const result = formatStorageValues(0, 2048); - expect(result).toEqual(['0', `2${unbreakableGap}KB`]); + expect(result).toEqual(['0', `2${UNBREAKABLE_GAP}KB`]); }); it('should use provided size and delimiter', () => { @@ -39,6 +39,6 @@ describe('formatStorageValues', () => { it('should handle non-numeric total gracefully', () => { const result = formatStorageValues(2048, 'Not a number' as any); - expect(result).toEqual([`2${unbreakableGap}KB`, '']); + expect(result).toEqual([`2${UNBREAKABLE_GAP}KB`, '']); }); }); diff --git a/src/utils/numeral.ts b/src/utils/numeral.ts index 19b9daafe..1ae6acc13 100644 --- a/src/utils/numeral.ts +++ b/src/utils/numeral.ts @@ -2,12 +2,12 @@ import numeral from 'numeral'; import 'numeral/locales'; // Without this numeral will throw an error when using not 'en' locale import {Lang, i18n} from './i18n'; -import {unbreakableGap} from './utils'; +import {UNBREAKABLE_GAP} from './utils'; // Set space delimiter for all locales possible in project Object.values(Lang).forEach((value) => { if (numeral.locales[value]) { - numeral.locales[value].delimiters.thousands = unbreakableGap; + numeral.locales[value].delimiters.thousands = UNBREAKABLE_GAP; } }); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d02017417..9c37ebca5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -105,4 +105,4 @@ export function toExponential(value: number, precision?: number) { return Number(value).toExponential(precision); } -export const unbreakableGap = '\xa0'; +export const UNBREAKABLE_GAP = '\xa0';