Skip to content

Commit 7be9d5f

Browse files
feat(Clusters): rework versions progress bar (#2642)
1 parent 5918555 commit 7be9d5f

File tree

24 files changed

+630
-211
lines changed

24 files changed

+630
-211
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@use '../../styles/mixins.scss';
2+
3+
.ydb-versions-bar {
4+
&__bar {
5+
width: 100%;
6+
height: 10px;
7+
}
8+
9+
&__titles-wrapper {
10+
width: max-content;
11+
}
12+
13+
&__title {
14+
overflow: hidden;
15+
16+
text-overflow: ellipsis;
17+
18+
color: var(--g-color-text-primary);
19+
20+
@include mixins.body-1-typography();
21+
}
22+
23+
&__version {
24+
min-width: 10px;
25+
26+
border-radius: var(--g-border-radius-xs);
27+
}
28+
29+
&__title,
30+
&__version,
31+
&__version-icon {
32+
&_dimmed {
33+
opacity: 0.5;
34+
}
35+
}
36+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import React from 'react';
2+
3+
import {Button, Flex, Tooltip} from '@gravity-ui/uikit';
4+
import {debounce} from 'lodash';
5+
6+
import {cn} from '../../utils/cn';
7+
import type {PreparedVersion} from '../../utils/versions/types';
8+
9+
import i18n from './i18n';
10+
11+
import './VersionsBar.scss';
12+
13+
const b = cn('ydb-versions-bar');
14+
15+
const TRUNCATION_THRESHOLD = 4;
16+
// One more line for Show more / Hide button
17+
const MAX_DISPLAYED_VERSIONS = TRUNCATION_THRESHOLD - 1;
18+
19+
const HOVER_DELAY = 200;
20+
const TOOLTIP_OPEN_DELAY = 200;
21+
22+
interface VersionsBarProps {
23+
preparedVersions: PreparedVersion[];
24+
}
25+
26+
export function VersionsBar({preparedVersions}: VersionsBarProps) {
27+
const shouldTruncateVersions = preparedVersions.length > TRUNCATION_THRESHOLD;
28+
29+
const [hoveredVersion, setHoveredVersion] = React.useState<string | undefined>();
30+
const [allVersionsDisplayed, setAllVersionsDisplayed] = React.useState<boolean>(false);
31+
32+
const displayedVersions = React.useMemo(() => {
33+
const total = preparedVersions.reduce((acc, item) => acc + (item.count || 0), 0);
34+
35+
return preparedVersions.map((item) => {
36+
return {
37+
value: ((item.count || 0) / total) * 100,
38+
color: item.color,
39+
version: item.version,
40+
count: item.count,
41+
};
42+
});
43+
}, [preparedVersions]);
44+
45+
const truncatedDisplayedVersions = React.useMemo(() => {
46+
if (allVersionsDisplayed) {
47+
return preparedVersions;
48+
}
49+
50+
return shouldTruncateVersions
51+
? preparedVersions.slice(0, MAX_DISPLAYED_VERSIONS)
52+
: preparedVersions;
53+
}, [allVersionsDisplayed, preparedVersions, shouldTruncateVersions]);
54+
55+
const handleShowAllVersions = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
56+
event.preventDefault();
57+
event.stopPropagation();
58+
setAllVersionsDisplayed(true);
59+
};
60+
const handleHideAllVersions = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
61+
event.preventDefault();
62+
event.stopPropagation();
63+
setAllVersionsDisplayed(false);
64+
};
65+
66+
const renderButton = () => {
67+
if (!shouldTruncateVersions) {
68+
return null;
69+
}
70+
71+
const truncatedVersionsCount = preparedVersions.length - MAX_DISPLAYED_VERSIONS;
72+
73+
if (allVersionsDisplayed) {
74+
return (
75+
<Button view="flat-secondary" size={'s'} onClick={handleHideAllVersions}>
76+
{i18n('action_hide', {
77+
count: truncatedVersionsCount,
78+
})}
79+
</Button>
80+
);
81+
} else {
82+
return (
83+
<Button view="flat-secondary" size={'s'} onClick={handleShowAllVersions}>
84+
{i18n('action_show_more', {
85+
count: truncatedVersionsCount,
86+
})}
87+
</Button>
88+
);
89+
}
90+
};
91+
92+
const handleMouseEnter = React.useMemo(() => {
93+
return debounce((version: string) => {
94+
setHoveredVersion(version);
95+
}, HOVER_DELAY);
96+
}, []);
97+
98+
const handleMouseLeave = () => {
99+
handleMouseEnter.cancel();
100+
setHoveredVersion(undefined);
101+
};
102+
103+
const isDimmed = (version: string) => {
104+
return hoveredVersion && hoveredVersion !== version;
105+
};
106+
107+
return (
108+
<Flex gap={2} direction={'column'} className={b(null)} wrap>
109+
<Flex className={b('bar')} grow={1} gap={0.5}>
110+
{displayedVersions.map((item) => (
111+
<Tooltip
112+
key={item.version}
113+
content={
114+
<React.Fragment>
115+
{i18n('tooltip_nodes', {count: item.count})}
116+
<br />
117+
{item.version}
118+
</React.Fragment>
119+
}
120+
placement={'top-start'}
121+
openDelay={TOOLTIP_OPEN_DELAY}
122+
>
123+
<span
124+
onMouseEnter={() => {
125+
handleMouseEnter(item.version);
126+
}}
127+
onMouseLeave={handleMouseLeave}
128+
className={b('version', {dimmed: isDimmed(item.version)})}
129+
style={{backgroundColor: item.color, width: `${item.value}%`}}
130+
/>
131+
</Tooltip>
132+
))}
133+
</Flex>
134+
135+
<Flex gap={0.5} direction={'column'}>
136+
{truncatedDisplayedVersions.map((item) => (
137+
<Tooltip
138+
key={item.version}
139+
content={i18n('tooltip_nodes', {count: item.count})}
140+
placement={'bottom-end'}
141+
openDelay={TOOLTIP_OPEN_DELAY}
142+
>
143+
<Flex gap={1} alignItems={'center'} className={b('titles-wrapper')}>
144+
<svg
145+
xmlns="http://www.w3.org/2000/svg"
146+
width="6"
147+
height="6"
148+
viewBox="0 0 6 6"
149+
fill="none"
150+
className={b('version-icon', {dimmed: isDimmed(item.version)})}
151+
>
152+
<circle cx="3" cy="3" r="3" fill={item.color} />
153+
</svg>
154+
<div
155+
className={b('title', {dimmed: isDimmed(item.version)})}
156+
onMouseEnter={() => {
157+
handleMouseEnter(item.version);
158+
}}
159+
onMouseLeave={handleMouseLeave}
160+
>
161+
{item.version}
162+
</div>
163+
</Flex>
164+
</Tooltip>
165+
))}
166+
<Flex>{renderButton()}</Flex>
167+
</Flex>
168+
</Flex>
169+
);
170+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"action_show_more": "Show {{count}} more",
3+
"action_hide": "Hide {{count}}",
4+
"tooltip_nodes": {
5+
"one": "{{count}} Node",
6+
"other": "{{count}} Nodes"
7+
}
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {registerKeysets} from '../../../utils/i18n';
2+
3+
import en from './en.json';
4+
5+
const COMPONENT = 'ydb-versions-bar';
6+
7+
export default registerKeysets(COMPONENT, {en});

src/containers/Cluster/VersionsBar/VersionsBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {ProgressProps} from '@gravity-ui/uikit';
22
import {Progress} from '@gravity-ui/uikit';
33

4-
import type {VersionValue} from '../../../types/versions';
54
import {cn} from '../../../utils/cn';
5+
import type {VersionValue} from '../../../utils/versions/types';
66

77
import './VersionsBar.scss';
88

src/containers/Clusters/Clusters.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@
2626
&__cluster-versions {
2727
text-decoration: none;
2828
}
29-
&__cluster-version {
30-
overflow: hidden;
3129

32-
text-overflow: ellipsis;
33-
}
3430
&__cluster-dc {
3531
white-space: normal;
3632
}

src/containers/Clusters/columns.tsx

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from 'react';
2-
31
import {Pencil, TrashBin} from '@gravity-ui/icons';
42
import DataTable from '@gravity-ui/react-data-table';
53
import type {Column} from '@gravity-ui/react-data-table';
@@ -15,6 +13,7 @@ import {
1513
} from '@gravity-ui/uikit';
1614

1715
import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus';
16+
import {VersionsBar} from '../../components/VersionsBar/VersionsBar';
1817
import type {PreparedCluster} from '../../store/reducers/clusters/types';
1918
import {EFlag} from '../../types/api/enums';
2019
import {uiFactory} from '../../uiFactory/uiFactory';
@@ -158,7 +157,7 @@ const CLUSTERS_COLUMNS: Column<PreparedCluster>[] = [
158157
{
159158
name: COLUMNS_NAMES.VERSIONS,
160159
header: COLUMNS_TITLES[COLUMNS_NAMES.VERSIONS],
161-
width: 300,
160+
width: 400,
162161
defaultOrder: DataTable.DESCENDING,
163162
sortAccessor: ({preparedVersions}) => {
164163
const versions = preparedVersions
@@ -181,16 +180,6 @@ const CLUSTERS_COLUMNS: Column<PreparedCluster>[] = [
181180
return EMPTY_CELL;
182181
}
183182

184-
const total = versions.reduce((acc, item) => acc + item.count, 0);
185-
const versionsValues = versions.map((item) => {
186-
return {
187-
value: (item.count / total) * 100,
188-
color: preparedVersions.find(
189-
(versionItem) => versionItem.version === item.version,
190-
)?.color,
191-
};
192-
});
193-
194183
return (
195184
preparedVersions.length > 0 && (
196185
<ExternalLink
@@ -201,19 +190,7 @@ const CLUSTERS_COLUMNS: Column<PreparedCluster>[] = [
201190
{withBasename: true},
202191
)}
203192
>
204-
<React.Fragment>
205-
{preparedVersions.map((item, index) => (
206-
<div
207-
className={b('cluster-version')}
208-
style={{color: item.color}}
209-
key={index}
210-
title={item.version}
211-
>
212-
{item.version}
213-
</div>
214-
))}
215-
{<Progress size="s" value={100} stack={versionsValues} />}
216-
</React.Fragment>
193+
<VersionsBar preparedVersions={preparedVersions} />
217194
</ExternalLink>
218195
)
219196
);

src/containers/Versions/GroupedNodesTree/GroupedNodesTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import React from 'react';
33
import {TreeView} from 'ydb-ui-components';
44

55
import type {NodesPreparedEntity} from '../../../store/reducers/nodes/types';
6-
import type {VersionValue} from '../../../types/versions';
76
import {cn} from '../../../utils/cn';
7+
import type {VersionValue} from '../../../utils/versions/types';
88
import {NodesTable} from '../NodesTable/NodesTable';
99
import {NodesTreeTitle} from '../NodesTreeTitle/NodesTreeTitle';
1010
import type {GroupedNodesItem} from '../types';

src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {ClipboardButton, Progress} from '@gravity-ui/uikit';
22

3-
import type {VersionValue} from '../../../types/versions';
43
import {cn} from '../../../utils/cn';
54
import type {PreparedNodeSystemState} from '../../../utils/nodes';
5+
import type {VersionValue} from '../../../utils/versions/types';
66
import type {GroupedNodesItem} from '../types';
77

88
import './NodesTreeTitle.scss';

src/containers/Versions/Versions.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
66
import {nodesApi} from '../../store/reducers/nodes/nodes';
77
import type {NodesPreparedEntity} from '../../store/reducers/nodes/types';
88
import type {TClusterInfo} from '../../types/api/cluster';
9-
import type {VersionToColorMap, VersionValue} from '../../types/versions';
109
import {cn} from '../../utils/cn';
1110
import {useAutoRefreshInterval} from '../../utils/hooks';
11+
import type {VersionValue, VersionsDataMap} from '../../utils/versions/types';
1212
import {VersionsBar} from '../Cluster/VersionsBar/VersionsBar';
1313

1414
import {GroupedNodesTree} from './GroupedNodesTree/GroupedNodesTree';
1515
import {getGroupedStorageNodes, getGroupedTenantNodes, getOtherNodes} from './groupNodes';
1616
import i18n from './i18n';
1717
import {GroupByValue} from './types';
18-
import {useGetVersionValues, useVersionToColorMap} from './utils';
18+
import {useGetVersionValues, useVersionsDataMap} from './utils';
1919

2020
import './Versions.scss';
2121

@@ -32,16 +32,16 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) {
3232
{tablets: false, fieldsRequired: ['SystemState', 'SubDomainKey']},
3333
{pollingInterval: autoRefreshInterval},
3434
);
35-
const versionToColor = useVersionToColorMap(cluster);
35+
const versionsDataMap = useVersionsDataMap(cluster);
3636

37-
const versionsValues = useGetVersionValues({cluster, versionToColor, clusterLoading: loading});
37+
const versionsValues = useGetVersionValues({cluster, versionsDataMap, clusterLoading: loading});
3838

3939
return (
4040
<LoaderWrapper loading={loading || isNodesLoading}>
4141
<Versions
4242
versionsValues={versionsValues}
4343
nodes={currentData?.Nodes}
44-
versionToColor={versionToColor}
44+
versionsDataMap={versionsDataMap}
4545
/>
4646
</LoaderWrapper>
4747
);
@@ -50,10 +50,10 @@ export function VersionsContainer({cluster, loading}: VersionsContainerProps) {
5050
interface VersionsProps {
5151
nodes?: NodesPreparedEntity[];
5252
versionsValues: VersionValue[];
53-
versionToColor?: VersionToColorMap;
53+
versionsDataMap?: VersionsDataMap;
5454
}
5555

56-
function Versions({versionsValues, nodes, versionToColor}: VersionsProps) {
56+
function Versions({versionsValues, nodes, versionsDataMap}: VersionsProps) {
5757
const [groupByValue, setGroupByValue] = React.useState<GroupByValue>(GroupByValue.VERSION);
5858
const [expanded, setExpanded] = React.useState(false);
5959

@@ -91,9 +91,9 @@ function Versions({versionsValues, nodes, versionToColor}: VersionsProps) {
9191
);
9292
};
9393

94-
const tenantNodes = getGroupedTenantNodes(nodes, versionToColor, groupByValue);
95-
const storageNodes = getGroupedStorageNodes(nodes, versionToColor);
96-
const otherNodes = getOtherNodes(nodes, versionToColor);
94+
const tenantNodes = getGroupedTenantNodes(nodes, versionsDataMap, groupByValue);
95+
const storageNodes = getGroupedStorageNodes(nodes, versionsDataMap);
96+
const otherNodes = getOtherNodes(nodes, versionsDataMap);
9797
const storageNodesContent = storageNodes?.length ? (
9898
<React.Fragment>
9999
<h4>{i18n('title_storage')}</h4>

0 commit comments

Comments
 (0)