Skip to content

Commit 8a311b7

Browse files
authored
Merge branch 'main' into astandrik.display-a-list-of-operations-1414
2 parents d360823 + 9daa5a3 commit 8a311b7

File tree

14 files changed

+180
-168
lines changed

14 files changed

+180
-168
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.hover-popup {
2+
padding: var(--g-spacing-3);
3+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
3+
import {Popup} from '@gravity-ui/uikit';
4+
import debounce from 'lodash/debounce';
5+
6+
import {cn} from '../../utils/cn';
7+
8+
import './HoverPopup.scss';
9+
10+
const b = cn('hover-popup');
11+
12+
const DEBOUNCE_TIMEOUT = 100;
13+
14+
interface HoverPopupProps {
15+
children: React.ReactNode;
16+
popupContent: React.ReactNode;
17+
showPopup?: boolean;
18+
offset?: [number, number];
19+
anchorRef?: React.RefObject<HTMLElement>;
20+
onShowPopup?: VoidFunction;
21+
onHidePopup?: VoidFunction;
22+
}
23+
24+
export const HoverPopup = ({
25+
children,
26+
popupContent,
27+
showPopup,
28+
offset,
29+
anchorRef,
30+
onShowPopup,
31+
onHidePopup,
32+
}: HoverPopupProps) => {
33+
const [isPopupVisible, setIsPopupVisible] = React.useState(false);
34+
const anchor = React.useRef<HTMLDivElement>(null);
35+
36+
const debouncedHandleShowPopup = React.useMemo(
37+
() =>
38+
debounce(() => {
39+
setIsPopupVisible(true);
40+
onShowPopup?.();
41+
}, DEBOUNCE_TIMEOUT),
42+
[onShowPopup],
43+
);
44+
45+
const hidePopup = React.useCallback(() => {
46+
setIsPopupVisible(false);
47+
onHidePopup?.();
48+
}, [onHidePopup]);
49+
50+
const debouncedHandleHidePopup = React.useMemo(
51+
() => debounce(hidePopup, DEBOUNCE_TIMEOUT),
52+
[hidePopup],
53+
);
54+
55+
const onMouseEnter = debouncedHandleShowPopup;
56+
57+
const onMouseLeave = () => {
58+
debouncedHandleShowPopup.cancel();
59+
debouncedHandleHidePopup();
60+
};
61+
62+
const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
63+
const [isFocused, setIsFocused] = React.useState(false);
64+
65+
const onPopupMouseEnter = React.useCallback(() => {
66+
setIsPopupContentHovered(true);
67+
}, []);
68+
69+
const onPopupMouseLeave = React.useCallback(() => {
70+
setIsPopupContentHovered(false);
71+
}, []);
72+
73+
const onPopupContextMenu = React.useCallback(() => {
74+
setIsFocused(true);
75+
}, []);
76+
77+
const onPopupBlur = React.useCallback(() => {
78+
setIsFocused(false);
79+
}, []);
80+
81+
const onPopupEscapeKeyDown = React.useCallback(() => {
82+
setIsFocused(false);
83+
setIsPopupContentHovered(false);
84+
hidePopup();
85+
}, [hidePopup]);
86+
87+
const open = isPopupVisible || showPopup || isPopupContentHovered || isFocused;
88+
89+
return (
90+
<React.Fragment>
91+
<div ref={anchor} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
92+
{children}
93+
</div>
94+
<Popup
95+
contentClassName={b()}
96+
anchorRef={anchorRef || anchor}
97+
open={open}
98+
onMouseEnter={onPopupMouseEnter}
99+
onMouseLeave={onPopupMouseLeave}
100+
onEscapeKeyDown={onPopupEscapeKeyDown}
101+
onBlur={onPopupBlur}
102+
placement={['top', 'bottom']}
103+
hasArrow
104+
// bigger offset for easier switching to neighbour nodes
105+
// matches the default offset for popup with arrow out of a sense of beauty
106+
offset={offset || [0, 12]}
107+
>
108+
<div onContextMenu={onPopupContextMenu}>{popupContent}</div>
109+
</Popup>
110+
</React.Fragment>
111+
);
112+
};

src/components/PDiskPopup/PDiskPopup.scss

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/components/PDiskPopup/PDiskPopup.tsx

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import React from 'react';
22

3-
import type {PopupProps} from '@gravity-ui/uikit';
4-
import {Popup} from '@gravity-ui/uikit';
5-
63
import {selectNodeHostsMap} from '../../store/reducers/nodesList';
74
import {EFlag} from '../../types/api/enums';
85
import {valueIsDefined} from '../../utils';
9-
import {cn} from '../../utils/cn';
106
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
117
import {getPDiskId} from '../../utils/disks/helpers';
128
import type {PreparedPDisk} from '../../utils/disks/types';
139
import {useTypedSelector} from '../../utils/hooks';
1410
import {bytesToGB} from '../../utils/utils';
15-
import type {InfoViewerItem} from '../InfoViewer';
1611
import {InfoViewer} from '../InfoViewer';
17-
18-
import './PDiskPopup.scss';
19-
20-
const b = cn('pdisk-storage-popup');
12+
import type {InfoViewerItem} from '../InfoViewer';
2113

2214
const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow];
2315

@@ -61,37 +53,14 @@ export const preparePDiskData = (data: PreparedPDisk, nodeHost?: string) => {
6153
return pdiskData;
6254
};
6355

64-
interface PDiskPopupProps extends PopupProps {
56+
interface PDiskPopupProps {
6557
data: PreparedPDisk;
6658
}
6759

68-
export const PDiskPopup = ({data, ...props}: PDiskPopupProps) => {
60+
export const PDiskPopup = ({data}: PDiskPopupProps) => {
6961
const nodeHostsMap = useTypedSelector(selectNodeHostsMap);
7062
const nodeHost = valueIsDefined(data.NodeId) ? nodeHostsMap?.get(data.NodeId) : undefined;
7163
const info = React.useMemo(() => preparePDiskData(data, nodeHost), [data, nodeHost]);
7264

73-
const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
74-
const onMouseLeave = React.useCallback(() => {
75-
setIsPopupContentHovered(false);
76-
}, []);
77-
const onMouseEnter = React.useCallback(() => {
78-
setIsPopupContentHovered(true);
79-
}, []);
80-
81-
return (
82-
<Popup
83-
contentClassName={b()}
84-
placement={['top', 'bottom']}
85-
hasArrow
86-
// bigger offset for easier switching to neighbour nodes
87-
// matches the default offset for popup with arrow out of a sense of beauty
88-
offset={[0, 12]}
89-
onMouseLeave={onMouseLeave}
90-
onMouseEnter={onMouseEnter}
91-
{...props}
92-
open={isPopupContentHovered || props.open}
93-
>
94-
<InfoViewer title="PDisk" info={info} size="s" />
95-
</Popup>
96-
);
65+
return <InfoViewer title="PDisk" info={info} size="s" />;
9766
};

src/components/VDisk/VDisk.tsx

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import React from 'react';
2-
3-
import {debounce} from 'lodash';
4-
51
import {cn} from '../../utils/cn';
62
import type {PreparedVDisk} from '../../utils/disks/types';
73
import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar';
4+
import {HoverPopup} from '../HoverPopup/HoverPopup';
85
import {InternalLink} from '../InternalLink';
96
import {VDiskPopup} from '../VDiskPopup/VDiskPopup';
107

@@ -14,8 +11,6 @@ import './VDisk.scss';
1411

1512
const b = cn('ydb-vdisk-component');
1613

17-
const DEBOUNCE_TIMEOUT = 100;
18-
1914
export interface VDiskProps {
2015
data?: PreparedVDisk;
2116
compact?: boolean;
@@ -35,33 +30,16 @@ export const VDisk = ({
3530
onHidePopup,
3631
progressBarClassName,
3732
}: VDiskProps) => {
38-
const [isPopupVisible, setIsPopupVisible] = React.useState(false);
39-
40-
const anchor = React.useRef(null);
41-
42-
const debouncedHandleShowPopup = debounce(() => {
43-
setIsPopupVisible(true);
44-
onShowPopup?.();
45-
}, DEBOUNCE_TIMEOUT);
46-
47-
const debouncedHandleHidePopup = debounce(() => {
48-
setIsPopupVisible(false);
49-
onHidePopup?.();
50-
}, DEBOUNCE_TIMEOUT);
51-
5233
const vDiskPath = getVDiskLink(data);
5334

5435
return (
55-
<React.Fragment>
56-
<div
57-
className={b()}
58-
ref={anchor}
59-
onMouseEnter={debouncedHandleShowPopup}
60-
onMouseLeave={() => {
61-
debouncedHandleShowPopup.cancel();
62-
debouncedHandleHidePopup();
63-
}}
64-
>
36+
<HoverPopup
37+
showPopup={showPopup}
38+
onShowPopup={onShowPopup}
39+
onHidePopup={onHidePopup}
40+
popupContent={<VDiskPopup data={data} />}
41+
>
42+
<div className={b()}>
6543
<InternalLink to={vDiskPath} className={b('content')}>
6644
<DiskStateProgressBar
6745
diskAllocatedPercent={data.AllocatedPercent}
@@ -72,7 +50,6 @@ export const VDisk = ({
7250
/>
7351
</InternalLink>
7452
</div>
75-
<VDiskPopup data={data} anchorRef={anchor} open={isPopupVisible || showPopup} />
76-
</React.Fragment>
53+
</HoverPopup>
7754
);
7855
};

src/components/VDiskPopup/VDiskPopup.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
.vdisk-storage-popup {
2-
padding: 12px;
3-
42
.info-viewer + .info-viewer {
53
margin-top: 8px;
64
padding-top: 8px;

src/components/VDiskPopup/VDiskPopup.tsx

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

3-
import type {PopupProps} from '@gravity-ui/uikit';
4-
import {Label, Popup} from '@gravity-ui/uikit';
3+
import {Label} from '@gravity-ui/uikit';
54

65
import {selectNodeHostsMap} from '../../store/reducers/nodesList';
76
import {EFlag} from '../../types/api/enums';
@@ -130,21 +129,13 @@ const prepareVDiskData = (data: PreparedVDisk) => {
130129
return vdiskData;
131130
};
132131

133-
interface VDiskPopupProps extends PopupProps {
132+
interface VDiskPopupProps {
134133
data: PreparedVDisk | UnavailableDonor;
135134
}
136135

137-
export const VDiskPopup = ({data, ...props}: VDiskPopupProps) => {
136+
export const VDiskPopup = ({data}: VDiskPopupProps) => {
138137
const isFullData = isFullVDiskData(data);
139138

140-
const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false);
141-
const onMouseLeave = React.useCallback(() => {
142-
setIsPopupContentHovered(false);
143-
}, []);
144-
const onMouseEnter = React.useCallback(() => {
145-
setIsPopupContentHovered(true);
146-
}, []);
147-
148139
const vdiskInfo = React.useMemo(
149140
() => (isFullData ? prepareVDiskData(data) : prepareUnavailableVDiskData(data)),
150141
[data, isFullData],
@@ -182,22 +173,11 @@ export const VDiskPopup = ({data, ...props}: VDiskPopupProps) => {
182173
}
183174

184175
return (
185-
<Popup
186-
contentClassName={b()}
187-
placement={['top', 'bottom']}
188-
hasArrow
189-
// bigger offset for easier switching to neighbour nodes
190-
// matches the default offset for popup with arrow out of a sense of beauty
191-
offset={[0, 12]}
192-
onMouseEnter={onMouseEnter}
193-
onMouseLeave={onMouseLeave}
194-
{...props}
195-
open={isPopupContentHovered || props.open}
196-
>
176+
<div className={b()}>
197177
{data.DonorMode && <Label className={b('donor-label')}>Donor</Label>}
198178
<InfoViewer title="VDisk" info={vdiskInfo} size="s" />
199179
{pdiskInfo && <InfoViewer title="PDisk" info={pdiskInfo} size="s" />}
200180
{donorsInfo.length > 0 && <InfoViewer title="Donors" info={donorsInfo} size="s" />}
201-
</Popup>
181+
</div>
202182
);
203183
};

0 commit comments

Comments
 (0)