From 1f7a2252524f0805551f5b9658f97a7ad153685b Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 31 Jul 2025 15:17:28 +0300 Subject: [PATCH 1/6] feat(Clusters): rework versions progress bar --- src/components/VersionsBar/VersionsBar.scss | 36 +++++ src/components/VersionsBar/VersionsBar.tsx | 152 ++++++++++++++++++ src/components/VersionsBar/i18n/en.json | 8 + src/components/VersionsBar/i18n/index.ts | 7 + .../Cluster/VersionsBar/VersionsBar.tsx | 2 +- src/containers/Clusters/Clusters.scss | 4 - src/containers/Clusters/columns.tsx | 29 +--- .../GroupedNodesTree/GroupedNodesTree.tsx | 2 +- .../NodesTreeTitle/NodesTreeTitle.tsx | 2 +- src/containers/Versions/Versions.tsx | 20 +-- src/containers/Versions/groupNodes.test.ts | 42 ++--- src/containers/Versions/groupNodes.ts | 20 +-- src/containers/Versions/types.ts | 2 +- src/containers/Versions/utils.ts | 34 ++-- src/store/reducers/clusters/types.ts | 4 +- src/store/reducers/clusters/utils.ts | 10 +- src/types/versions.ts | 9 -- .../versions/__test__/sortVerions.test.ts | 128 +++++++++++++++ .../{ => versions}/clusterVersionColors.ts | 77 +++++---- src/utils/versions/getVersionsColors.ts | 42 +++-- .../versions/parseNodesToVersionsValues.ts | 14 +- src/utils/versions/sortVersions.ts | 42 +++++ src/utils/versions/types.ts | 27 ++++ 23 files changed, 554 insertions(+), 159 deletions(-) create mode 100644 src/components/VersionsBar/VersionsBar.scss create mode 100644 src/components/VersionsBar/VersionsBar.tsx create mode 100644 src/components/VersionsBar/i18n/en.json create mode 100644 src/components/VersionsBar/i18n/index.ts delete mode 100644 src/types/versions.ts create mode 100644 src/utils/versions/__test__/sortVerions.test.ts rename src/utils/{ => versions}/clusterVersionColors.ts (52%) create mode 100644 src/utils/versions/sortVersions.ts create mode 100644 src/utils/versions/types.ts diff --git a/src/components/VersionsBar/VersionsBar.scss b/src/components/VersionsBar/VersionsBar.scss new file mode 100644 index 0000000000..ebe36586fb --- /dev/null +++ b/src/components/VersionsBar/VersionsBar.scss @@ -0,0 +1,36 @@ +@use '../../styles/mixins.scss'; + +.ydb-versions-bar { + &__bar { + width: 100%; + height: 10px; + } + + &__titles-wrapper { + width: max-content; + } + + &__title { + overflow: hidden; + + text-overflow: ellipsis; + + color: var(--g-color-text-primary); + + @include mixins.body-1-typography(); + } + + &__version { + min-width: 10px; + + border-radius: var(--g-border-radius-xs); + } + + &__title, + &__version, + &__version-icon { + &_dimmed { + opacity: 0.5; + } + } +} diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx new file mode 100644 index 0000000000..955613d322 --- /dev/null +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -0,0 +1,152 @@ +import React from 'react'; + +import {Button, Flex, Tooltip} from '@gravity-ui/uikit'; + +import {cn} from '../../utils/cn'; +import type {PreparedVersion} from '../../utils/versions/types'; + +import i18n from './i18n'; + +import './VersionsBar.scss'; + +const b = cn('ydb-versions-bar'); + +interface VersionsBarProps { + preparedVersions: PreparedVersion[]; +} + +export function VersionsBar({preparedVersions}: VersionsBarProps) { + const shouldTruncateVersions = preparedVersions.length > 4; + + const [hoveredVersion, setHoveredVersion] = React.useState(); + const [allVersionsDisplayed, setAllVersionsDisplayed] = React.useState(false); + + const displayedVersions = React.useMemo(() => { + const total = preparedVersions.reduce((acc, item) => acc + (item.count || 0), 0); + + return preparedVersions.map((item) => { + return { + value: (item.count || 0 / total) * 100, + color: item.color, + version: item.version, + count: item.count, + }; + }); + }, [preparedVersions]); + + const truncatedDisplayedVersions = React.useMemo(() => { + if (allVersionsDisplayed) { + return preparedVersions; + } + + return shouldTruncateVersions ? preparedVersions.slice(0, 3) : preparedVersions; + }, [allVersionsDisplayed, preparedVersions, shouldTruncateVersions]); + + const handleShowAllVersions = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setAllVersionsDisplayed(true); + }; + const handleHideAllVersions = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setAllVersionsDisplayed(false); + }; + + const renderButton = () => { + if (shouldTruncateVersions) { + const truncatedVersionsCount = preparedVersions.length - 3; + + if (allVersionsDisplayed) { + return ( + + ); + } else { + return ( + + ); + } + } + return null; + }; + + const handelMouseLeave = () => { + setHoveredVersion(undefined); + }; + + const isDimmed = (version: string) => { + return hoveredVersion && hoveredVersion !== version; + }; + + return ( + + + {displayedVersions.map((item) => ( + + {i18n('tooltip_nodes', {count: item.count})} +
+ {item.version} + + } + placement={'top-start'} + openDelay={100} + > + { + setHoveredVersion(item.version); + }} + onMouseLeave={handelMouseLeave} + className={b('version', {dimmed: isDimmed(item.version)})} + style={{backgroundColor: item.color, width: `${item.value}%`}} + /> +
+ ))} +
+ + + {truncatedDisplayedVersions.map((item) => ( + + + + + +
{ + setHoveredVersion(item.version); + }} + onMouseLeave={handelMouseLeave} + > + {item.version} +
+
+
+ ))} + {renderButton()} +
+
+ ); +} diff --git a/src/components/VersionsBar/i18n/en.json b/src/components/VersionsBar/i18n/en.json new file mode 100644 index 0000000000..e1fcfbba08 --- /dev/null +++ b/src/components/VersionsBar/i18n/en.json @@ -0,0 +1,8 @@ +{ + "action_show_more": "Show {{count}} more", + "action_hide": "Hide {{count}}", + "tooltip_nodes": { + "one": "{{count}} Node", + "other": "{{count}} Nodes" + } +} diff --git a/src/components/VersionsBar/i18n/index.ts b/src/components/VersionsBar/i18n/index.ts new file mode 100644 index 0000000000..151dcdaea4 --- /dev/null +++ b/src/components/VersionsBar/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-versions-bar'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Cluster/VersionsBar/VersionsBar.tsx b/src/containers/Cluster/VersionsBar/VersionsBar.tsx index e04c649705..d0ddee8d8a 100644 --- a/src/containers/Cluster/VersionsBar/VersionsBar.tsx +++ b/src/containers/Cluster/VersionsBar/VersionsBar.tsx @@ -1,8 +1,8 @@ import type {ProgressProps} from '@gravity-ui/uikit'; import {Progress} from '@gravity-ui/uikit'; -import type {VersionValue} from '../../../types/versions'; import {cn} from '../../../utils/cn'; +import type {VersionValue} from '../../../utils/versions/types'; import './VersionsBar.scss'; diff --git a/src/containers/Clusters/Clusters.scss b/src/containers/Clusters/Clusters.scss index 72d21aa075..101fb8f80e 100644 --- a/src/containers/Clusters/Clusters.scss +++ b/src/containers/Clusters/Clusters.scss @@ -26,11 +26,7 @@ &__cluster-versions { text-decoration: none; } - &__cluster-version { - overflow: hidden; - text-overflow: ellipsis; - } &__cluster-dc { white-space: normal; } diff --git a/src/containers/Clusters/columns.tsx b/src/containers/Clusters/columns.tsx index ec168f21c3..dc980a7f4f 100644 --- a/src/containers/Clusters/columns.tsx +++ b/src/containers/Clusters/columns.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import {Pencil, TrashBin} from '@gravity-ui/icons'; import DataTable from '@gravity-ui/react-data-table'; import type {Column} from '@gravity-ui/react-data-table'; @@ -15,6 +13,7 @@ import { } from '@gravity-ui/uikit'; import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus'; +import {VersionsBar} from '../../components/VersionsBar/VersionsBar'; import type {PreparedCluster} from '../../store/reducers/clusters/types'; import {EFlag} from '../../types/api/enums'; import {uiFactory} from '../../uiFactory/uiFactory'; @@ -158,7 +157,7 @@ const CLUSTERS_COLUMNS: Column[] = [ { name: COLUMNS_NAMES.VERSIONS, header: COLUMNS_TITLES[COLUMNS_NAMES.VERSIONS], - width: 300, + width: 400, defaultOrder: DataTable.DESCENDING, sortAccessor: ({preparedVersions}) => { const versions = preparedVersions @@ -181,16 +180,6 @@ const CLUSTERS_COLUMNS: Column[] = [ return EMPTY_CELL; } - const total = versions.reduce((acc, item) => acc + item.count, 0); - const versionsValues = versions.map((item) => { - return { - value: (item.count / total) * 100, - color: preparedVersions.find( - (versionItem) => versionItem.version === item.version, - )?.color, - }; - }); - return ( preparedVersions.length > 0 && ( [] = [ {withBasename: true}, )} > - - {preparedVersions.map((item, index) => ( -
- {item.version} -
- ))} - {} -
+
) ); diff --git a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx index a4fd5a3c13..acfc0d2370 100644 --- a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx +++ b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx @@ -3,8 +3,8 @@ import React from 'react'; import {TreeView} from 'ydb-ui-components'; import type {NodesPreparedEntity} from '../../../store/reducers/nodes/types'; -import type {VersionValue} from '../../../types/versions'; import {cn} from '../../../utils/cn'; +import type {VersionValue} from '../../../utils/versions/types'; import {NodesTable} from '../NodesTable/NodesTable'; import {NodesTreeTitle} from '../NodesTreeTitle/NodesTreeTitle'; import type {GroupedNodesItem} from '../types'; diff --git a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx index 12084b30a8..e412637072 100644 --- a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx +++ b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx @@ -1,8 +1,8 @@ import {ClipboardButton, Progress} from '@gravity-ui/uikit'; -import type {VersionValue} from '../../../types/versions'; import {cn} from '../../../utils/cn'; import type {PreparedNodeSystemState} from '../../../utils/nodes'; +import type {VersionValue} from '../../../utils/versions/types'; import type {GroupedNodesItem} from '../types'; import './NodesTreeTitle.scss'; diff --git a/src/containers/Versions/Versions.tsx b/src/containers/Versions/Versions.tsx index 2eb389fcd9..438762894b 100644 --- a/src/containers/Versions/Versions.tsx +++ b/src/containers/Versions/Versions.tsx @@ -6,16 +6,16 @@ import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import {nodesApi} from '../../store/reducers/nodes/nodes'; import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; import type {TClusterInfo} from '../../types/api/cluster'; -import type {VersionToColorMap, VersionValue} from '../../types/versions'; import {cn} from '../../utils/cn'; import {useAutoRefreshInterval} from '../../utils/hooks'; +import type {VersionValue, VersionsDataMap} from '../../utils/versions/types'; import {VersionsBar} from '../Cluster/VersionsBar/VersionsBar'; import {GroupedNodesTree} from './GroupedNodesTree/GroupedNodesTree'; import {getGroupedStorageNodes, getGroupedTenantNodes, getOtherNodes} from './groupNodes'; import i18n from './i18n'; import {GroupByValue} from './types'; -import {useGetVersionValues, useVersionToColorMap} from './utils'; +import {useGetVersionValues, useVersionsDataMap} from './utils'; import './Versions.scss'; @@ -32,16 +32,16 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) { {tablets: false, fieldsRequired: ['SystemState', 'SubDomainKey']}, {pollingInterval: autoRefreshInterval}, ); - const versionToColor = useVersionToColorMap(cluster); + const versionsDataMap = useVersionsDataMap(cluster); - const versionsValues = useGetVersionValues({cluster, versionToColor, clusterLoading: loading}); + const versionsValues = useGetVersionValues({cluster, versionsDataMap, clusterLoading: loading}); return ( ); @@ -50,10 +50,10 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) { interface VersionsProps { nodes?: NodesPreparedEntity[]; versionsValues: VersionValue[]; - versionToColor?: VersionToColorMap; + versionsDataMap?: VersionsDataMap; } -function Versions({versionsValues, nodes, versionToColor}: VersionsProps) { +function Versions({versionsValues, nodes, versionsDataMap}: VersionsProps) { const [groupByValue, setGroupByValue] = React.useState(GroupByValue.VERSION); const [expanded, setExpanded] = React.useState(false); @@ -91,9 +91,9 @@ function Versions({versionsValues, nodes, versionToColor}: VersionsProps) { ); }; - const tenantNodes = getGroupedTenantNodes(nodes, versionToColor, groupByValue); - const storageNodes = getGroupedStorageNodes(nodes, versionToColor); - const otherNodes = getOtherNodes(nodes, versionToColor); + const tenantNodes = getGroupedTenantNodes(nodes, versionsDataMap, groupByValue); + const storageNodes = getGroupedStorageNodes(nodes, versionsDataMap); + const otherNodes = getOtherNodes(nodes, versionsDataMap); const storageNodesContent = storageNodes?.length ? (

{i18n('title_storage')}

diff --git a/src/containers/Versions/groupNodes.test.ts b/src/containers/Versions/groupNodes.test.ts index 063792eb8c..edd63d4e83 100644 --- a/src/containers/Versions/groupNodes.test.ts +++ b/src/containers/Versions/groupNodes.test.ts @@ -68,14 +68,15 @@ describe('getGroupedTenantNodes', () => { expect(result).toBeUndefined(); }); + // eslint-disable-next-line complexity test('should group tenant nodes by version when groupByValue is VERSION', () => { - const versionToColor = new Map([ - ['25-1-1', 'red'], - ['25-1-2', 'blue'], - ['25-1-3', 'green'], + const versionsDataMap = new Map([ + ['25-1-1', {color: 'red'}], + ['25-1-2', {color: 'blue'}], + ['25-1-3', {color: 'green'}], ]); - const result = getGroupedTenantNodes(nodes, versionToColor, GroupByValue.VERSION); + const result = getGroupedTenantNodes(nodes, versionsDataMap, GroupByValue.VERSION); expect(result).toHaveLength(3); @@ -104,14 +105,15 @@ describe('getGroupedTenantNodes', () => { expect(result?.[2].items?.[0].nodes?.[0].NodeId).toBe(6); }); + // eslint-disable-next-line complexity test('should group tenant nodes by tenant when groupByValue is TENANT', () => { - const versionToColor = new Map([ - ['25-1-1', 'red'], - ['25-1-2', 'blue'], - ['25-1-3', 'green'], + const versionsDataMap = new Map([ + ['25-1-1', {color: 'red'}], + ['25-1-2', {color: 'blue'}], + ['25-1-3', {color: 'green'}], ]); - const result = getGroupedTenantNodes(nodes, versionToColor, GroupByValue.TENANT); + const result = getGroupedTenantNodes(nodes, versionsDataMap, GroupByValue.TENANT); expect(result).toHaveLength(3); @@ -155,13 +157,13 @@ describe('getGroupedStorageNodes', () => { }); test('should group storage nodes by version', () => { - const versionToColor = new Map([ - ['25-1-1', 'red'], - ['25-1-2', 'blue'], - ['25-1-3', 'green'], + const versionsDataMap = new Map([ + ['25-1-1', {color: 'red'}], + ['25-1-2', {color: 'blue'}], + ['25-1-3', {color: 'green'}], ]); - const result = getGroupedStorageNodes(nodes, versionToColor); + const result = getGroupedStorageNodes(nodes, versionsDataMap); expect(result).toHaveLength(3); @@ -197,13 +199,13 @@ describe('getOtherNodes', () => { }); test('should group other nodes by version', () => { - const versionToColor = new Map([ - ['25-1-1', 'red'], - ['25-1-2', 'blue'], - ['25-1-3', 'green'], + const versionsDataMap = new Map([ + ['25-1-1', {color: 'red'}], + ['25-1-2', {color: 'blue'}], + ['25-1-3', {color: 'green'}], ]); - const result = getOtherNodes(nodes, versionToColor); + const result = getOtherNodes(nodes, versionsDataMap); expect(result).toHaveLength(1); diff --git a/src/containers/Versions/groupNodes.ts b/src/containers/Versions/groupNodes.ts index 97a413eb26..b41447df41 100644 --- a/src/containers/Versions/groupNodes.ts +++ b/src/containers/Versions/groupNodes.ts @@ -1,8 +1,8 @@ import groupBy from 'lodash/groupBy'; import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; -import type {VersionToColorMap} from '../../types/versions'; -import {getMinorVersion, parseNodesToVersionsValues} from '../../utils/versions'; +import {getColorFromVersionsData, parseNodesToVersionsValues} from '../../utils/versions'; +import type {VersionsDataMap} from '../../utils/versions/types'; import type {GroupedNodesItem} from './types'; import {GroupByValue} from './types'; @@ -12,7 +12,7 @@ const sortByTitle = (a: GroupedNodesItem, b: GroupedNodesItem) => export const getGroupedTenantNodes = ( nodes: NodesPreparedEntity[] | undefined, - versionToColor: VersionToColorMap | undefined, + versionsDataMap: VersionsDataMap | undefined, groupByValue: GroupByValue, ): GroupedNodesItem[] | undefined => { if (!nodes || !nodes.length) { @@ -43,7 +43,7 @@ export const getGroupedTenantNodes = ( return { title: version, items: items, - versionColor: versionToColor?.get(getMinorVersion(version)), + versionColor: getColorFromVersionsData(version, versionsDataMap), }; }) .filter((item): item is GroupedNodesItem => Boolean(item)); @@ -55,7 +55,7 @@ export const getGroupedTenantNodes = ( .map((tenant) => { const versionsValues = parseNodesToVersionsValues( dividedByTenant[tenant], - versionToColor, + versionsDataMap, ); const dividedByVersion = groupBy(dividedByTenant[tenant], 'Version'); @@ -63,7 +63,7 @@ export const getGroupedTenantNodes = ( return { title: version, nodes: dividedByVersion[version], - versionColor: versionToColor?.get(getMinorVersion(version)), + versionColor: getColorFromVersionsData(version, versionsDataMap), }; }); @@ -84,7 +84,7 @@ export const getGroupedTenantNodes = ( export const getGroupedStorageNodes = ( nodes: NodesPreparedEntity[] | undefined, - versionToColor: VersionToColorMap | undefined, + versionsDataMap: VersionsDataMap | undefined, ): GroupedNodesItem[] | undefined => { if (!nodes || !nodes.length) { return undefined; @@ -97,14 +97,14 @@ export const getGroupedStorageNodes = ( return { title: version, nodes: storageNodesDividedByVersion[version], - versionColor: versionToColor?.get(getMinorVersion(version)), + versionColor: getColorFromVersionsData(version, versionsDataMap), }; }); }; export const getOtherNodes = ( nodes: NodesPreparedEntity[] | undefined, - versionToColor: VersionToColorMap | undefined, + versionsDataMap: VersionsDataMap | undefined, ): GroupedNodesItem[] | undefined => { if (!nodes || !nodes.length) { return undefined; @@ -120,7 +120,7 @@ export const getOtherNodes = ( return { title: version, nodes: otherNodesDividedByVersion[version], - versionColor: versionToColor?.get(getMinorVersion(version)), + versionColor: getColorFromVersionsData(version, versionsDataMap), }; }); }; diff --git a/src/containers/Versions/types.ts b/src/containers/Versions/types.ts index 019e1d4d78..7108983fff 100644 --- a/src/containers/Versions/types.ts +++ b/src/containers/Versions/types.ts @@ -1,5 +1,5 @@ import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; -import type {VersionValue} from '../../types/versions'; +import type {VersionValue} from '../../utils/versions/types'; export interface GroupedNodesItem { title?: string; diff --git a/src/containers/Versions/utils.ts b/src/containers/Versions/utils.ts index 2293933c10..279e6ac1b7 100644 --- a/src/containers/Versions/utils.ts +++ b/src/containers/Versions/utils.ts @@ -7,24 +7,24 @@ import {clustersApi} from '../../store/reducers/clusters/clusters'; import {nodesApi} from '../../store/reducers/nodes/nodes'; import {isClusterInfoV2} from '../../types/api/cluster'; import type {TClusterInfo} from '../../types/api/cluster'; -import type {VersionToColorMap} from '../../types/versions'; -import {getVersionColors, getVersionMap} from '../../utils/clusterVersionColors'; import {useTypedSelector} from '../../utils/hooks'; import { parseNodeGroupsToVersionsValues, parseNodesToVersionsValues, - parseVersionsToVersionToColorMap, + parseVersionsToVersionsDataMap, } from '../../utils/versions'; +import {getVersionMap, getVersionsData} from '../../utils/versions/clusterVersionColors'; +import type {VersionsDataMap} from '../../utils/versions/types'; interface UseGetVersionValuesProps { cluster?: TClusterInfo; - versionToColor?: VersionToColorMap; + versionsDataMap?: VersionsDataMap; clusterLoading?: boolean; } export const useGetVersionValues = ({ cluster, - versionToColor, + versionsDataMap, clusterLoading, }: UseGetVersionValuesProps) => { const {currentData} = nodesApi.useGetNodesQuery( @@ -43,7 +43,7 @@ export const useGetVersionValues = ({ name: version, count, })); - return parseNodeGroupsToVersionsValues(groups, versionToColor, cluster.NodesTotal); + return parseNodeGroupsToVersionsValues(groups, versionsDataMap, cluster.NodesTotal); } if (!currentData) { return []; @@ -51,29 +51,29 @@ export const useGetVersionValues = ({ if (Array.isArray(currentData.NodeGroups)) { return parseNodeGroupsToVersionsValues( currentData.NodeGroups, - versionToColor, + versionsDataMap, cluster?.NodesTotal, ); } - return parseNodesToVersionsValues(currentData.Nodes, versionToColor); - }, [currentData, versionToColor, cluster]); + return parseNodesToVersionsValues(currentData.Nodes, versionsDataMap); + }, [currentData, versionsDataMap, cluster]); return versionsValues; }; -export function useVersionToColorMap(cluster?: TClusterInfo) { - const getVersionToColorMap = useGetClusterVersionToColorMap(); +export function useVersionsDataMap(cluster?: TClusterInfo) { + const getVersionsDataMap = useGetClusterVersionsDataMap(); return React.useMemo(() => { - if (getVersionToColorMap) { - return getVersionToColorMap(); + if (getVersionsDataMap) { + return getVersionsDataMap(); } - return parseVersionsToVersionToColorMap(cluster?.Versions); - }, [cluster?.Versions, getVersionToColorMap]); + return parseVersionsToVersionsDataMap(cluster?.Versions); + }, [cluster?.Versions, getVersionsDataMap]); } /** For multi-cluster version - with using meta handlers */ -function useGetClusterVersionToColorMap(): (() => VersionToColorMap) | undefined { +function useGetClusterVersionsDataMap(): (() => VersionsDataMap) | undefined { const [clusterName] = useQueryParam('clusterName', StringParam); const singleClusterMode = useTypedSelector((state) => state.singleClusterMode); const {data} = clustersApi.useGetClustersListQuery(undefined, {skip: singleClusterMode}); @@ -87,6 +87,6 @@ function useGetClusterVersionToColorMap(): (() => VersionToColorMap) | undefined const info = clusters.find((cluster) => cluster.name === clusterName); const versions = info?.versions || []; - return () => getVersionColors(getVersionMap(versions)); + return () => getVersionsData(getVersionMap(versions)); }, [singleClusterMode, data, clusterName]); } diff --git a/src/store/reducers/clusters/types.ts b/src/store/reducers/clusters/types.ts index a22c855c79..0acb621278 100644 --- a/src/store/reducers/clusters/types.ts +++ b/src/store/reducers/clusters/types.ts @@ -1,8 +1,8 @@ import type {MetaExtendedClusterInfo} from '../../../types/api/meta'; -import type {ExtendedMetaClusterVersion} from '../../../utils/clusterVersionColors'; +import type {PreparedVersion} from '../../../utils/versions/types'; export interface PreparedCluster extends MetaExtendedClusterInfo { - preparedVersions: ExtendedMetaClusterVersion[]; + preparedVersions: PreparedVersion[]; preparedBackend?: string; } diff --git a/src/store/reducers/clusters/utils.ts b/src/store/reducers/clusters/utils.ts index 43c2b81571..5d562a07e9 100644 --- a/src/store/reducers/clusters/utils.ts +++ b/src/store/reducers/clusters/utils.ts @@ -1,10 +1,10 @@ import type {MetaClusters} from '../../../types/api/meta'; +import {prepareBackendFromBalancer} from '../../../utils/parseBalancer'; import { - getVersionColors, getVersionMap, + getVersionsData, prepareClusterVersions, -} from '../../../utils/clusterVersionColors'; -import {prepareBackendFromBalancer} from '../../../utils/parseBalancer'; +} from '../../../utils/versions/clusterVersionColors'; import type {PreparedCluster} from './types'; @@ -19,12 +19,12 @@ export const prepareClustersData = (data: MetaClusters): PreparedCluster[] => { }); // Get colors map for all clusters colors - const versionToColor = getVersionColors(allMinorVersions); + const versionsData = getVersionsData(allMinorVersions); // Apply color map to every cluster in the list return clusters.map((cluster) => ({ ...cluster, - preparedVersions: prepareClusterVersions(cluster.versions, versionToColor), + preparedVersions: prepareClusterVersions(cluster.versions, versionsData), preparedBackend: cluster.balancer ? prepareBackendFromBalancer(cluster.balancer) : undefined, diff --git a/src/types/versions.ts b/src/types/versions.ts deleted file mode 100644 index f242e36725..0000000000 --- a/src/types/versions.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type VersionsMap = Map>; -export type VersionToColorMap = Map; - -export interface VersionValue { - value: number; - color: string | undefined; - version: string; - title: string; -} diff --git a/src/utils/versions/__test__/sortVerions.test.ts b/src/utils/versions/__test__/sortVerions.test.ts new file mode 100644 index 0000000000..e3c90368e7 --- /dev/null +++ b/src/utils/versions/__test__/sortVerions.test.ts @@ -0,0 +1,128 @@ +import {sortVerions} from '../sortVersions'; +import type {PreparedVersion} from '../types'; + +describe('sortVerions', () => { + test('should sort versions by majorIndex in descending order', function () { + const versions: PreparedVersion[] = [ + {version: 'v2.0.0', majorIndex: 1}, + {version: 'v1.0.0', majorIndex: 2}, + {version: 'v3.0.0', majorIndex: 0}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v3.0.0', majorIndex: 0}, + {version: 'v2.0.0', majorIndex: 1}, + {version: 'v1.0.0', majorIndex: 2}, + ]); + }); + + test('should place versions with undefined majorIndex after versions with defined majorIndex', function () { + const versions: PreparedVersion[] = [ + {version: 'v2.0.0', majorIndex: undefined}, + {version: 'v1.0.0', majorIndex: 2}, + {version: 'v3.0.0', majorIndex: 0}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v3.0.0', majorIndex: 0}, + {version: 'v1.0.0', majorIndex: 2}, + {version: 'v2.0.0', majorIndex: undefined}, + ]); + }); + + test('should sort versions by minorIndex in descending order when majorIndex is the same', function () { + const versions: PreparedVersion[] = [ + {version: 'v1.2.0', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.0', majorIndex: 2, minorIndex: 2}, + {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, + {version: 'v1.2.0', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.0', majorIndex: 2, minorIndex: 2}, + ]); + }); + + test('should place versions with undefined minorIndex after versions with defined minorIndex when majorIndex is the same', function () { + const versions: PreparedVersion[] = [ + {version: 'v1.2.0', majorIndex: 2, minorIndex: 1}, + {version: 'v1.0.0', majorIndex: 2, minorIndex: undefined}, + {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, + {version: 'v1.2.0', majorIndex: 2, minorIndex: 1}, + {version: 'v1.0.0', majorIndex: 2, minorIndex: undefined}, + ]); + }); + + test('should sort versions by hashCode when both majorIndex and minorIndex are the same (higher hash first)', function () { + const versions: PreparedVersion[] = [ + {version: 'v1.1.1', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.2', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.0', majorIndex: 2, minorIndex: 1}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v1.1.2', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.1', majorIndex: 2, minorIndex: 1}, + {version: 'v1.1.0', majorIndex: 2, minorIndex: 1}, + ]); + }); + + test('should sort versions with undefined major by hashCode', function () { + const versions: PreparedVersion[] = [ + {version: 'v1.0.0', majorIndex: undefined}, + {version: 'v2.0.0', majorIndex: undefined}, + {version: 'v3.0.0', majorIndex: undefined}, + ]; + + const sortedVersions = sortVerions(versions); + + // When majorIndex is undefined, versions are sorted by hashCode + // This test assumes the hashCode implementation results in this order + expect(sortedVersions).toEqual([ + {version: 'v3.0.0', majorIndex: undefined}, + {version: 'v2.0.0', majorIndex: undefined}, + {version: 'v1.0.0', majorIndex: undefined}, + ]); + }); + + test('should handle complex sorting with mixed undefined indices', function () { + const versions: PreparedVersion[] = [ + {version: 'v2.1.0', majorIndex: 1, minorIndex: 1}, + {version: 'v1.0.0', majorIndex: 2, minorIndex: undefined}, + {version: 'v3.0.0', majorIndex: 0, minorIndex: 0}, + {version: 'v1.2.0', majorIndex: 2, minorIndex: 0}, + {version: 'v2.0.0', majorIndex: 1, minorIndex: undefined}, + ]; + + const sortedVersions = sortVerions(versions); + + expect(sortedVersions).toEqual([ + {version: 'v3.0.0', majorIndex: 0, minorIndex: 0}, + {version: 'v2.1.0', majorIndex: 1, minorIndex: 1}, + {version: 'v2.0.0', majorIndex: 1, minorIndex: undefined}, + {version: 'v1.2.0', majorIndex: 2, minorIndex: 0}, + {version: 'v1.0.0', majorIndex: 2, minorIndex: undefined}, + ]); + }); + + test('should handle empty array', function () { + const versions: PreparedVersion[] = []; + const sortedVersions = sortVerions(versions); + expect(sortedVersions).toEqual([]); + }); +}); diff --git a/src/utils/clusterVersionColors.ts b/src/utils/versions/clusterVersionColors.ts similarity index 52% rename from src/utils/clusterVersionColors.ts rename to src/utils/versions/clusterVersionColors.ts index 0a88ed78eb..aefe69befc 100644 --- a/src/utils/clusterVersionColors.ts +++ b/src/utils/versions/clusterVersionColors.ts @@ -1,23 +1,20 @@ -import uniqBy from 'lodash/uniqBy'; +import type {MetaClusterVersion} from '../../types/api/meta'; -import type {MetaClusterVersion} from '../types/api/meta'; -import type {VersionToColorMap} from '../types/versions'; +import {sortVerions} from './sortVersions'; +import type { + ColorIndexToVersionsMap, + PreparedVersion, + PreparedVersions, + VersionsDataMap, +} from './types'; -import { - COLORS, - DEFAULT_COLOR, - getMinorVersion, - getMinorVersionColorVariant, - hashCode, -} from './versions'; +import {COLORS, DEFAULT_COLOR, getMinorVersion, getMinorVersionColorVariant, hashCode} from '.'; const UNDEFINED_COLOR_INDEX = '__no_color__'; -type VersionsMap = Map>; - export const getVersionMap = ( versions: MetaClusterVersion[], - initialMap: VersionsMap = new Map(), + initialMap: ColorIndexToVersionsMap = new Map(), ) => { versions.forEach( ({version, version_base_color_index: versionBaseColorIndex = UNDEFINED_COLOR_INDEX}) => { @@ -31,8 +28,8 @@ export const getVersionMap = ( return initialMap; }; -export const getVersionColors = (versionMap: VersionsMap) => { - const versionToColor: VersionToColorMap = new Map(); +export const getVersionsData = (versionMap: ColorIndexToVersionsMap) => { + const versionsDataMap: VersionsDataMap = new Map(); for (const [baseColorIndex, item] of versionMap) { Array.from(item) @@ -41,7 +38,9 @@ export const getVersionColors = (versionMap: VersionsMap) => { .sort((a, b) => hashCode(b) - hashCode(a)) .forEach((minor, minorIndex) => { if (baseColorIndex === UNDEFINED_COLOR_INDEX) { - versionToColor.set(minor, DEFAULT_COLOR); + versionsDataMap.set(minor, { + color: DEFAULT_COLOR, + }); } else { // baseColorIndex is numeric as we check if it is UNDEFINED_COLOR_INDEX before const currentColorIndex = Number(baseColorIndex) % COLORS.length; @@ -53,35 +52,43 @@ export const getVersionColors = (versionMap: VersionsMap) => { ); const minorColor = COLORS[currentColorIndex][minorColorVariant]; - versionToColor.set(minor, minorColor); + versionsDataMap.set(minor, { + color: minorColor, + majorIndex: currentColorIndex, + minorIndex, + }); } }); } - return versionToColor; + return versionsDataMap; }; -export interface ExtendedMetaClusterVersion extends MetaClusterVersion { - minorVersion: string; - color: string | undefined; -} - export const prepareClusterVersions = ( clusterVersions: MetaClusterVersion[] = [], - versionToColor: VersionToColorMap, -) => { + versionsDataMap: VersionsDataMap, +): PreparedVersion[] => { const filteredVersions = clusterVersions.filter((item) => item.version); - const preparedVersions = uniqBy(filteredVersions, 'version').map((item) => { - return { - ...item, - minorVersion: getMinorVersion(item.version), + + const result: PreparedVersions = {}; + + filteredVersions.forEach((item) => { + if (result[item.version]) { + result[item.version].count = result[item.version].count || 0 + item.count || 0; + } + + const minorVersion = getMinorVersion(item.version); + const data = versionsDataMap.get(minorVersion); + + result[item.version] = { + version: item.version, + minorVersion, + color: data?.color, + majorIndex: data?.majorIndex, + minorIndex: data?.minorIndex, + count: item.count || 0, }; }); - const versionsColors = preparedVersions.reduce((acc, item) => { - const color = versionToColor.get(item.minorVersion); - acc.push({...item, color}); - return acc; - }, []); - return versionsColors; + return sortVerions(Object.values(result)); }; diff --git a/src/utils/versions/getVersionsColors.ts b/src/utils/versions/getVersionsColors.ts index 7755f8aae6..7e408cc607 100644 --- a/src/utils/versions/getVersionsColors.ts +++ b/src/utils/versions/getVersionsColors.ts @@ -1,6 +1,5 @@ -import type {VersionToColorMap, VersionsMap} from '../../types/versions'; - import {getMajorVersion, getMinorVersion} from './parseVersion'; +import type {VersionsDataMap, VersionsMap} from './types'; export const hashCode = (s: string) => { return s.split('').reduce((a, b) => { @@ -122,7 +121,7 @@ export const getVersionsMap = (versions: string[], initialMap: VersionsMap = new return initialMap; }; -export const getVersionToColorMap = (versionsMap: VersionsMap) => { +export const getVersionsDataMap = (versionsMap: VersionsMap) => { const clustersVersions = Array.from(versionsMap.keys()).map((version) => { return { version, @@ -130,7 +129,7 @@ export const getVersionToColorMap = (versionsMap: VersionsMap) => { }; }); - const versionToColor: VersionToColorMap = new Map(); + const versionsDataMap: VersionsDataMap = new Map(); // not every version is colored, therefore iteration index can't be used consistently // init with the colors length to put increment right after condition for better readability let currentColorIndex = COLORS.length - 1; @@ -143,8 +142,12 @@ export const getVersionToColorMap = (versionsMap: VersionsMap) => { if (/^(\w+-)?stable/.test(item.version)) { currentColorIndex = (currentColorIndex + 1) % COLORS.length; - // Use fisrt color for major - versionToColor.set(item.version, COLORS[currentColorIndex][0]); + versionsDataMap.set(item.version, { + // Use fisrt color for major + color: COLORS[currentColorIndex][0], + majorIndex: currentColorIndex, + minorIndex: 0, + }); const minors = Array.from(versionsMap.get(item.version) || []) .filter((v) => v !== item.version) @@ -167,15 +170,32 @@ export const getVersionToColorMap = (versionsMap: VersionsMap) => { minorQuantity, ); const minorColor = COLORS[currentColorIndex][minorColorVariant]; - versionToColor.set(minor.version, minorColor); + + versionsDataMap.set(minor.version, { + color: minorColor, + majorIndex: currentColorIndex, + minorIndex: minorIndex, + }); }); } else { - versionToColor.set(item.version, DEFAULT_COLOR); + versionsDataMap.set(item.version, { + color: DEFAULT_COLOR, + }); } }); - return versionToColor; + return versionsDataMap; }; -export const parseVersionsToVersionToColorMap = (versions: string[] = []) => { - return getVersionToColorMap(getVersionsMap(versions)); +export const parseVersionsToVersionsDataMap = (versions: string[] = []) => { + return getVersionsDataMap(getVersionsMap(versions)); }; + +export function getColorFromVersionsData( + version: string, + versionsDataMap: VersionsDataMap | undefined, +) { + const minorVersion = getMinorVersion(version); + const versionData = versionsDataMap?.get(minorVersion); + + return versionData?.color; +} diff --git a/src/utils/versions/parseNodesToVersionsValues.ts b/src/utils/versions/parseNodesToVersionsValues.ts index 0271fd09a5..42aaa90b0c 100644 --- a/src/utils/versions/parseNodesToVersionsValues.ts +++ b/src/utils/versions/parseNodesToVersionsValues.ts @@ -1,14 +1,14 @@ import type {NodesGroup} from '../../store/reducers/nodes/types'; import type {TSystemStateInfo} from '../../types/api/nodes'; -import type {VersionToColorMap, VersionValue} from '../../types/versions'; -import {getMinorVersion} from './parseVersion'; +import {getColorFromVersionsData} from './getVersionsColors'; +import type {VersionValue, VersionsDataMap} from './types'; const MIN_VALUE = 0.5; export const parseNodesToVersionsValues = ( nodes: TSystemStateInfo[] = [], - versionsToColor?: VersionToColorMap, + versionsDataMap?: VersionsDataMap, ): VersionValue[] => { const versionsCount = nodes.reduce>((acc, node) => { if (node.Version) { @@ -22,11 +22,12 @@ export const parseNodesToVersionsValues = ( }, {}); const result = Object.keys(versionsCount).map((version) => { const value = (versionsCount[version] / nodes.length) * 100; + return { title: version, version: version, - color: versionsToColor?.get(getMinorVersion(version)), value: value < MIN_VALUE ? MIN_VALUE : value, + color: getColorFromVersionsData(version, versionsDataMap), }; }); return normalizeResult(result); @@ -34,17 +35,18 @@ export const parseNodesToVersionsValues = ( export function parseNodeGroupsToVersionsValues( groups: NodesGroup[], - versionsToColor?: VersionToColorMap, + versionsDataMap?: VersionsDataMap, total?: number, ) { const normalizedTotal = total ?? groups.reduce((acc, group) => acc + group.count, 0); const result = groups.map((group) => { const value = (group.count / normalizedTotal) * 100; + return { title: group.name, version: group.name, - color: versionsToColor?.get(getMinorVersion(group.name)), value: value < MIN_VALUE ? MIN_VALUE : value, + color: getColorFromVersionsData(group.name, versionsDataMap), }; }); const normalized = normalizeResult(result); diff --git a/src/utils/versions/sortVersions.ts b/src/utils/versions/sortVersions.ts new file mode 100644 index 0000000000..584ad625ce --- /dev/null +++ b/src/utils/versions/sortVersions.ts @@ -0,0 +1,42 @@ +import {hashCode} from './getVersionsColors'; +import type {PreparedVersion} from './types'; + +/** + * Sorts cluster versions according to the following rules: + * 1. First by majorIndex in ascending order (lower index first) + * - Higher version numbers typically have lower indices (e.g., v3.0.0: index 0, v2.0.0: index 1, v1.0.0: index 2) + * - Versions with undefined majorIndex come last + * 2. Then by minorIndex in ascending order (lower index first) when majorIndex is the same + * - Higher minor versions typically have lower indices + * - Versions with undefined minorIndex come last within their major group + * 3. Finally by version string hashCode in descending order when both indices are the same + * - Higher hash values come first + * + * The function creates a copy of the input array to avoid modifying the original. + * @param versions - Array of prepared cluster versions to sort + * @returns A new sorted array of cluster versions + */ +export function sortVerions(versions: PreparedVersion[]) { + return versions.slice().sort((versionA, versionB) => { + if (versionA.majorIndex !== versionB.majorIndex) { + if (versionA.majorIndex === undefined) { + return 1; + } + if (versionB.majorIndex === undefined) { + return -1; + } + return versionA.majorIndex - versionB.majorIndex; + } + if (versionA.minorIndex !== versionB.minorIndex) { + if (versionA.minorIndex === undefined) { + return 1; + } + if (versionB.minorIndex === undefined) { + return -1; + } + return versionA.minorIndex - versionB.minorIndex; + } + + return hashCode(versionB.version) - hashCode(versionA.version); + }); +} diff --git a/src/utils/versions/types.ts b/src/utils/versions/types.ts new file mode 100644 index 0000000000..2c349ddf5e --- /dev/null +++ b/src/utils/versions/types.ts @@ -0,0 +1,27 @@ +export type VersionsMap = Map>; +export type VersionToColorMap = Map; + +export interface VersionValue { + value: number; + color?: string; + version: string; + title: string; +} + +export type ColorIndexToVersionsMap = Map>; + +interface VersionWithColorIndexes { + color?: string; + majorIndex?: number; + minorIndex?: number; +} + +export type VersionsDataMap = Map; + +export interface PreparedVersion extends VersionWithColorIndexes { + version: string; + minorVersion?: string; + count?: number; +} + +export type PreparedVersions = Record; From 52ee19cb6d2271caeb967b25a3c808f6b5dc7f19 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 31 Jul 2025 16:59:46 +0300 Subject: [PATCH 2/6] fix: copilot review --- src/components/VersionsBar/VersionsBar.tsx | 8 ++++---- .../versions/__test__/sortVerions.test.ts | 20 +++++++++---------- src/utils/versions/clusterVersionColors.ts | 10 +++++++--- src/utils/versions/sortVersions.ts | 2 +- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx index 955613d322..0429ee4e25 100644 --- a/src/components/VersionsBar/VersionsBar.tsx +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -26,7 +26,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { return preparedVersions.map((item) => { return { - value: (item.count || 0 / total) * 100, + value: ((item.count || 0) / total) * 100, color: item.color, version: item.version, count: item.count, @@ -78,7 +78,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { return null; }; - const handelMouseLeave = () => { + const handleMouseLeave = () => { setHoveredVersion(undefined); }; @@ -106,7 +106,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { onMouseEnter={() => { setHoveredVersion(item.version); }} - onMouseLeave={handelMouseLeave} + onMouseLeave={handleMouseLeave} className={b('version', {dimmed: isDimmed(item.version)})} style={{backgroundColor: item.color, width: `${item.value}%`}} /> @@ -138,7 +138,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { onMouseEnter={() => { setHoveredVersion(item.version); }} - onMouseLeave={handelMouseLeave} + onMouseLeave={handleMouseLeave} > {item.version} diff --git a/src/utils/versions/__test__/sortVerions.test.ts b/src/utils/versions/__test__/sortVerions.test.ts index e3c90368e7..d3031b08bb 100644 --- a/src/utils/versions/__test__/sortVerions.test.ts +++ b/src/utils/versions/__test__/sortVerions.test.ts @@ -1,7 +1,7 @@ -import {sortVerions} from '../sortVersions'; +import {sortVersions} from '../sortVersions'; import type {PreparedVersion} from '../types'; -describe('sortVerions', () => { +describe('sortVersions', () => { test('should sort versions by majorIndex in descending order', function () { const versions: PreparedVersion[] = [ {version: 'v2.0.0', majorIndex: 1}, @@ -9,7 +9,7 @@ describe('sortVerions', () => { {version: 'v3.0.0', majorIndex: 0}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v3.0.0', majorIndex: 0}, @@ -25,7 +25,7 @@ describe('sortVerions', () => { {version: 'v3.0.0', majorIndex: 0}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v3.0.0', majorIndex: 0}, @@ -41,7 +41,7 @@ describe('sortVerions', () => { {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, @@ -57,7 +57,7 @@ describe('sortVerions', () => { {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v1.3.0', majorIndex: 2, minorIndex: 0}, @@ -73,7 +73,7 @@ describe('sortVerions', () => { {version: 'v1.1.0', majorIndex: 2, minorIndex: 1}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v1.1.2', majorIndex: 2, minorIndex: 1}, @@ -89,7 +89,7 @@ describe('sortVerions', () => { {version: 'v3.0.0', majorIndex: undefined}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); // When majorIndex is undefined, versions are sorted by hashCode // This test assumes the hashCode implementation results in this order @@ -109,7 +109,7 @@ describe('sortVerions', () => { {version: 'v2.0.0', majorIndex: 1, minorIndex: undefined}, ]; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([ {version: 'v3.0.0', majorIndex: 0, minorIndex: 0}, @@ -122,7 +122,7 @@ describe('sortVerions', () => { test('should handle empty array', function () { const versions: PreparedVersion[] = []; - const sortedVersions = sortVerions(versions); + const sortedVersions = sortVersions(versions); expect(sortedVersions).toEqual([]); }); }); diff --git a/src/utils/versions/clusterVersionColors.ts b/src/utils/versions/clusterVersionColors.ts index aefe69befc..0aee8b2016 100644 --- a/src/utils/versions/clusterVersionColors.ts +++ b/src/utils/versions/clusterVersionColors.ts @@ -1,6 +1,6 @@ import type {MetaClusterVersion} from '../../types/api/meta'; -import {sortVerions} from './sortVersions'; +import {sortVersions} from './sortVersions'; import type { ColorIndexToVersionsMap, PreparedVersion, @@ -74,7 +74,11 @@ export const prepareClusterVersions = ( filteredVersions.forEach((item) => { if (result[item.version]) { - result[item.version].count = result[item.version].count || 0 + item.count || 0; + // Summ count for versions of different nodes types + const currentCount = result[item.version].count || 0; + const itemCount = item.count || 0; + + result[item.version].count = currentCount + itemCount; } const minorVersion = getMinorVersion(item.version); @@ -90,5 +94,5 @@ export const prepareClusterVersions = ( }; }); - return sortVerions(Object.values(result)); + return sortVersions(Object.values(result)); }; diff --git a/src/utils/versions/sortVersions.ts b/src/utils/versions/sortVersions.ts index 584ad625ce..6ac2f49495 100644 --- a/src/utils/versions/sortVersions.ts +++ b/src/utils/versions/sortVersions.ts @@ -16,7 +16,7 @@ import type {PreparedVersion} from './types'; * @param versions - Array of prepared cluster versions to sort * @returns A new sorted array of cluster versions */ -export function sortVerions(versions: PreparedVersion[]) { +export function sortVersions(versions: PreparedVersion[]) { return versions.slice().sort((versionA, versionB) => { if (versionA.majorIndex !== versionB.majorIndex) { if (versionA.majorIndex === undefined) { From 91b1704f8bb257060acfdd2ee406167822228190 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 31 Jul 2025 17:10:55 +0300 Subject: [PATCH 3/6] fix: copilot review 2 --- src/components/VersionsBar/VersionsBar.tsx | 12 +++++++++--- .../{sortVerions.test.ts => sortVersions.test.ts} | 0 src/utils/versions/clusterVersionColors.ts | 2 +- src/utils/versions/sortVersions.ts | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) rename src/utils/versions/__test__/{sortVerions.test.ts => sortVersions.test.ts} (100%) diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx index 0429ee4e25..794a7ea5fb 100644 --- a/src/components/VersionsBar/VersionsBar.tsx +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -11,12 +11,16 @@ import './VersionsBar.scss'; const b = cn('ydb-versions-bar'); +const TRUNCATION_THRESHOLD = 4; +// One more line for Show more / Hide button +const MAX_DISPLAYED_VERSIONS = TRUNCATION_THRESHOLD - 1; + interface VersionsBarProps { preparedVersions: PreparedVersion[]; } export function VersionsBar({preparedVersions}: VersionsBarProps) { - const shouldTruncateVersions = preparedVersions.length > 4; + const shouldTruncateVersions = preparedVersions.length > TRUNCATION_THRESHOLD; const [hoveredVersion, setHoveredVersion] = React.useState(); const [allVersionsDisplayed, setAllVersionsDisplayed] = React.useState(false); @@ -39,7 +43,9 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { return preparedVersions; } - return shouldTruncateVersions ? preparedVersions.slice(0, 3) : preparedVersions; + return shouldTruncateVersions + ? preparedVersions.slice(0, MAX_DISPLAYED_VERSIONS) + : preparedVersions; }, [allVersionsDisplayed, preparedVersions, shouldTruncateVersions]); const handleShowAllVersions = (event: React.MouseEvent) => { @@ -55,7 +61,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { const renderButton = () => { if (shouldTruncateVersions) { - const truncatedVersionsCount = preparedVersions.length - 3; + const truncatedVersionsCount = preparedVersions.length - MAX_DISPLAYED_VERSIONS; if (allVersionsDisplayed) { return ( diff --git a/src/utils/versions/__test__/sortVerions.test.ts b/src/utils/versions/__test__/sortVersions.test.ts similarity index 100% rename from src/utils/versions/__test__/sortVerions.test.ts rename to src/utils/versions/__test__/sortVersions.test.ts diff --git a/src/utils/versions/clusterVersionColors.ts b/src/utils/versions/clusterVersionColors.ts index 0aee8b2016..bb505e939d 100644 --- a/src/utils/versions/clusterVersionColors.ts +++ b/src/utils/versions/clusterVersionColors.ts @@ -74,7 +74,7 @@ export const prepareClusterVersions = ( filteredVersions.forEach((item) => { if (result[item.version]) { - // Summ count for versions of different nodes types + // Sum count for versions of different nodes types const currentCount = result[item.version].count || 0; const itemCount = item.count || 0; diff --git a/src/utils/versions/sortVersions.ts b/src/utils/versions/sortVersions.ts index 6ac2f49495..97d7d5e75e 100644 --- a/src/utils/versions/sortVersions.ts +++ b/src/utils/versions/sortVersions.ts @@ -4,7 +4,8 @@ import type {PreparedVersion} from './types'; /** * Sorts cluster versions according to the following rules: * 1. First by majorIndex in ascending order (lower index first) - * - Higher version numbers typically have lower indices (e.g., v3.0.0: index 0, v2.0.0: index 1, v1.0.0: index 2) + * - In embedded versions higher version numbers typically have lower indices (e.g., v3.0.0: index 0, v2.0.0: index 1, v1.0.0: index 2) + * - In multi-cluster version indices may be provided by backend with no specific rule, but we use the same sorting for consistency * - Versions with undefined majorIndex come last * 2. Then by minorIndex in ascending order (lower index first) when majorIndex is the same * - Higher minor versions typically have lower indices From 107d2e673093e60a0657bdf3a6f20277c0de73a2 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 31 Jul 2025 17:19:09 +0300 Subject: [PATCH 4/6] fix: copilot review 3 --- .../versions/__test__/sortVersions.test.ts | 4 +-- src/utils/versions/clusterVersionColors.ts | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/utils/versions/__test__/sortVersions.test.ts b/src/utils/versions/__test__/sortVersions.test.ts index d3031b08bb..b2a8e0b471 100644 --- a/src/utils/versions/__test__/sortVersions.test.ts +++ b/src/utils/versions/__test__/sortVersions.test.ts @@ -2,7 +2,7 @@ import {sortVersions} from '../sortVersions'; import type {PreparedVersion} from '../types'; describe('sortVersions', () => { - test('should sort versions by majorIndex in descending order', function () { + test('should sort versions by majorIndex in ascending order', function () { const versions: PreparedVersion[] = [ {version: 'v2.0.0', majorIndex: 1}, {version: 'v1.0.0', majorIndex: 2}, @@ -34,7 +34,7 @@ describe('sortVersions', () => { ]); }); - test('should sort versions by minorIndex in descending order when majorIndex is the same', function () { + test('should sort versions by minorIndex in ascending order when majorIndex is the same', function () { const versions: PreparedVersion[] = [ {version: 'v1.2.0', majorIndex: 2, minorIndex: 1}, {version: 'v1.1.0', majorIndex: 2, minorIndex: 2}, diff --git a/src/utils/versions/clusterVersionColors.ts b/src/utils/versions/clusterVersionColors.ts index bb505e939d..8abed4b398 100644 --- a/src/utils/versions/clusterVersionColors.ts +++ b/src/utils/versions/clusterVersionColors.ts @@ -1,5 +1,7 @@ import type {MetaClusterVersion} from '../../types/api/meta'; +import {COLORS, DEFAULT_COLOR, getMinorVersionColorVariant, hashCode} from './getVersionsColors'; +import {getMinorVersion} from './parseVersion'; import {sortVersions} from './sortVersions'; import type { ColorIndexToVersionsMap, @@ -8,8 +10,6 @@ import type { VersionsDataMap, } from './types'; -import {COLORS, DEFAULT_COLOR, getMinorVersion, getMinorVersionColorVariant, hashCode} from '.'; - const UNDEFINED_COLOR_INDEX = '__no_color__'; export const getVersionMap = ( @@ -75,23 +75,24 @@ export const prepareClusterVersions = ( filteredVersions.forEach((item) => { if (result[item.version]) { // Sum count for versions of different nodes types + // Do not recalculate version data const currentCount = result[item.version].count || 0; const itemCount = item.count || 0; result[item.version].count = currentCount + itemCount; + } else { + const minorVersion = getMinorVersion(item.version); + const data = versionsDataMap.get(minorVersion); + + result[item.version] = { + version: item.version, + minorVersion, + color: data?.color, + majorIndex: data?.majorIndex, + minorIndex: data?.minorIndex, + count: item.count || 0, + }; } - - const minorVersion = getMinorVersion(item.version); - const data = versionsDataMap.get(minorVersion); - - result[item.version] = { - version: item.version, - minorVersion, - color: data?.color, - majorIndex: data?.majorIndex, - minorIndex: data?.minorIndex, - count: item.count || 0, - }; }); return sortVersions(Object.values(result)); From e6a662b03785e02ac7134d801d279c624d0033a6 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 31 Jul 2025 17:27:53 +0300 Subject: [PATCH 5/6] fix: copilot review 4 --- src/utils/versions/getVersionsColors.ts | 2 +- src/utils/versions/sortVersions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/versions/getVersionsColors.ts b/src/utils/versions/getVersionsColors.ts index 7e408cc607..cc43671441 100644 --- a/src/utils/versions/getVersionsColors.ts +++ b/src/utils/versions/getVersionsColors.ts @@ -143,7 +143,7 @@ export const getVersionsDataMap = (versionsMap: VersionsMap) => { currentColorIndex = (currentColorIndex + 1) % COLORS.length; versionsDataMap.set(item.version, { - // Use fisrt color for major + // Use first color for major color: COLORS[currentColorIndex][0], majorIndex: currentColorIndex, minorIndex: 0, diff --git a/src/utils/versions/sortVersions.ts b/src/utils/versions/sortVersions.ts index 97d7d5e75e..54fc73d20d 100644 --- a/src/utils/versions/sortVersions.ts +++ b/src/utils/versions/sortVersions.ts @@ -4,7 +4,7 @@ import type {PreparedVersion} from './types'; /** * Sorts cluster versions according to the following rules: * 1. First by majorIndex in ascending order (lower index first) - * - In embedded versions higher version numbers typically have lower indices (e.g., v3.0.0: index 0, v2.0.0: index 1, v1.0.0: index 2) + * - In single-cluster versions higher version numbers typically have lower indices (e.g., v3.0.0: index 0, v2.0.0: index 1, v1.0.0: index 2) * - In multi-cluster version indices may be provided by backend with no specific rule, but we use the same sorting for consistency * - Versions with undefined majorIndex come last * 2. Then by minorIndex in ascending order (lower index first) when majorIndex is the same From 09c80c56c3a1db7f38cc8d0d9b2cdd570a47e8cd Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 1 Aug 2025 10:34:16 +0300 Subject: [PATCH 6/6] fix: review --- src/components/VersionsBar/VersionsBar.tsx | 62 +++++++++++++--------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx index 794a7ea5fb..bb422fac6a 100644 --- a/src/components/VersionsBar/VersionsBar.tsx +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {Button, Flex, Tooltip} from '@gravity-ui/uikit'; +import {debounce} from 'lodash'; import {cn} from '../../utils/cn'; import type {PreparedVersion} from '../../utils/versions/types'; @@ -15,6 +16,9 @@ const TRUNCATION_THRESHOLD = 4; // One more line for Show more / Hide button const MAX_DISPLAYED_VERSIONS = TRUNCATION_THRESHOLD - 1; +const HOVER_DELAY = 200; +const TOOLTIP_OPEN_DELAY = 200; + interface VersionsBarProps { preparedVersions: PreparedVersion[]; } @@ -60,31 +64,39 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { }; const renderButton = () => { - if (shouldTruncateVersions) { - const truncatedVersionsCount = preparedVersions.length - MAX_DISPLAYED_VERSIONS; - - if (allVersionsDisplayed) { - return ( - - ); - } else { - return ( - - ); - } + if (!shouldTruncateVersions) { + return null; + } + + const truncatedVersionsCount = preparedVersions.length - MAX_DISPLAYED_VERSIONS; + + if (allVersionsDisplayed) { + return ( + + ); + } else { + return ( + + ); } - return null; }; + const handleMouseEnter = React.useMemo(() => { + return debounce((version: string) => { + setHoveredVersion(version); + }, HOVER_DELAY); + }, []); + const handleMouseLeave = () => { + handleMouseEnter.cancel(); setHoveredVersion(undefined); }; @@ -106,11 +118,11 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) {
} placement={'top-start'} - openDelay={100} + openDelay={TOOLTIP_OPEN_DELAY} > { - setHoveredVersion(item.version); + handleMouseEnter(item.version); }} onMouseLeave={handleMouseLeave} className={b('version', {dimmed: isDimmed(item.version)})} @@ -126,7 +138,7 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { key={item.version} content={i18n('tooltip_nodes', {count: item.count})} placement={'bottom-end'} - openDelay={100} + openDelay={TOOLTIP_OPEN_DELAY} > { - setHoveredVersion(item.version); + handleMouseEnter(item.version); }} onMouseLeave={handleMouseLeave} >