Skip to content

Commit fa0abe3

Browse files
feat(storage): update vdisk donor/replica visuals and tooltips (#3110)
1 parent 24cae9c commit fa0abe3

File tree

30 files changed

+865
-271
lines changed

30 files changed

+865
-271
lines changed

src/components/DiskStateProgressBar/DiskStateProgressBar.scss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
--progress-bar-full-height: var(--g-text-body-3-line-height);
1111
--progress-bar-compact-height: 12px;
1212

13+
--stripe-width: 4px;
14+
--stripe-step: 8px;
15+
1316
position: relative;
1417
z-index: 0;
1518

@@ -45,6 +48,20 @@
4548
background-color: unset;
4649
}
4750

51+
&_striped {
52+
overflow: hidden;
53+
// Inset shadow = border overlay without shrinking the striped fill
54+
border: none;
55+
background-image: repeating-linear-gradient(
56+
135deg,
57+
transparent 0,
58+
transparent var(--stripe-width),
59+
var(--entity-state-fill-color) var(--stripe-width),
60+
var(--entity-state-fill-color) var(--stripe-step)
61+
);
62+
box-shadow: 0 0 0 $border-width var(--entity-state-shadow-color) inset;
63+
}
64+
4865
&__fill-bar {
4966
position: absolute;
5067
top: 0;
@@ -76,10 +93,21 @@
7693
position: relative;
7794
z-index: 2;
7895

96+
margin-right: var(--g-spacing-1);
97+
7998
font-size: var(--g-text-body-1-font-size);
8099
// bar height minus borders
81100
line-height: calc(var(--progress-bar-full-height) - #{$border-width * 2});
82101

83102
color: inherit;
84103
}
104+
105+
&__icon {
106+
position: relative;
107+
z-index: 2;
108+
109+
margin-left: var(--g-spacing-1);
110+
111+
color: var(--entity-state-border-color);
112+
}
85113
}

src/components/DiskStateProgressBar/DiskStateProgressBar.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import React from 'react';
22

3+
import {Flex, Icon} from '@gravity-ui/uikit';
4+
35
import {SETTING_KEYS} from '../../store/reducers/settings/constants';
46
import {cn} from '../../utils/cn';
5-
import {getSeverityColor} from '../../utils/disks/helpers';
7+
import {DONOR_COLOR} from '../../utils/disks/constants';
8+
import {getSeverityColor, getVDiskStatusIcon} from '../../utils/disks/helpers';
69
import {useSetting} from '../../utils/hooks';
710

811
import './DiskStateProgressBar.scss';
@@ -16,8 +19,11 @@ interface DiskStateProgressBarProps {
1619
faded?: boolean;
1720
inactive?: boolean;
1821
empty?: boolean;
22+
striped?: boolean;
1923
content?: React.ReactNode;
2024
className?: string;
25+
isDonor?: boolean;
26+
withIcon?: boolean;
2127
}
2228

2329
export function DiskStateProgressBar({
@@ -28,15 +34,29 @@ export function DiskStateProgressBar({
2834
inactive,
2935
empty,
3036
content,
37+
striped,
3138
className,
39+
isDonor,
40+
withIcon,
3241
}: DiskStateProgressBarProps) {
3342
const [inverted] = useSetting<boolean | undefined>(SETTING_KEYS.INVERTED_DISKS);
3443

35-
const mods: Record<string, boolean | undefined> = {inverted, compact, faded, empty, inactive};
44+
const mods: Record<string, boolean | undefined> = {
45+
inverted,
46+
compact,
47+
faded,
48+
empty,
49+
inactive,
50+
striped,
51+
};
3652

37-
const color = severity !== undefined && getSeverityColor(severity);
38-
if (color) {
39-
mods[color.toLocaleLowerCase()] = true;
53+
if (isDonor) {
54+
mods[DONOR_COLOR.toLocaleLowerCase()] = true;
55+
} else {
56+
const color = severity !== undefined && getSeverityColor(severity);
57+
if (color) {
58+
mods[color.toLocaleLowerCase()] = true;
59+
}
4060
}
4161

4262
const renderAllocatedPercent = () => {
@@ -69,17 +89,33 @@ export function DiskStateProgressBar({
6989
return null;
7090
};
7191

92+
let iconElement: React.ReactNode = null;
93+
94+
if (withIcon) {
95+
const icon = getVDiskStatusIcon(severity, isDonor);
96+
97+
if (icon) {
98+
iconElement = <Icon className={b('icon')} data={icon} size={12} />;
99+
}
100+
}
101+
102+
const hasIcon = Boolean(iconElement);
103+
const justifyContent = hasIcon ? 'space-between' : 'flex-end';
104+
72105
return (
73-
<div
106+
<Flex
107+
alignItems="center"
108+
justifyContent={justifyContent}
74109
className={b(mods, className)}
75110
role="meter"
76111
aria-label="Disk allocated space"
77112
aria-valuemin={0}
78113
aria-valuemax={100}
79114
aria-valuenow={diskAllocatedPercent}
80115
>
116+
{iconElement}
81117
{renderAllocatedPercent()}
82118
{renderContent()}
83-
</div>
119+
</Flex>
84120
);
85121
}
Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,129 @@
11
import React from 'react';
22

3-
import {Flex} from '@gravity-ui/uikit';
3+
import {Flex, Label} from '@gravity-ui/uikit';
4+
import {isNil} from 'lodash';
45

56
import {selectNodesMap} from '../../store/reducers/nodesList';
67
import {EFlag} from '../../types/api/enums';
7-
import {valueIsDefined} from '../../utils';
88
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
99
import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI';
10+
import {getStateSeverity} from '../../utils/disks/calculatePDiskSeverity';
11+
import {NUMERIC_SEVERITY_TO_LABEL_VIEW} from '../../utils/disks/constants';
1012
import type {PreparedPDisk} from '../../utils/disks/types';
1113
import {useTypedSelector} from '../../utils/hooks';
1214
import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
1315
import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges';
1416
import {bytesToGB, isNumeric} from '../../utils/utils';
15-
import {InfoViewer} from '../InfoViewer';
16-
import type {InfoViewerItem} from '../InfoViewer';
1717
import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon';
1818
import {pDiskInfoKeyset} from '../PDiskInfo/i18n';
1919
import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink';
20+
import {StatusIcon} from '../StatusIcon/StatusIcon';
21+
import type {
22+
YDBDefinitionListHeaderLabel,
23+
YDBDefinitionListItem,
24+
} from '../YDBDefinitionList/YDBDefinitionList';
25+
import {YDBDefinitionList} from '../YDBDefinitionList/YDBDefinitionList';
26+
27+
import {pDiskPopupKeyset} from './i18n';
2028

2129
const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow];
2230

23-
export const preparePDiskData = (
24-
data: PreparedPDisk,
25-
nodeData?: {Host?: string; DC?: string},
26-
withDeveloperUILink?: boolean,
27-
) => {
28-
const {
29-
AvailableSize,
30-
TotalSize,
31-
State,
32-
PDiskId,
33-
NodeId,
34-
StringifiedId,
35-
Path,
36-
Realtime,
37-
Type,
38-
Device,
39-
} = data;
40-
41-
const pdiskData: InfoViewerItem[] = [
42-
{
43-
label: 'PDisk',
44-
value: StringifiedId ?? EMPTY_DATA_PLACEHOLDER,
45-
},
46-
{label: 'State', value: State || 'not available'},
47-
{label: 'Type', value: Type || 'unknown'},
31+
export const preparePDiskData = (data: PreparedPDisk, nodeData?: {Host?: string; DC?: string}) => {
32+
const {AvailableSize, TotalSize, NodeId, Path, Realtime, Type, Device} = data;
33+
34+
const pdiskData: YDBDefinitionListItem[] = [
35+
{name: pDiskPopupKeyset('label_type'), content: Type || pDiskPopupKeyset('value_unknown')},
4836
];
4937

5038
if (NodeId) {
51-
pdiskData.push({label: 'Node Id', value: NodeId});
39+
pdiskData.push({name: pDiskPopupKeyset('label_node-id'), content: NodeId});
5240
}
5341

5442
if (nodeData?.Host) {
55-
pdiskData.push({label: 'Host', value: nodeData.Host});
43+
pdiskData.push({name: pDiskPopupKeyset('label_host'), content: nodeData.Host});
5644
}
45+
5746
if (nodeData?.DC) {
58-
pdiskData.push({label: 'DC', value: nodeData.DC});
47+
pdiskData.push({name: pDiskPopupKeyset('label_dc'), content: <Label>{nodeData.DC}</Label>});
5948
}
6049

6150
if (Path) {
62-
pdiskData.push({label: 'Path', value: Path});
51+
pdiskData.push({name: pDiskPopupKeyset('label_path'), content: Path});
6352
}
6453

65-
if (isNumeric(TotalSize)) {
54+
if (isNumeric(TotalSize) && isNumeric(AvailableSize)) {
6655
pdiskData.push({
67-
label: 'Available',
68-
value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`,
56+
name: pDiskPopupKeyset('label_available'),
57+
content: `${bytesToGB(AvailableSize)} ${pDiskPopupKeyset('value_of')} ${bytesToGB(TotalSize)}`,
6958
});
7059
}
7160

7261
if (Realtime && errorColors.includes(Realtime)) {
73-
pdiskData.push({label: 'Realtime', value: Realtime});
62+
pdiskData.push({
63+
name: pDiskPopupKeyset('label_realtime'),
64+
content: <StatusIcon mode="icons" status={Realtime} />,
65+
});
7466
}
7567

7668
if (Device && errorColors.includes(Device)) {
77-
pdiskData.push({label: 'Device', value: Device});
69+
pdiskData.push({
70+
name: pDiskPopupKeyset('label_device'),
71+
content: <StatusIcon mode="icons" status={Device} />,
72+
});
7873
}
7974

80-
if (withDeveloperUILink && valueIsDefined(NodeId) && valueIsDefined(PDiskId)) {
81-
const pDiskInternalViewerPath = createPDiskDeveloperUILink({
82-
nodeId: NodeId,
83-
pDiskId: PDiskId,
75+
return pdiskData;
76+
};
77+
78+
export const preparePDiskHeaderLabels = (data: PreparedPDisk): YDBDefinitionListHeaderLabel[] => {
79+
const labels: YDBDefinitionListHeaderLabel[] = [];
80+
const {State} = data;
81+
82+
if (!State) {
83+
labels.push({
84+
id: 'state',
85+
value: pDiskPopupKeyset('context_not-available'),
8486
});
8587

86-
pdiskData.push({
87-
label: 'Links',
88-
value: (
89-
<Flex gap={2} wrap="wrap">
90-
<PDiskPageLink pDiskId={PDiskId} nodeId={NodeId} />
91-
<LinkWithIcon
92-
title={pDiskInfoKeyset('developer-ui')}
93-
url={pDiskInternalViewerPath}
94-
/>
95-
</Flex>
96-
),
88+
return labels;
89+
}
90+
91+
if (State) {
92+
const severity = getStateSeverity(State);
93+
const {theme, icon} = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity];
94+
95+
labels.push({
96+
id: 'state',
97+
value: State,
98+
theme: theme,
99+
icon: icon,
97100
});
98101
}
99102

100-
return pdiskData;
103+
return labels;
104+
};
105+
106+
export const buildPDiskFooter = (
107+
data: PreparedPDisk,
108+
withDeveloperUILink?: boolean,
109+
): React.ReactNode | null => {
110+
const {NodeId, PDiskId} = data;
111+
112+
if (!withDeveloperUILink || isNil(NodeId) || isNil(PDiskId)) {
113+
return null;
114+
}
115+
116+
const pDiskInternalViewerPath = createPDiskDeveloperUILink({
117+
nodeId: NodeId,
118+
pDiskId: PDiskId,
119+
});
120+
121+
return (
122+
<Flex gap={2} wrap="wrap">
123+
<PDiskPageLink pDiskId={PDiskId} nodeId={NodeId} />
124+
<LinkWithIcon title={pDiskInfoKeyset('developer-ui')} url={pDiskInternalViewerPath} />
125+
</Flex>
126+
);
101127
};
102128

103129
interface PDiskPopupProps {
@@ -108,11 +134,31 @@ export const PDiskPopup = ({data}: PDiskPopupProps) => {
108134
const database = useDatabaseFromQuery();
109135
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();
110136
const nodesMap = useTypedSelector((state) => selectNodesMap(state, database));
111-
const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined;
112-
const info = React.useMemo(
113-
() => preparePDiskData(data, nodeData, isUserAllowedToMakeChanges),
114-
[data, nodeData, isUserAllowedToMakeChanges],
137+
const nodeData = isNil(data.NodeId) ? undefined : nodesMap?.get(data.NodeId);
138+
139+
const info = React.useMemo(() => preparePDiskData(data, nodeData), [data, nodeData]);
140+
141+
const headerLabels = React.useMemo<YDBDefinitionListHeaderLabel[]>(
142+
() => preparePDiskHeaderLabels(data),
143+
[data],
144+
);
145+
146+
const footer = React.useMemo(
147+
() => buildPDiskFooter(data, isUserAllowedToMakeChanges),
148+
[data, isUserAllowedToMakeChanges],
115149
);
116150

117-
return <InfoViewer title="PDisk" info={info} size="s" />;
151+
const pdiskId = data.StringifiedId;
152+
153+
return (
154+
<YDBDefinitionList
155+
compact
156+
title="PDisk"
157+
titleSuffix={pdiskId ?? EMPTY_DATA_PLACEHOLDER}
158+
items={info}
159+
headerLabels={headerLabels}
160+
footer={footer}
161+
nameMaxWidth={100}
162+
/>
163+
);
118164
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"context_not-available": "Not available",
3+
"label_type": "Type",
4+
"label_node-id": "Node Id",
5+
"label_host": "Host",
6+
"label_dc": "DC",
7+
"label_path": "Path",
8+
"label_available": "Available",
9+
"label_realtime": "Realtime",
10+
"label_device": "Device",
11+
"value_unknown": "unknown",
12+
"value_of": "of"
13+
}
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-pDisk-popup';
6+
7+
export const pDiskPopupKeyset = registerKeysets(COMPONENT, {en});

0 commit comments

Comments
 (0)