Skip to content

Commit 34ea140

Browse files
authored
feat: drawer table scroll (#2364)
1 parent 91deb63 commit 34ea140

File tree

9 files changed

+434
-8
lines changed

9 files changed

+434
-8
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.ydb-fixed-height-query {
2+
position: relative;
3+
4+
overflow: hidden;
5+
6+
max-width: 100%;
7+
8+
// Target the YDBSyntaxHighlighter wrapper
9+
> div {
10+
overflow: hidden;
11+
12+
height: 100%;
13+
14+
text-overflow: ellipsis;
15+
16+
// Target the ReactSyntaxHighlighter pre element
17+
pre {
18+
// stylelint-disable-next-line value-no-vendor-prefix
19+
display: -webkit-box !important;
20+
-webkit-box-orient: vertical !important;
21+
-webkit-line-clamp: var(--line-clamp, 4) !important;
22+
overflow: hidden !important;
23+
24+
height: 100% !important;
25+
margin: 0 !important;
26+
padding: var(--g-spacing-2) !important;
27+
28+
white-space: pre-wrap !important;
29+
text-overflow: ellipsis !important;
30+
word-break: break-word !important;
31+
}
32+
33+
// Target code elements within
34+
code {
35+
overflow: hidden !important;
36+
37+
white-space: pre-wrap !important;
38+
text-overflow: ellipsis !important;
39+
word-break: break-word !important;
40+
}
41+
}
42+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
3+
import {cn} from '../../utils/cn';
4+
import {YDBSyntaxHighlighter} from '../SyntaxHighlighter/YDBSyntaxHighlighter';
5+
6+
import './FixedHeightQuery.scss';
7+
8+
const b = cn('ydb-fixed-height-query');
9+
10+
const FIXED_PADDING = 8;
11+
const LINE_HEIGHT = 20;
12+
13+
interface FixedHeightQueryProps {
14+
value?: string;
15+
lines?: number;
16+
hasClipboardButton?: boolean;
17+
clipboardButtonAlwaysVisible?: boolean;
18+
}
19+
20+
export const FixedHeightQuery = ({
21+
value = '',
22+
lines = 4,
23+
hasClipboardButton,
24+
clipboardButtonAlwaysVisible,
25+
}: FixedHeightQueryProps) => {
26+
const heightValue = `${lines * LINE_HEIGHT + FIXED_PADDING}px`;
27+
28+
// Remove empty lines from the beginning (lines with only whitespace are considered empty)
29+
const trimmedValue = value.replace(/^(\s*\n)+/, '');
30+
31+
return (
32+
<div
33+
className={b()}
34+
style={
35+
{
36+
height: heightValue,
37+
'--line-clamp': lines,
38+
} as React.CSSProperties & {'--line-clamp': number}
39+
}
40+
>
41+
<YDBSyntaxHighlighter
42+
language="yql"
43+
text={trimmedValue}
44+
withClipboardButton={
45+
hasClipboardButton
46+
? {
47+
alwaysVisible: clipboardButtonAlwaysVisible,
48+
copyText: value,
49+
withLabel: false,
50+
}
51+
: false
52+
}
53+
/>
54+
</div>
55+
);
56+
};

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
TOP_QUERIES_SELECTED_COLUMNS_LS_KEY,
3131
} from './columns/constants';
3232
import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants';
33+
import type {ReactList} from './hooks/useScrollToSelected';
34+
import {useScrollToSelected} from './hooks/useScrollToSelected';
3335
import {useSetSelectedTopQueryRowFromParams} from './hooks/useSetSelectedTopQueryRowFromParams';
3436
import {useTopQueriesSort} from './hooks/useTopQueriesSort';
3537
import i18n from './i18n';
@@ -61,6 +63,9 @@ export const TopQueriesData = ({
6163
// null is reserved for not found state
6264
const [selectedRow, setSelectedRow] = React.useState<KeyValueRow | null | undefined>(undefined);
6365

66+
// Ref for react-list component to enable scrolling to selected row
67+
const reactListRef = React.useRef<ReactList>(null);
68+
6469
// Get columns for top queries
6570
const columns: Column<KeyValueRow>[] = React.useMemo(() => {
6671
return getTopQueriesColumns();
@@ -89,6 +94,18 @@ export const TopQueriesData = ({
8994
const rows = currentData?.resultSets?.[0]?.result;
9095
useSetSelectedTopQueryRowFromParams(setSelectedRow, rows);
9196

97+
// Enhanced table settings with dynamicInnerRef for scrolling
98+
const tableSettings = React.useMemo(
99+
() => ({
100+
...TOP_QUERIES_TABLE_SETTINGS,
101+
dynamicInnerRef: reactListRef,
102+
}),
103+
[],
104+
);
105+
106+
// Use custom hook to handle scrolling to selected row
107+
useScrollToSelected({selectedRow, rows, reactListRef});
108+
92109
const handleCloseDetails = React.useCallback(() => {
93110
setSelectedRow(undefined);
94111
}, [setSelectedRow]);
@@ -182,7 +199,7 @@ export const TopQueriesData = ({
182199
columns={columnsToShow}
183200
data={rows || []}
184201
loading={isFetching && currentData === undefined}
185-
settings={TOP_QUERIES_TABLE_SETTINGS}
202+
settings={tableSettings}
186203
onRowClick={onRowClick}
187204
rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})}
188205
sortOrder={tableSort}

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import DataTable from '@gravity-ui/react-data-table';
22
import type {Column, OrderType} from '@gravity-ui/react-data-table';
33

4+
import {FixedHeightQuery} from '../../../../../components/FixedHeightQuery/FixedHeightQuery';
45
import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
5-
import {TruncatedQuery} from '../../../../../components/TruncatedQuery/TruncatedQuery';
66
import type {KeyValueRow} from '../../../../../types/api/query';
77
import {cn} from '../../../../../utils/cn';
88
import {formatDateTime, formatNumber} from '../../../../../utils/dataFormatters/dataFormatters';
99
import {generateHash} from '../../../../../utils/generateHash';
1010
import {formatToMs, parseUsToMs} from '../../../../../utils/timeParsers';
11-
import {MAX_QUERY_HEIGHT} from '../../../utils/constants';
1211

1312
import {
1413
QUERIES_COLUMNS_IDS,
@@ -34,11 +33,7 @@ const queryTextColumn: Column<KeyValueRow> = {
3433
header: QUERIES_COLUMNS_TITLES.QueryText,
3534
render: ({row}) => (
3635
<div className={b('query')}>
37-
<TruncatedQuery
38-
value={row.QueryText?.toString()}
39-
maxQueryHeight={MAX_QUERY_HEIGHT}
40-
hasClipboardButton
41-
/>
36+
<FixedHeightQuery value={row.QueryText?.toString()} lines={3} hasClipboardButton />
4237
</div>
4338
),
4439
width: 500,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
3+
import {isEqual} from 'lodash';
4+
5+
import type {KeyValueRow} from '../../../../../types/api/query';
6+
7+
// Type for react-list component
8+
export interface ReactList {
9+
scrollTo: (index: number) => void;
10+
scrollAround: (index: number) => void;
11+
getVisibleRange: () => [number, number];
12+
}
13+
14+
interface UseScrollToSelectedParams {
15+
selectedRow: KeyValueRow | null | undefined;
16+
rows: KeyValueRow[] | undefined;
17+
reactListRef: React.RefObject<ReactList>;
18+
}
19+
20+
/**
21+
* Custom hook to handle scrolling to selected row in react-list
22+
* Only scrolls if the selected item is not currently visible
23+
* When scrolling, positions the item in the middle of the viewport
24+
*/
25+
export function useScrollToSelected({selectedRow, rows, reactListRef}: UseScrollToSelectedParams) {
26+
React.useEffect(() => {
27+
if (selectedRow && rows && reactListRef.current) {
28+
const selectedIndex = rows.findIndex((row) => isEqual(row, selectedRow));
29+
if (selectedIndex !== -1) {
30+
const reactList = reactListRef.current;
31+
32+
try {
33+
const visibleRange = reactList.getVisibleRange();
34+
const [firstVisible, lastVisible] = visibleRange;
35+
36+
// Check if selected item is already visible
37+
const isVisible = selectedIndex >= firstVisible && selectedIndex <= lastVisible;
38+
39+
if (!isVisible) {
40+
reactList.scrollTo(selectedIndex - 1);
41+
}
42+
} catch {
43+
// Fallback to scrollAround if getVisibleRange fails
44+
reactList.scrollAround(selectedIndex);
45+
}
46+
}
47+
}
48+
}, [selectedRow, rows, reactListRef]);
49+
}

src/containers/Tenant/Diagnostics/TopQueries/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const TOP_QUERIES_TABLE_SETTINGS: Settings = {
1313
...QUERY_TABLE_SETTINGS,
1414
disableSortReset: true,
1515
externalSort: true,
16+
dynamicRenderType: 'uniform', // All rows have fixed height due to FixedHeightQuery
1617
};
1718

1819
export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] {

tests/suites/tenant/diagnostics/Diagnostics.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ export class Table {
120120
});
121121
return true;
122122
}
123+
124+
async clickRow(row: number) {
125+
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
126+
await rowElement.click();
127+
}
128+
129+
async getRowPosition(row: number) {
130+
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
131+
return await rowElement.boundingBox();
132+
}
133+
134+
async isRowVisible(row: number) {
135+
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
136+
const boundingBox = await rowElement.boundingBox();
137+
if (!boundingBox) {
138+
return false;
139+
}
140+
141+
const viewportHeight = await rowElement.page().evaluate(() => window.innerHeight);
142+
return boundingBox.y >= 0 && boundingBox.y + boundingBox.height <= viewportHeight;
143+
}
123144
}
124145

125146
export enum QueriesSwitch {
@@ -222,6 +243,8 @@ export class Diagnostics {
222243
private memoryCard: Locator;
223244
private healthcheckCard: Locator;
224245
private tableRadioButton: Locator;
246+
private fixedHeightQueryElements: Locator;
247+
private copyLinkButton: Locator;
225248

226249
constructor(page: Page) {
227250
this.storage = new StoragePage(page);
@@ -238,6 +261,8 @@ export class Diagnostics {
238261
this.tableRadioButton = page.locator(
239262
'.ydb-table-with-controls-layout__controls .g-radio-button',
240263
);
264+
this.fixedHeightQueryElements = page.locator('.ydb-fixed-height-query');
265+
this.copyLinkButton = page.locator('.ydb-copy-link-button__icon');
241266

242267
// Info tab cards
243268
this.cpuCard = page.locator('.metrics-cards__tab:has-text("CPU")');
@@ -373,4 +398,29 @@ export class Diagnostics {
373398
.textContent();
374399
return selectedText?.trim() || '';
375400
}
401+
402+
async getFixedHeightQueryElementsCount(): Promise<number> {
403+
return await this.fixedHeightQueryElements.count();
404+
}
405+
406+
async getFixedHeightQueryElementHeight(index: number): Promise<string> {
407+
const element = this.fixedHeightQueryElements.nth(index);
408+
return await element.evaluate((el) => {
409+
return window.getComputedStyle(el).height;
410+
});
411+
}
412+
413+
async clickCopyLinkButton(): Promise<void> {
414+
await this.copyLinkButton.first().click();
415+
}
416+
417+
async isCopyLinkButtonVisible(): Promise<boolean> {
418+
return await this.copyLinkButton.first().isVisible();
419+
}
420+
421+
async isRowActive(rowIndex: number): Promise<boolean> {
422+
const rowElement = this.dataTable.locator(`tr.data-table__row:nth-child(${rowIndex})`);
423+
const rowElementClass = await rowElement.getAttribute('class');
424+
return rowElementClass?.includes('kv-top-queries__row_active') || false;
425+
}
376426
}

0 commit comments

Comments
 (0)