Skip to content

Commit 3a3e8d2

Browse files
committed
feat: Drawer
1 parent 986e6ab commit 3a3e8d2

File tree

14 files changed

+718
-134
lines changed

14 files changed

+718
-134
lines changed

src/components/Drawer/Drawer.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,16 @@ const DrawerPaneContentWrapper = ({
115115
);
116116
};
117117

118+
export enum DrawerControlType {
119+
CLOSE = 'close',
120+
COPY_LINK = 'copyLink',
121+
CUSTOM = 'custom',
122+
}
123+
118124
type DrawerControl =
119-
| {type: 'close'}
120-
| {type: 'copyLink'; link: string}
121-
| {type: 'custom'; node: React.ReactNode; key: string};
125+
| {type: DrawerControlType.CLOSE}
126+
| {type: DrawerControlType.COPY_LINK; link: string}
127+
| {type: DrawerControlType.CUSTOM; node: React.ReactNode; key: string};
122128

123129
interface DrawerPaneProps {
124130
children: React.ReactNode;
Lines changed: 71 additions & 0 deletions
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+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {Code} from '@gravity-ui/icons';
2+
import {Button, Flex, Icon} from '@gravity-ui/uikit';
3+
4+
import type {InfoViewerItem} from '../../../../../components/InfoViewer';
5+
import {InfoViewer} from '../../../../../components/InfoViewer';
6+
import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
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 QueryDetailsProps {
15+
queryText: string;
16+
infoItems: InfoViewerItem[];
17+
onOpenInEditor: () => void;
18+
}
19+
20+
export const QueryDetails = ({queryText, infoItems, onOpenInEditor}: QueryDetailsProps) => {
21+
return (
22+
<div className={b()}>
23+
<Flex direction="column" className={b('content')}>
24+
<InfoViewer info={infoItems} />
25+
26+
<div className={b('query-content')}>
27+
<div className={b('query-header')}>
28+
<div className={b('query-title')}>{i18n('query-details.query.title')}</div>
29+
<Button
30+
view="flat-secondary"
31+
size="m"
32+
onClick={onOpenInEditor}
33+
className={b('editor-button')}
34+
>
35+
<Icon data={Code} size={16} />
36+
{i18n('query-details.open-in-editor')}
37+
</Button>
38+
</div>
39+
<YDBSyntaxHighlighter
40+
language="yql"
41+
text={queryText}
42+
withClipboardButton={{alwaysVisible: true, withLabel: false}}
43+
/>
44+
</div>
45+
</Flex>
46+
</div>
47+
);
48+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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; // Needed for the "not found" case
29+
}
30+
31+
export const QueryDetailsDrawerContent = ({row, onClose}: QueryDetailsDrawerContentProps) => {
32+
const dispatch = useTypedDispatch();
33+
const location = useLocation();
34+
const history = useHistory();
35+
36+
const handleOpenInEditor = React.useCallback(() => {
37+
if (row) {
38+
const input = row.QueryText as string;
39+
dispatch(changeUserInput({input}));
40+
dispatch(setIsDirty(false));
41+
42+
const queryParams = parseQuery(location);
43+
44+
const queryPath = getTenantPath({
45+
...queryParams,
46+
[TENANT_PAGE]: TENANT_PAGES_IDS.query,
47+
[TenantTabsGroups.queryTab]: TENANT_QUERY_TABS_ID.newQuery,
48+
});
49+
50+
history.push(queryPath);
51+
}
52+
}, [dispatch, history, location, row]);
53+
54+
if (row) {
55+
return (
56+
<QueryDetails
57+
queryText={row.QueryText as string}
58+
infoItems={createQueryInfoItems(row)}
59+
onOpenInEditor={handleOpenInEditor}
60+
/>
61+
);
62+
}
63+
64+
return (
65+
<div className={b('not-found-container')}>
66+
<Icon data={CryCatIcon} size={100} />
67+
<Text variant="subheader-2" className={b('not-found-title')}>
68+
{i18n('query-details.not-found.title')}
69+
</Text>
70+
<Text variant="body-1" color="complementary" className={b('not-found-description')}>
71+
{i18n('query-details.not-found.description')}
72+
</Text>
73+
<Button size="m" view="normal" className={b('not-found-close')} onClick={onClose}>
74+
{i18n('query-details.close')}
75+
</Button>
76+
</div>
77+
);
78+
};

src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import React from 'react';
22

33
import type {Column} from '@gravity-ui/react-data-table';
44
import {TableColumnSetup} from '@gravity-ui/uikit';
5+
import {isEqual} from 'lodash';
56

7+
import {DrawerWrapper} from '../../../../components/Drawer';
8+
import {DrawerControlType} from '../../../../components/Drawer/Drawer';
69
import {ResponseError} from '../../../../components/Errors/ResponseError';
710
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
811
import {Search} from '../../../../components/Search';
@@ -14,6 +17,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'
1417
import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns';
1518
import {parseQueryErrorToString} from '../../../../utils/query';
1619

20+
import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent';
1721
import {getRunningQueriesColumns} from './columns/columns';
1822
import {
1923
DEFAULT_RUNNING_QUERIES_COLUMNS,
@@ -31,18 +35,19 @@ const b = cn('kv-top-queries');
3135
interface RunningQueriesDataProps {
3236
tenantName: string;
3337
renderQueryModeControl: () => React.ReactNode;
34-
onRowClick: (query: string) => void;
3538
handleTextSearchUpdate: (text: string) => void;
3639
}
3740

3841
export const RunningQueriesData = ({
3942
tenantName,
4043
renderQueryModeControl,
41-
onRowClick,
4244
handleTextSearchUpdate,
4345
}: RunningQueriesDataProps) => {
4446
const [autoRefreshInterval] = useAutoRefreshInterval();
4547
const filters = useTypedSelector((state) => state.executeTopQueries);
48+
// Internal state for selected row
49+
// null is reserved for not found state
50+
const [selectedRow, setSelectedRow] = React.useState<KeyValueRow | null | undefined>(undefined);
4651

4752
// Get columns for running queries
4853
const columns: Column<KeyValueRow>[] = React.useMemo(() => {
@@ -70,44 +75,90 @@ export const RunningQueriesData = ({
7075
{pollingInterval: autoRefreshInterval},
7176
);
7277

73-
const handleRowClick = (row: KeyValueRow) => {
74-
return onRowClick(row.QueryText as string);
75-
};
78+
const rows = data?.resultSets?.[0]?.result;
79+
80+
const isDrawerVisible = selectedRow !== undefined;
81+
82+
const handleCloseDetails = React.useCallback(() => {
83+
setSelectedRow(undefined);
84+
}, [setSelectedRow]);
85+
86+
const renderDrawerContent = React.useCallback(() => {
87+
if (!isDrawerVisible) {
88+
return null;
89+
}
90+
return <QueryDetailsDrawerContent row={selectedRow} onClose={handleCloseDetails} />;
91+
}, [isDrawerVisible, selectedRow, handleCloseDetails]);
92+
93+
const onRowClick = React.useCallback(
94+
(
95+
row: KeyValueRow | null,
96+
_index?: number,
97+
event?: React.MouseEvent<HTMLTableRowElement>,
98+
) => {
99+
event?.stopPropagation();
100+
setSelectedRow(row);
101+
},
102+
[setSelectedRow],
103+
);
104+
105+
const inputRef = React.useRef<HTMLInputElement>(null);
106+
107+
React.useEffect(() => {
108+
if (isDrawerVisible) {
109+
inputRef.current?.blur();
110+
}
111+
}, [isDrawerVisible]);
112+
113+
const drawerControls = React.useMemo(() => [{type: DrawerControlType.CLOSE} as const], []);
76114

77115
return (
78-
<TableWithControlsLayout>
79-
<TableWithControlsLayout.Controls>
80-
{renderQueryModeControl()}
81-
<Search
82-
value={filters.text}
83-
onChange={handleTextSearchUpdate}
84-
placeholder={i18n('filter.text.placeholder')}
85-
className={b('search')}
86-
/>
87-
<TableColumnSetup
88-
popupWidth={200}
89-
items={columnsToSelect}
90-
showStatus
91-
onUpdate={setColumns}
92-
sortable={false}
93-
/>
94-
</TableWithControlsLayout.Controls>
95-
96-
{error ? <ResponseError error={parseQueryErrorToString(error)} /> : null}
97-
<TableWithControlsLayout.Table loading={isLoading}>
98-
<ResizeableDataTable
99-
emptyDataMessage={i18n('no-data')}
100-
columnsWidthLSKey={RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY}
101-
columns={columnsToShow}
102-
data={data?.resultSets?.[0].result || []}
103-
loading={isFetching && currentData === undefined}
104-
settings={TOP_QUERIES_TABLE_SETTINGS}
105-
onRowClick={handleRowClick}
106-
rowClassName={() => b('row')}
107-
sortOrder={tableSort}
108-
onSort={handleTableSort}
109-
/>
110-
</TableWithControlsLayout.Table>
111-
</TableWithControlsLayout>
116+
<DrawerWrapper
117+
isDrawerVisible={isDrawerVisible}
118+
onCloseDrawer={handleCloseDetails}
119+
renderDrawerContent={renderDrawerContent}
120+
drawerId="running-query-details"
121+
storageKey="running-queries-drawer-width"
122+
detectClickOutside
123+
isPercentageWidth
124+
title={i18n('query-details.title')}
125+
drawerControls={drawerControls}
126+
>
127+
<TableWithControlsLayout>
128+
<TableWithControlsLayout.Controls>
129+
{renderQueryModeControl()}
130+
<Search
131+
value={filters.text}
132+
onChange={handleTextSearchUpdate}
133+
placeholder={i18n('filter.text.placeholder')}
134+
className={b('search')}
135+
inputRef={inputRef}
136+
/>
137+
<TableColumnSetup
138+
popupWidth={200}
139+
items={columnsToSelect}
140+
showStatus
141+
onUpdate={setColumns}
142+
sortable={false}
143+
/>
144+
</TableWithControlsLayout.Controls>
145+
146+
{error ? <ResponseError error={parseQueryErrorToString(error)} /> : null}
147+
<TableWithControlsLayout.Table loading={isLoading}>
148+
<ResizeableDataTable
149+
emptyDataMessage={i18n('no-data')}
150+
columnsWidthLSKey={RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY}
151+
columns={columnsToShow}
152+
data={rows || []}
153+
loading={isFetching && currentData === undefined}
154+
settings={TOP_QUERIES_TABLE_SETTINGS}
155+
onRowClick={onRowClick}
156+
rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})}
157+
sortOrder={tableSort}
158+
onSort={handleTableSort}
159+
/>
160+
</TableWithControlsLayout.Table>
161+
</TableWithControlsLayout>
162+
</DrawerWrapper>
112163
);
113164
};

0 commit comments

Comments
 (0)