Skip to content

Commit 1b98190

Browse files
committed
feat: side panel aka refrigerator for query text in top queries
1 parent b401559 commit 1b98190

File tree

9 files changed

+286
-32
lines changed

9 files changed

+286
-32
lines changed

src/containers/Tenant/Diagnostics/Diagnostics.scss

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
}
3535
}
3636

37+
&__drawer-container {
38+
position: relative;
39+
40+
height: 100%;
41+
}
42+
3743
&__page-wrapper {
3844
overflow: auto;
3945
flex-grow: 1;

src/containers/Tenant/Diagnostics/Diagnostics.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,10 @@ function Diagnostics(props: DiagnosticsProps) {
194194
</Helmet>
195195
) : null}
196196
{renderTabs()}
197-
<div className={b('page-wrapper')} ref={containerRef}>
198-
{renderTabContent()}
197+
<div className={b('drawer-container')}>
198+
<div className={b('page-wrapper')} ref={containerRef}>
199+
{renderTabContent()}
200+
</div>
199201
</div>
200202
</div>
201203
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
@import '../../../../styles/mixins.scss';
2+
3+
.kv-query-details {
4+
display: flex;
5+
flex-direction: column;
6+
height: 100%;
7+
background-color: var(--g-color-base-background-dark);
8+
color: var(--g-color-text-primary-invert);
9+
padding: var(--g-spacing-5) var(--g-spacing-6);
10+
11+
&__header {
12+
display: flex;
13+
align-items: center;
14+
justify-content: space-between;
15+
}
16+
17+
&__title {
18+
margin: 0;
19+
font-size: 16px;
20+
font-weight: 500;
21+
}
22+
23+
&__actions {
24+
display: flex;
25+
gap: 8px;
26+
}
27+
28+
&__content {
29+
flex: 1;
30+
padding-top: var(--g-spacing-5);
31+
overflow: auto;
32+
}
33+
34+
&__query-section {
35+
margin-top: 20px;
36+
}
37+
38+
&__query-header {
39+
display: flex;
40+
justify-content: space-between;
41+
align-items: center;
42+
margin-bottom: 12px;
43+
}
44+
45+
&__query-title {
46+
font-size: 14px;
47+
font-weight: 500;
48+
}
49+
50+
&__query-content {
51+
background-color: #1e1e1e;
52+
border-radius: 4px;
53+
overflow: hidden;
54+
position: relative;
55+
56+
pre {
57+
margin: 0 !important;
58+
max-height: 100%;
59+
background-color: transparent !important;
60+
}
61+
}
62+
63+
&__close-button {
64+
color: var(--g-color-text-secondary-invert);
65+
66+
&:hover {
67+
color: var(--g-color-text-primary-invert);
68+
}
69+
}
70+
71+
&__editor-button {
72+
display: flex;
73+
align-items: center;
74+
gap: 6px;
75+
color: var(--g-color-text-secondary-invert);
76+
77+
&:hover {
78+
color: var(--g-color-text-primary-invert);
79+
}
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
3+
import {Button, Icon} from '@gravity-ui/uikit';
4+
5+
import type {InfoViewerItem} from '../../../../components/InfoViewer';
6+
import {InfoViewer} from '../../../../components/InfoViewer';
7+
import {YDBSyntaxHighlighter} from '../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
8+
import type {KeyValueRow} from '../../../../types/api/query';
9+
import {cn} from '../../../../utils/cn';
10+
import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
11+
import {generateHash} from '../../../../utils/generateHash';
12+
import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers';
13+
14+
import i18n from './i18n';
15+
16+
import './QueryDetails.scss';
17+
18+
const b = cn('kv-query-details');
19+
20+
interface QueryDetailsProps {
21+
row: KeyValueRow;
22+
onClose: () => void;
23+
onOpenInEditor: () => void;
24+
}
25+
26+
export const QueryDetails = ({row, onClose, onOpenInEditor}: QueryDetailsProps) => {
27+
const query = row.QueryText as string;
28+
// Create info items for the InfoViewer with formatting matching the columns
29+
const infoItems: InfoViewerItem[] = React.useMemo(() => {
30+
return [
31+
{label: i18n('query-details.query-hash'), value: generateHash(String(row.QueryText))},
32+
{
33+
label: i18n('query-details.cpu-time'),
34+
value: formatToMs(parseUsToMs(row.CPUTimeUs ?? undefined)),
35+
},
36+
{
37+
label: i18n('query-details.duration'),
38+
value: formatToMs(parseUsToMs(row.Duration ?? undefined)),
39+
},
40+
{label: i18n('query-details.read-bytes'), value: formatNumber(row.ReadBytes)},
41+
{label: i18n('query-details.request-units'), value: formatNumber(row.RequestUnits)},
42+
{
43+
label: i18n('query-details.end-time'),
44+
value: row.EndTime
45+
? formatDateTime(new Date(row.EndTime as string).getTime())
46+
: '–',
47+
},
48+
{label: i18n('query-details.read-rows'), value: formatNumber(row.ReadRows)},
49+
{label: i18n('query-details.user-sid'), value: row.UserSID || '–'},
50+
{label: i18n('query-details.application-name'), value: row.ApplicationName || '–'},
51+
{
52+
label: i18n('query-details.query-start-at'),
53+
value: row.QueryStartAt
54+
? formatDateTime(new Date(row.QueryStartAt as string).getTime())
55+
: '–',
56+
},
57+
];
58+
}, [row]);
59+
60+
return (
61+
<div className={b()}>
62+
<div className={b('header')}>
63+
<div className={b('title')}>Query</div>
64+
<div className={b('actions')}>
65+
<Button view="flat" size="l" onClick={onClose} className={b('close-button')}>
66+
<Icon data="close" size={16} />
67+
</Button>
68+
</div>
69+
</div>
70+
71+
<div className={b('content')}>
72+
<InfoViewer info={infoItems} />
73+
74+
<div className={b('query-section')}>
75+
<div className={b('query-header')}>
76+
<div className={b('query-title')}>{i18n('query-details.query.title')}</div>
77+
<Button
78+
view="flat"
79+
size="m"
80+
onClick={onOpenInEditor}
81+
className={b('editor-button')}
82+
>
83+
<Icon data="code" size={16} />
84+
{i18n('query-details.open-in-editor')}
85+
</Button>
86+
</div>
87+
<div className={b('query-content')}>
88+
<YDBSyntaxHighlighter language="yql" text={query} withClipboardButton />
89+
</div>
90+
</div>
91+
</div>
92+
</div>
93+
);
94+
};

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ const b = cn('kv-top-queries');
3030
interface RunningQueriesDataProps {
3131
tenantName: string;
3232
renderQueryModeControl: () => React.ReactNode;
33-
onRowClick: (query: string) => void;
33+
handleRowClick: (row: KeyValueRow) => void;
3434
handleTextSearchUpdate: (text: string) => void;
3535
}
3636

3737
export const RunningQueriesData = ({
3838
tenantName,
3939
renderQueryModeControl,
40-
onRowClick,
40+
handleRowClick,
4141
handleTextSearchUpdate,
4242
}: RunningQueriesDataProps) => {
4343
const [autoRefreshInterval] = useAutoRefreshInterval();
@@ -69,10 +69,6 @@ export const RunningQueriesData = ({
6969
{pollingInterval: autoRefreshInterval},
7070
);
7171

72-
const handleRowClick = (row: KeyValueRow) => {
73-
return onRowClick(row.QueryText as string);
74-
};
75-
7672
return (
7773
<TableWithControlsLayout>
7874
<TableWithControlsLayout.Controls>

src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss

+8
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,12 @@
3232

3333
text-overflow: ellipsis;
3434
}
35+
36+
&__drawer-item {
37+
z-index: 4;
38+
// Because of tabs padding it is needed to move drawer item a little higher
39+
// to make it stick to bottom of tabs
40+
margin-top: calc(-1 * var(--g-spacing-4));
41+
height: calc(100% + var(--g-spacing-4));
42+
}
3543
}

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

+74-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22

3+
import {Drawer, DrawerItem} from '@gravity-ui/navigation';
34
import type {RadioButtonOption} from '@gravity-ui/uikit';
45
import {RadioButton} from '@gravity-ui/uikit';
56
import {useHistory, useLocation} from 'react-router-dom';
@@ -16,10 +17,13 @@ import {
1617
TENANT_PAGES_IDS,
1718
TENANT_QUERY_TABS_ID,
1819
} from '../../../../store/reducers/tenant/constants';
20+
import type {KeyValueRow} from '../../../../types/api/query';
21+
import {cn} from '../../../../utils/cn';
1922
import {useTypedDispatch} from '../../../../utils/hooks';
2023
import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
2124
import {TenantTabsGroups, getTenantPath} from '../../TenantPages';
2225

26+
import {QueryDetails} from './QueryDetails';
2327
import {RunningQueriesData} from './RunningQueriesData';
2428
import {TopQueriesData} from './TopQueriesData';
2529
import {TimeFrameIds} from './constants';
@@ -54,6 +58,9 @@ interface TopQueriesProps {
5458
tenantName: string;
5559
}
5660

61+
const DRAWER_WIDTH_KEY = 'kv-top-queries-drawer-width';
62+
const b = cn('kv-top-queries');
63+
5764
export const TopQueries = ({tenantName}: TopQueriesProps) => {
5865
const dispatch = useTypedDispatch();
5966
const location = useLocation();
@@ -66,6 +73,13 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
6673

6774
const isTopQueries = queryMode === QueryModeIds.top;
6875

76+
const [selectedRow, setSelectedRow] = React.useState<KeyValueRow | null>(null);
77+
const [isDrawerVisible, setIsDrawerVisible] = React.useState(false);
78+
const [drawerWidth, setDrawerWidth] = React.useState(() => {
79+
const savedWidth = localStorage.getItem(DRAWER_WIDTH_KEY);
80+
return savedWidth ? Number(savedWidth) : 400;
81+
});
82+
6983
const applyRowClick = React.useCallback(
7084
(input: string) => {
7185
dispatch(changeUserInput({input}));
@@ -104,22 +118,65 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
104118
);
105119
}, [queryMode, setQueryMode]);
106120

107-
return isTopQueries ? (
108-
<TopQueriesData
109-
tenantName={tenantName}
110-
timeFrame={timeFrame}
111-
renderQueryModeControl={renderQueryModeControl}
112-
onRowClick={onRowClick}
113-
handleTimeFrameChange={handleTimeFrameChange}
114-
handleDateRangeChange={handleDateRangeChange}
115-
handleTextSearchUpdate={handleTextSearchUpdate}
116-
/>
117-
) : (
118-
<RunningQueriesData
119-
tenantName={tenantName}
120-
renderQueryModeControl={renderQueryModeControl}
121-
onRowClick={onRowClick}
122-
handleTextSearchUpdate={handleTextSearchUpdate}
123-
/>
121+
const handleRowClick = (row: KeyValueRow) => {
122+
setSelectedRow(row);
123+
setIsDrawerVisible(true);
124+
};
125+
126+
const handleOpenInEditor = () => {
127+
if (selectedRow) {
128+
onRowClick(selectedRow.QueryText as string);
129+
}
130+
};
131+
132+
const handleCloseDetails = () => {
133+
setIsDrawerVisible(false);
134+
};
135+
136+
const handleResizeDrawer = (width: number) => {
137+
setDrawerWidth(width);
138+
localStorage.setItem(DRAWER_WIDTH_KEY, width.toString());
139+
};
140+
141+
return (
142+
<React.Fragment>
143+
{isTopQueries ? (
144+
<TopQueriesData
145+
tenantName={tenantName}
146+
timeFrame={timeFrame}
147+
renderQueryModeControl={renderQueryModeControl}
148+
handleRowClick={handleRowClick}
149+
handleTimeFrameChange={handleTimeFrameChange}
150+
handleDateRangeChange={handleDateRangeChange}
151+
handleTextSearchUpdate={handleTextSearchUpdate}
152+
/>
153+
) : (
154+
<RunningQueriesData
155+
tenantName={tenantName}
156+
renderQueryModeControl={renderQueryModeControl}
157+
handleRowClick={handleRowClick}
158+
handleTextSearchUpdate={handleTextSearchUpdate}
159+
/>
160+
)}
161+
<Drawer onEscape={handleCloseDetails} onVeilClick={handleCloseDetails} hideVeil>
162+
<DrawerItem
163+
id="query-details"
164+
visible={isDrawerVisible}
165+
resizable
166+
width={drawerWidth}
167+
onResize={handleResizeDrawer}
168+
direction="right"
169+
className={b('drawer-item')}
170+
>
171+
{selectedRow && (
172+
<QueryDetails
173+
row={selectedRow}
174+
onClose={handleCloseDetails}
175+
onOpenInEditor={handleOpenInEditor}
176+
/>
177+
)}
178+
</DrawerItem>
179+
</Drawer>
180+
</React.Fragment>
124181
);
125182
};

0 commit comments

Comments
 (0)