Skip to content

Commit 85933b2

Browse files
committed
feat: side panel aka refrigerator for query text in top queries
1 parent 5e75a0b commit 85933b2

14 files changed

+813
-131
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
3+
import {Link} from '@gravity-ui/icons';
4+
import type {ButtonProps, CopyToClipboardStatus} from '@gravity-ui/uikit';
5+
import {ActionTooltip, Button, CopyToClipboard, Icon} from '@gravity-ui/uikit';
6+
7+
import {cn} from '../../../../../utils/cn';
8+
import i18n from '../i18n';
9+
10+
import './QueryDetails.scss';
11+
12+
const b = cn('kv-query-details');
13+
14+
interface LinkButtonComponentProps extends ButtonProps {
15+
size?: ButtonProps['size'];
16+
hasTooltip?: boolean;
17+
status: CopyToClipboardStatus;
18+
closeDelay?: number;
19+
}
20+
21+
const DEFAULT_TIMEOUT = 1200;
22+
const TOOLTIP_ANIMATION = 200;
23+
24+
const LinkButtonComponent = (props: LinkButtonComponentProps) => {
25+
const {size = 'm', hasTooltip = true, status, closeDelay, ...rest} = props;
26+
27+
return (
28+
<ActionTooltip
29+
title={
30+
status === 'success'
31+
? i18n('query-details.link-copied')
32+
: i18n('query-details.copy-link')
33+
}
34+
disabled={!hasTooltip}
35+
closeDelay={closeDelay}
36+
>
37+
<Button view="flat-secondary" size={size} {...rest}>
38+
<Button.Icon className={b('icon')}>
39+
<Icon data={Link} size={16} />
40+
</Button.Icon>
41+
</Button>
42+
</ActionTooltip>
43+
);
44+
};
45+
46+
export interface CopyLinkButtonProps extends ButtonProps {
47+
text: string;
48+
}
49+
50+
export function CopyLinkButton(props: CopyLinkButtonProps) {
51+
const {text, ...buttonProps} = props;
52+
53+
const timerIdRef = React.useRef<number>();
54+
const [tooltipCloseDelay, setTooltipCloseDelay] = React.useState<number | undefined>(undefined);
55+
const [tooltipDisabled, setTooltipDisabled] = React.useState(false);
56+
const timeout = DEFAULT_TIMEOUT;
57+
58+
React.useEffect(() => window.clearTimeout(timerIdRef.current), []);
59+
60+
const handleCopy = React.useCallback(() => {
61+
setTooltipDisabled(false);
62+
setTooltipCloseDelay(timeout);
63+
64+
window.clearTimeout(timerIdRef.current);
65+
66+
timerIdRef.current = window.setTimeout(() => {
67+
setTooltipDisabled(true);
68+
}, timeout - TOOLTIP_ANIMATION);
69+
}, [timeout]);
70+
71+
return (
72+
<CopyToClipboard text={text} timeout={timeout} onCopy={handleCopy}>
73+
{(status) => (
74+
<LinkButtonComponent
75+
{...buttonProps}
76+
closeDelay={tooltipCloseDelay}
77+
hasTooltip={!tooltipDisabled}
78+
status={status}
79+
/>
80+
)}
81+
</CopyToClipboard>
82+
);
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
@import '../../../../../styles/mixins.scss';
2+
3+
.kv-query-details {
4+
display: flex;
5+
flex-direction: column;
6+
7+
height: 100%;
8+
9+
color: var(--g-color-text-primary);
10+
background-color: var(--g-color-base-background-dark);
11+
12+
&__header {
13+
display: flex;
14+
justify-content: space-between;
15+
align-items: center;
16+
17+
padding: var(--g-spacing-5) var(--g-spacing-6) 0 var(--g-spacing-6);
18+
}
19+
20+
&__title {
21+
margin: 0;
22+
23+
font-size: 16px;
24+
font-weight: 500;
25+
}
26+
27+
&__actions {
28+
display: flex;
29+
gap: var(--g-spacing-2);
30+
}
31+
32+
&__content {
33+
overflow: auto;
34+
flex: 1;
35+
36+
padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-6);
37+
}
38+
39+
&__query-header {
40+
display: flex;
41+
justify-content: space-between;
42+
align-items: center;
43+
44+
padding: var(--g-spacing-2) var(--g-spacing-3);
45+
46+
border-bottom: 1px solid var(--g-color-line-generic);
47+
}
48+
49+
&__query-title {
50+
font-size: 14px;
51+
font-weight: 500;
52+
}
53+
54+
&__query-content {
55+
position: relative;
56+
57+
display: flex;
58+
flex: 1;
59+
flex-direction: column;
60+
61+
margin-top: var(--g-spacing-5);
62+
63+
border-radius: 4px;
64+
background-color: var(--code-background-color);
65+
}
66+
67+
&__icon {
68+
// prevent button icon from firing onMouseEnter/onFocus through parent button's handler
69+
pointer-events: none;
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
3+
import {Code, Xmark} from '@gravity-ui/icons';
4+
import {Button, Flex, Icon} from '@gravity-ui/uikit';
5+
6+
import type {InfoViewerItem} from '../../../../../components/InfoViewer';
7+
import {InfoViewer} from '../../../../../components/InfoViewer';
8+
import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
9+
import {cn} from '../../../../../utils/cn';
10+
import i18n from '../i18n';
11+
12+
import {CopyLinkButton} from './CopyLinkButton';
13+
14+
import './QueryDetails.scss';
15+
16+
const b = cn('kv-query-details');
17+
18+
interface QueryDetailsProps {
19+
queryText: string;
20+
infoItems: InfoViewerItem[];
21+
onClose: () => void;
22+
onOpenInEditor: () => void;
23+
getTopQueryUrl?: () => string;
24+
}
25+
26+
export const QueryDetails = ({
27+
queryText,
28+
infoItems,
29+
onClose,
30+
onOpenInEditor,
31+
getTopQueryUrl,
32+
}: QueryDetailsProps) => {
33+
const topQueryUrl = React.useMemo(() => getTopQueryUrl?.(), [getTopQueryUrl]);
34+
35+
return (
36+
<div className={b()}>
37+
<div className={b('header')}>
38+
<div className={b('title')}>Query</div>
39+
<div className={b('actions')}>
40+
{topQueryUrl && <CopyLinkButton text={topQueryUrl} />}
41+
<Button view="flat-secondary" onClick={onClose}>
42+
<Icon data={Xmark} size={16} />
43+
</Button>
44+
</div>
45+
</div>
46+
47+
<Flex direction="column" className={b('content')}>
48+
<InfoViewer info={infoItems} />
49+
50+
<div className={b('query-content')}>
51+
<div className={b('query-header')}>
52+
<div className={b('query-title')}>{i18n('query-details.query.title')}</div>
53+
<Button
54+
view="flat-secondary"
55+
size="m"
56+
onClick={onOpenInEditor}
57+
className={b('editor-button')}
58+
>
59+
<Icon data={Code} size={16} />
60+
{i18n('query-details.open-in-editor')}
61+
</Button>
62+
</div>
63+
<YDBSyntaxHighlighter
64+
language="yql"
65+
text={queryText}
66+
withClipboardButton={{alwaysVisible: true, withLabel: false}}
67+
/>
68+
</div>
69+
</Flex>
70+
</div>
71+
);
72+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
3+
import {Button, Icon, Text} from '@gravity-ui/uikit';
4+
import {useHistory, useLocation} from 'react-router-dom';
5+
6+
import {parseQuery} from '../../../../../routes';
7+
import {changeUserInput, setIsDirty} from '../../../../../store/reducers/query/query';
8+
import {
9+
TENANT_PAGE,
10+
TENANT_PAGES_IDS,
11+
TENANT_QUERY_TABS_ID,
12+
} from '../../../../../store/reducers/tenant/constants';
13+
import type {KeyValueRow} from '../../../../../types/api/query';
14+
import {cn} from '../../../../../utils/cn';
15+
import {useTypedDispatch} from '../../../../../utils/hooks';
16+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
17+
import i18n from '../i18n';
18+
import {createQueryInfoItems} from '../utils';
19+
20+
import {QueryDetails} from './QueryDetails';
21+
22+
import CryCatIcon from '../../../../../assets/icons/cry-cat.svg';
23+
24+
const b = cn('kv-top-queries');
25+
26+
interface QueryDetailsDrawerContentProps {
27+
row: KeyValueRow | null;
28+
onClose: () => void;
29+
getTopQueryUrl?: () => string;
30+
}
31+
32+
export const QueryDetailsDrawerContent = ({
33+
row,
34+
onClose,
35+
getTopQueryUrl,
36+
}: QueryDetailsDrawerContentProps) => {
37+
const dispatch = useTypedDispatch();
38+
const location = useLocation();
39+
const history = useHistory();
40+
41+
const handleOpenInEditor = React.useCallback(() => {
42+
if (row) {
43+
const input = row.QueryText as string;
44+
dispatch(changeUserInput({input}));
45+
dispatch(setIsDirty(false));
46+
47+
const queryParams = parseQuery(location);
48+
49+
const queryPath = getTenantPath({
50+
...queryParams,
51+
[TENANT_PAGE]: TENANT_PAGES_IDS.query,
52+
[TenantTabsGroups.queryTab]: TENANT_QUERY_TABS_ID.newQuery,
53+
});
54+
55+
history.push(queryPath);
56+
}
57+
}, [dispatch, history, location, row]);
58+
59+
if (row) {
60+
return (
61+
<QueryDetails
62+
queryText={row.QueryText as string}
63+
infoItems={createQueryInfoItems(row)}
64+
onClose={onClose}
65+
onOpenInEditor={handleOpenInEditor}
66+
getTopQueryUrl={getTopQueryUrl}
67+
/>
68+
);
69+
}
70+
71+
return (
72+
<div className={b('not-found-container')}>
73+
<Icon data={CryCatIcon} size={100} />
74+
<Text variant="subheader-2" className={b('not-found-title')}>
75+
{i18n('query-details.not-found.title')}
76+
</Text>
77+
<Text variant="body-1" color="complementary" className={b('not-found-description')}>
78+
{i18n('query-details.not-found.description')}
79+
</Text>
80+
<Button size="m" view="normal" className={b('not-found-close')} onClick={onClose}>
81+
{i18n('query-details.close')}
82+
</Button>
83+
</div>
84+
);
85+
};

0 commit comments

Comments
 (0)