From 698a37673d66f5ab3efd10e78cdae4f85b1cd5fe Mon Sep 17 00:00:00 2001 From: mufazalov Date: Tue, 12 Aug 2025 11:56:15 +0300 Subject: [PATCH 1/3] feat(Versions): redesign --- src/components/VersionsBar/VersionsBar.scss | 50 +++- src/components/VersionsBar/VersionsBar.tsx | 133 ++++++--- .../Cluster/VersionsBar/VersionsBar.scss | 27 -- .../Cluster/VersionsBar/VersionsBar.tsx | 39 --- .../GroupedNodesTree/GroupedNodesTree.scss | 42 +-- .../GroupedNodesTree/GroupedNodesTree.tsx | 108 ++++---- .../NodesTreeTitle/NodesTreeTitle.scss | 52 +--- .../NodesTreeTitle/NodesTreeTitle.tsx | 78 +++--- src/containers/Versions/Versions.scss | 45 +-- src/containers/Versions/Versions.tsx | 260 ++++++++++++------ .../Versions/VersionsBlock/VersionsBlock.scss | 7 + .../Versions/VersionsBlock/VersionsBlock.tsx | 28 ++ src/containers/Versions/constants.ts | 17 ++ src/containers/Versions/groupNodes.ts | 8 +- src/containers/Versions/i18n/en.json | 18 +- src/containers/Versions/types.ts | 5 +- src/containers/Versions/utils.ts | 24 +- .../versions/parseNodesToVersionsValues.ts | 59 ++-- src/utils/versions/types.ts | 7 - 19 files changed, 539 insertions(+), 468 deletions(-) delete mode 100644 src/containers/Cluster/VersionsBar/VersionsBar.scss delete mode 100644 src/containers/Cluster/VersionsBar/VersionsBar.tsx create mode 100644 src/containers/Versions/VersionsBlock/VersionsBlock.scss create mode 100644 src/containers/Versions/VersionsBlock/VersionsBlock.tsx create mode 100644 src/containers/Versions/constants.ts diff --git a/src/components/VersionsBar/VersionsBar.scss b/src/components/VersionsBar/VersionsBar.scss index ebe36586fb..a572e069ed 100644 --- a/src/components/VersionsBar/VersionsBar.scss +++ b/src/components/VersionsBar/VersionsBar.scss @@ -1,16 +1,50 @@ @use '../../styles/mixins.scss'; .ydb-versions-bar { + &__bar-wrapper { + gap: var(--g-spacing-2); + + width: 100%; + + &_size_m { + gap: var(--g-spacing-3); + } + } + &__bar { + gap: var(--g-spacing-half); + width: 100%; height: 10px; + + &_size_m { + gap: var(--g-spacing-1); + + height: 20px; + } } &__titles-wrapper { - width: max-content; + flex-direction: column; + gap: var(--g-spacing-half); + + &_size_m { + flex-flow: row wrap; + gap: var(--g-spacing-4); + } } &__title { + gap: var(--g-spacing-1); + + width: max-content; + + &_size_m { + gap: var(--g-spacing-2); + } + } + + &__title-text { overflow: hidden; text-overflow: ellipsis; @@ -18,17 +52,29 @@ color: var(--g-color-text-primary); @include mixins.body-1-typography(); + + &_size_m { + overflow: visible; + + text-overflow: initial; + } } &__version { min-width: 10px; border-radius: var(--g-border-radius-xs); + + &_size_m { + min-width: 20px; + } } - &__title, + &__title-text, &__version, &__version-icon { + cursor: pointer; + &_dimmed { opacity: 0.5; } diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx index 0241129aab..7876bbc149 100644 --- a/src/components/VersionsBar/VersionsBar.tsx +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -19,12 +19,16 @@ const MAX_DISPLAYED_VERSIONS = TRUNCATION_THRESHOLD - 1; const HOVER_DELAY = 200; const TOOLTIP_OPEN_DELAY = 200; +type VersionsBarSize = 's' | 'm'; + interface VersionsBarProps { preparedVersions: PreparedVersion[]; + withTitles?: boolean; + size?: VersionsBarSize; } -export function VersionsBar({preparedVersions}: VersionsBarProps) { - const shouldTruncateVersions = preparedVersions.length > TRUNCATION_THRESHOLD; +export function VersionsBar({preparedVersions, withTitles = true, size = 's'}: VersionsBarProps) { + const shouldTruncateVersions = preparedVersions.length > TRUNCATION_THRESHOLD && size === 's'; const [hoveredVersion, setHoveredVersion] = React.useState(); const [allVersionsDisplayed, setAllVersionsDisplayed] = React.useState(false); @@ -103,12 +107,12 @@ export function VersionsBar({preparedVersions}: VersionsBarProps) { }, [handleMouseEnter]); const isDimmed = (version: string) => { - return hoveredVersion && hoveredVersion !== version; + return Boolean(hoveredVersion && hoveredVersion !== version); }; return ( - - + + {displayedVersions.map((item) => ( } - placement={'top-start'} + placement={size === 'm' ? 'auto' : 'top-start'} openDelay={TOOLTIP_OPEN_DELAY} > ))} - - {truncatedDisplayedVersions.map((item) => ( - - - - - -
{ - handleMouseEnter(item.version); - }} - onMouseLeave={handleMouseLeave} - > - {item.version} -
-
-
- ))} - {renderButton()} -
+ {withTitles && ( + + {truncatedDisplayedVersions.map((item) => ( + { + handleMouseEnter(item.version); + }} + onMouseLeave={handleMouseLeave} + size={size} + /> + ))} + {renderButton()} + + )}
); } + +interface VersionTitleProps { + version: string; + color: string; + count?: number; + isDimmed: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; + size: VersionsBarSize; +} + +function VersionTitle({ + version, + color, + count, + isDimmed, + onMouseEnter, + onMouseLeave, + size, +}: VersionTitleProps) { + return ( + + + +
+ {version} +
+
+
+ ); +} + +interface VersionCircleProps { + size: VersionsBarSize; + dimmed: boolean; + color: string; +} + +function VersionCircle({size, dimmed, color}: VersionCircleProps) { + const numericSize = size === 'm' ? 8 : 6; + const radius = numericSize / 2; + + return ( + + + + ); +} diff --git a/src/containers/Cluster/VersionsBar/VersionsBar.scss b/src/containers/Cluster/VersionsBar/VersionsBar.scss deleted file mode 100644 index 7631afd47f..0000000000 --- a/src/containers/Cluster/VersionsBar/VersionsBar.scss +++ /dev/null @@ -1,27 +0,0 @@ -.ydb-cluster-versions-bar { - display: flex; - flex-direction: column; - - min-width: 600px; - - & .g-progress { - width: 100%; - } - - &__versions { - display: flex; - flex-flow: row wrap; - - margin-top: 6px; - } - - &__version-title { - margin-left: 3px; - - white-space: nowrap; - } - - & .g-progress__stack { - cursor: pointer; - } -} diff --git a/src/containers/Cluster/VersionsBar/VersionsBar.tsx b/src/containers/Cluster/VersionsBar/VersionsBar.tsx deleted file mode 100644 index d0ddee8d8a..0000000000 --- a/src/containers/Cluster/VersionsBar/VersionsBar.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type {ProgressProps} from '@gravity-ui/uikit'; -import {Progress} from '@gravity-ui/uikit'; - -import {cn} from '../../../utils/cn'; -import type {VersionValue} from '../../../utils/versions/types'; - -import './VersionsBar.scss'; - -const b = cn('ydb-cluster-versions-bar'); - -interface VersionsBarProps { - versionsValues?: VersionValue[]; - size?: ProgressProps['size']; - progressClassName?: string; -} - -export const VersionsBar = ({ - versionsValues = [], - size = 's', - progressClassName: className, -}: VersionsBarProps) => { - return ( -
- -
- {versionsValues.map((item, index) => ( -
- {`${item.version}${index === versionsValues.length - 1 ? '' : ','}`} -
- ))} -
-
- ); -}; diff --git a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.scss b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.scss index 0cdd0c626d..26234749e8 100644 --- a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.scss +++ b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.scss @@ -1,15 +1,9 @@ @use '../../../styles/mixins.scss'; .ydb-versions-grouped-node-tree { - $item-width: 100%; - $margin-size: 24px; - - &_first-level { - margin-top: 10px; - margin-bottom: 10px; - + &__wrapper { border: 1px solid var(--g-color-line-generic); - border-radius: 10px; + border-radius: var(--g-border-radius-m); } &__dt-wrapper { @@ -17,37 +11,5 @@ z-index: 0; overflow: auto hidden; - - margin-right: $margin-size; - margin-left: $margin-size; - } - - .ydb-tree-view { - @include mixins.body-2-typography(); - - // Apply margin ignoring first element of the tree - .ydb-tree-view { - margin-left: $margin-size; - } - } - - & .tree-view_item { - height: 40px; - margin: 0; - - // By default tree is rendered with padding calculated based on level - // We replace padding with margin for correct hover - padding: 0 10px !important; - - border: 0; - border-radius: 10px; - } - - & .tree-view_children .tree-view_item { - width: $item-width; - } - - & .g-progress__stack { - cursor: pointer; } } diff --git a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx index acfc0d2370..cc76ebe172 100644 --- a/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx +++ b/src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import {TreeView} from 'ydb-ui-components'; - import type {NodesPreparedEntity} from '../../../store/reducers/nodes/types'; import {cn} from '../../../utils/cn'; -import type {VersionValue} from '../../../utils/versions/types'; +import type {PreparedVersion} from '../../../utils/versions/types'; import {NodesTable} from '../NodesTable/NodesTable'; import {NodesTreeTitle} from '../NodesTreeTitle/NodesTreeTitle'; +import {VersionsBlock} from '../VersionsBlock/VersionsBlock'; import type {GroupedNodesItem} from '../types'; import './GroupedNodesTree.scss'; @@ -15,83 +14,80 @@ const b = cn('ydb-versions-grouped-node-tree'); interface GroupedNodesTreeProps { title?: string; + isDatabase?: boolean; nodes?: NodesPreparedEntity[]; items?: GroupedNodesItem[]; expanded?: boolean; versionColor?: string; - versionsValues?: VersionValue[]; - level?: number; + preparedVersions?: PreparedVersion[]; } export const GroupedNodesTree = ({ title, + isDatabase, nodes, items, expanded = false, versionColor, - versionsValues, - level = 0, + preparedVersions, }: GroupedNodesTreeProps) => { - const [isOpened, toggleBlock] = React.useState(false); + const [isOpened, setIsOpened] = React.useState(false); React.useEffect(() => { - toggleBlock(expanded); + setIsOpened(expanded); }, [expanded]); - const groupTitle = ( - - ); - const toggleCollapsed = () => { - toggleBlock((value) => !value); + setIsOpened((value) => !value); }; - if (items) { + const renderHeader = () => { return ( -
- - {items.map((item, index) => ( - - ))} - -
+ ); - } + }; + + const renderItemsContent = () => { + return items?.map((item, index) => ( + + )); + }; + + const renderNodesContent = () => { + return ; + }; + + const renderContent = () => { + if (items) { + return renderItemsContent(); + } + return renderNodesContent(); + }; return ( -
- -
- -
-
+
+
); }; diff --git a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.scss b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.scss index d3cf9f69cb..b3c5df008d 100644 --- a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.scss +++ b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.scss @@ -5,54 +5,26 @@ align-items: center; width: 100%; - } - - &__overview-info { - display: flex; - align-items: center; - - margin-left: 25px; - - & > *:not(:first-child) { - margin-left: 30px; - } - } - - &__overview-container { - display: flex; - align-items: center; - } + padding: var(--g-spacing-4); - &__info-label { - font-weight: 200; + cursor: pointer; - color: var(--g-color-text-complementary); + border-radius: var(--g-border-radius-m); - &_margin_left { - margin-left: 5px; - } - &_margin_right { - margin-right: 5px; + &:hover { + background-color: var(--g-color-base-generic-hover); } } &__version-color { - width: 16px; - height: 16px; - margin-right: 10px; + width: 12px; + height: 12px; border-radius: 100%; } &__version-progress { - display: flex; - align-items: center; - width: 250px; - - & .g-progress { - width: 200px; - } } &__overview-title { @@ -60,14 +32,8 @@ align-items: center; } - &__clipboard-button { - margin-left: 8px; - - opacity: 0; + &__clipboard-button, + &__icon { color: var(--g-color-text-secondary); - .ydb-tree-view__item:hover &, - &:focus-visible { - opacity: 1; - } } } diff --git a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx index e412637072..f1071fb422 100644 --- a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx +++ b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx @@ -1,8 +1,13 @@ -import {ClipboardButton, Progress} from '@gravity-ui/uikit'; +import React from 'react'; +import {ChevronDown, ChevronUp, Database} from '@gravity-ui/icons'; +import {ClipboardButton, Flex, Icon, Text} from '@gravity-ui/uikit'; + +import {VersionsBar} from '../../../components/VersionsBar/VersionsBar'; import {cn} from '../../../utils/cn'; import type {PreparedNodeSystemState} from '../../../utils/nodes'; -import type {VersionValue} from '../../../utils/versions/types'; +import type {PreparedVersion} from '../../../utils/versions/types'; +import i18n from '../i18n'; import type {GroupedNodesItem} from '../types'; import './NodesTreeTitle.scss'; @@ -11,61 +16,72 @@ const b = cn('ydb-versions-nodes-tree-title'); interface NodesTreeTitleProps { title?: string; + isDatabase?: boolean; + expanded?: boolean; nodes?: PreparedNodeSystemState[]; items?: GroupedNodesItem[]; versionColor?: string; - versionsValues?: VersionValue[]; + preparedVersions?: PreparedVersion[]; + onClick?: () => void; } export const NodesTreeTitle = ({ title, + isDatabase, + expanded, nodes, items, versionColor, - versionsValues, + preparedVersions, + onClick, }: NodesTreeTitleProps) => { - let nodesAmount; - if (items) { - nodesAmount = items.reduce((acc, curr) => { - if (!curr.nodes) { - return acc; - } - return acc + curr.nodes.length; - }, 0); - } else { - nodesAmount = nodes ? nodes.length : 0; - } + const nodesAmount = React.useMemo(() => { + if (items) { + return items.reduce((acc, curr) => { + if (!curr.nodes) { + return acc; + } + return acc + curr.nodes.length; + }, 0); + } else { + return nodes ? nodes.length : 0; + } + }, [items, nodes]); return ( -
-
- {versionColor ? ( +
+ + {versionColor && !isDatabase ? (
) : null} + {isDatabase ? : null} {title ? ( - + {title} { + e.preventDefault(); + e.stopPropagation(); + }} /> - + ) : null} -
-
-
- {nodesAmount} - Nodes -
- {versionsValues ? ( + + {i18n('nodes-count', {count: nodesAmount})} + + + + {preparedVersions ? (
- Versions - +
) : null} -
+ +
); }; diff --git a/src/containers/Versions/Versions.scss b/src/containers/Versions/Versions.scss index 928965e7cb..ab6cd9a409 100644 --- a/src/containers/Versions/Versions.scss +++ b/src/containers/Versions/Versions.scss @@ -4,49 +4,16 @@ --ydb-info-viewer-font-size: var(--g-text-body-2-font-size); --ydb-info-viewer-line-height: var(--g-text-body-2-line-height); + padding-right: var(--g-spacing-5); + font-size: var(--ydb-info-viewer-font-size); line-height: var(--ydb-info-viewer-line-height); &__controls { - display: flex; - align-items: center; - - padding: 0 0 20px; - - #{$_} { - &__label { - margin-right: 10px; - - font-weight: 500; - } - - &__checkbox { - margin: 0; - } - } - - & > * { - margin-right: 25px; - } + margin-bottom: var(--g-spacing-3); } - &__overall-wrapper { - margin-top: 10px; - margin-bottom: 10px; - padding: 20px; - - border: 1px solid var(--g-color-line-generic); - border-radius: 10px; - } - &__overall-progress { - height: 20px; - - line-height: 20px; - - border-radius: 5px; - .g-progress__stack { - height: 20px; - - line-height: 20px; - } + &__overall { + margin-top: var(--g-spacing-4); + margin-bottom: var(--g-spacing-6); } } diff --git a/src/containers/Versions/Versions.tsx b/src/containers/Versions/Versions.tsx index 438762894b..9e2c7ac28f 100644 --- a/src/containers/Versions/Versions.tsx +++ b/src/containers/Versions/Versions.tsx @@ -1,21 +1,26 @@ import React from 'react'; -import {Checkbox, SegmentedRadioGroup} from '@gravity-ui/uikit'; +import {ChevronsCollapseVertical, ChevronsExpandVertical} from '@gravity-ui/icons'; +import {Button, Flex, Icon, SegmentedRadioGroup, Select, Text} from '@gravity-ui/uikit'; +import {StringParam, useQueryParams} from 'use-query-params'; +import {z} from 'zod'; import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; +import {VersionsBar} from '../../components/VersionsBar/VersionsBar'; import {nodesApi} from '../../store/reducers/nodes/nodes'; import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; import type {TClusterInfo} from '../../types/api/cluster'; 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 type {PreparedVersion, VersionsDataMap} from '../../utils/versions/types'; import {GroupedNodesTree} from './GroupedNodesTree/GroupedNodesTree'; +import type {NodeType} from './constants'; +import {NODE_TYPES, NODE_TYPES_TITLE} from './constants'; import {getGroupedStorageNodes, getGroupedTenantNodes, getOtherNodes} from './groupNodes'; import i18n from './i18n'; import {GroupByValue} from './types'; -import {useGetVersionValues, useVersionsDataMap} from './utils'; +import {useGetPreparedVersions, useVersionsDataMap} from './utils'; import './Versions.scss'; @@ -34,12 +39,16 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) { ); const versionsDataMap = useVersionsDataMap(cluster); - const versionsValues = useGetVersionValues({cluster, versionsDataMap, clusterLoading: loading}); + const preparedVersions = useGetPreparedVersions({ + cluster, + versionsDataMap, + clusterLoading: loading, + }); return ( @@ -49,117 +58,198 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) { interface VersionsProps { nodes?: NodesPreparedEntity[]; - versionsValues: VersionValue[]; + preparedVersions: PreparedVersion[]; versionsDataMap?: VersionsDataMap; } -function Versions({versionsValues, nodes, versionsDataMap}: VersionsProps) { - const [groupByValue, setGroupByValue] = React.useState(GroupByValue.VERSION); +const nodeTypeSchema = z.nativeEnum(NODE_TYPES).catch(NODE_TYPES.storage); +const groupByValueSchema = z.nativeEnum(GroupByValue).catch(GroupByValue.VERSION); + +function Versions({preparedVersions, nodes, versionsDataMap}: VersionsProps) { + const [{nodeType: rawNodeType, groupBy: rawGroupByValue}, setQueryParams] = useQueryParams({ + nodeType: StringParam, + groupBy: StringParam, + }); + + const nodeType = nodeTypeSchema.parse(rawNodeType); + const groupByValue = groupByValueSchema.parse(rawGroupByValue); + const [expanded, setExpanded] = React.useState(false); + const tenantNodes = React.useMemo(() => { + return getGroupedTenantNodes(nodes, versionsDataMap, groupByValue); + }, [groupByValue, nodes, versionsDataMap]); + const storageNodes = React.useMemo(() => { + return getGroupedStorageNodes(nodes, versionsDataMap); + }, [nodes, versionsDataMap]); + const otherNodes = React.useMemo(() => { + return getOtherNodes(nodes, versionsDataMap); + }, [nodes, versionsDataMap]); + const handleGroupByValueChange = (value: string) => { - setGroupByValue(value as GroupByValue); + setQueryParams({groupBy: value as GroupByValue}, 'replaceIn'); + }; + const handleNodeTypeChange = (value: string) => { + setQueryParams({nodeType: value as NodeType}, 'replaceIn'); }; - const renderGroupControl = () => { + const renderExpandButton = () => { return ( -
- Group by: - - - {GroupByValue.TENANT} - - - {GroupByValue.VERSION} - - -
+ ); }; + + const renderNodeTypeRadio = () => { + const options = [ + + {NODE_TYPES_TITLE.storage} + , + + {NODE_TYPES_TITLE.database} + , + ]; + + if (otherNodes?.length) { + options.push( + + {NODE_TYPES_TITLE.other} + , + ); + } + + return ( + + {options} + + ); + }; + + const renderGroupControl = () => { + if (nodeType === NODE_TYPES.database) { + const options = [ + {value: GroupByValue.TENANT, content: i18n('title_database')}, + {value: GroupByValue.VERSION, content: i18n('title_version')}, + ]; + return ( + handleGroupByValueChange(values[0])} From 2302736c715c6ae4ec8339f5e73359219901af10 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 15 Aug 2025 16:39:13 +0300 Subject: [PATCH 3/3] fix: review --- src/components/VersionsBar/VersionsBar.scss | 4 ++++ src/components/VersionsBar/VersionsBar.tsx | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/VersionsBar/VersionsBar.scss b/src/components/VersionsBar/VersionsBar.scss index a572e069ed..7f48d95d2e 100644 --- a/src/components/VersionsBar/VersionsBar.scss +++ b/src/components/VersionsBar/VersionsBar.scss @@ -70,6 +70,10 @@ } } + &__button { + width: max-content; + } + &__title-text, &__version, &__version-icon { diff --git a/src/components/VersionsBar/VersionsBar.tsx b/src/components/VersionsBar/VersionsBar.tsx index 7876bbc149..21c3f22f78 100644 --- a/src/components/VersionsBar/VersionsBar.tsx +++ b/src/components/VersionsBar/VersionsBar.tsx @@ -76,7 +76,12 @@ export function VersionsBar({preparedVersions, withTitles = true, size = 's'}: V if (allVersionsDisplayed) { return ( -