Skip to content

feat: drawer table scroll #2364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/components/FixedHeightQuery/FixedHeightQuery.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.ydb-fixed-height-query {
position: relative;

overflow: hidden;

max-width: 100%;

// Target the YDBSyntaxHighlighter wrapper
> div {
overflow: hidden;

height: 100%;

text-overflow: ellipsis;

// Target the ReactSyntaxHighlighter pre element
pre {
// stylelint-disable-next-line value-no-vendor-prefix
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: var(--line-clamp, 4) !important;
overflow: hidden !important;

height: 100% !important;
margin: 0 !important;
padding: var(--g-spacing-2) !important;

white-space: pre-wrap !important;
text-overflow: ellipsis !important;
word-break: break-word !important;
}

// Target code elements within
code {
overflow: hidden !important;

white-space: pre-wrap !important;
text-overflow: ellipsis !important;
word-break: break-word !important;
}
}
}
56 changes: 56 additions & 0 deletions src/components/FixedHeightQuery/FixedHeightQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

import {cn} from '../../utils/cn';
import {YDBSyntaxHighlighter} from '../SyntaxHighlighter/YDBSyntaxHighlighter';

import './FixedHeightQuery.scss';

const b = cn('ydb-fixed-height-query');

const FIXED_PADDING = 8;
const LINE_HEIGHT = 20;

interface FixedHeightQueryProps {
value?: string;
lines?: number;
hasClipboardButton?: boolean;
clipboardButtonAlwaysVisible?: boolean;
}

export const FixedHeightQuery = ({
value = '',
lines = 4,
hasClipboardButton,
clipboardButtonAlwaysVisible,
}: FixedHeightQueryProps) => {
const heightValue = `${lines * LINE_HEIGHT + FIXED_PADDING}px`;

// Remove empty lines from the beginning (lines with only whitespace are considered empty)
const trimmedValue = value.replace(/^(\s*\n)+/, '');

return (
<div
className={b()}
style={
{
height: heightValue,
'--line-clamp': lines,
} as React.CSSProperties & {'--line-clamp': number}
}
>
<YDBSyntaxHighlighter
language="yql"
text={trimmedValue}
withClipboardButton={
hasClipboardButton
? {
alwaysVisible: clipboardButtonAlwaysVisible,
copyText: value,
withLabel: false,
}
: false
}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
TOP_QUERIES_SELECTED_COLUMNS_LS_KEY,
} from './columns/constants';
import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants';
import type {ReactList} from './hooks/useScrollToSelected';
import {useScrollToSelected} from './hooks/useScrollToSelected';
import {useSetSelectedTopQueryRowFromParams} from './hooks/useSetSelectedTopQueryRowFromParams';
import {useTopQueriesSort} from './hooks/useTopQueriesSort';
import i18n from './i18n';
Expand Down Expand Up @@ -61,6 +63,9 @@ export const TopQueriesData = ({
// null is reserved for not found state
const [selectedRow, setSelectedRow] = React.useState<KeyValueRow | null | undefined>(undefined);

// Ref for react-list component to enable scrolling to selected row
const reactListRef = React.useRef<ReactList>(null);

// Get columns for top queries
const columns: Column<KeyValueRow>[] = React.useMemo(() => {
return getTopQueriesColumns();
Expand Down Expand Up @@ -89,6 +94,18 @@ export const TopQueriesData = ({
const rows = currentData?.resultSets?.[0]?.result;
useSetSelectedTopQueryRowFromParams(setSelectedRow, rows);

// Enhanced table settings with dynamicInnerRef for scrolling
const tableSettings = React.useMemo(
() => ({
...TOP_QUERIES_TABLE_SETTINGS,
dynamicInnerRef: reactListRef,
}),
[],
);

// Use custom hook to handle scrolling to selected row
useScrollToSelected({selectedRow, rows, reactListRef});

const handleCloseDetails = React.useCallback(() => {
setSelectedRow(undefined);
}, [setSelectedRow]);
Expand Down Expand Up @@ -182,7 +199,7 @@ export const TopQueriesData = ({
columns={columnsToShow}
data={rows || []}
loading={isFetching && currentData === undefined}
settings={TOP_QUERIES_TABLE_SETTINGS}
settings={tableSettings}
onRowClick={onRowClick}
rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})}
sortOrder={tableSort}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import DataTable from '@gravity-ui/react-data-table';
import type {Column, OrderType} from '@gravity-ui/react-data-table';

import {FixedHeightQuery} from '../../../../../components/FixedHeightQuery/FixedHeightQuery';
import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
import {TruncatedQuery} from '../../../../../components/TruncatedQuery/TruncatedQuery';
import type {KeyValueRow} from '../../../../../types/api/query';
import {cn} from '../../../../../utils/cn';
import {formatDateTime, formatNumber} from '../../../../../utils/dataFormatters/dataFormatters';
import {generateHash} from '../../../../../utils/generateHash';
import {formatToMs, parseUsToMs} from '../../../../../utils/timeParsers';
import {MAX_QUERY_HEIGHT} from '../../../utils/constants';

import {
QUERIES_COLUMNS_IDS,
Expand All @@ -34,11 +33,7 @@ const queryTextColumn: Column<KeyValueRow> = {
header: QUERIES_COLUMNS_TITLES.QueryText,
render: ({row}) => (
<div className={b('query')}>
<TruncatedQuery
value={row.QueryText?.toString()}
maxQueryHeight={MAX_QUERY_HEIGHT}
hasClipboardButton
/>
<FixedHeightQuery value={row.QueryText?.toString()} lines={3} hasClipboardButton />
</div>
),
width: 500,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';

import {isEqual} from 'lodash';

import type {KeyValueRow} from '../../../../../types/api/query';

// Type for react-list component
export interface ReactList {
scrollTo: (index: number) => void;
scrollAround: (index: number) => void;
getVisibleRange: () => [number, number];
}

interface UseScrollToSelectedParams {
selectedRow: KeyValueRow | null | undefined;
rows: KeyValueRow[] | undefined;
reactListRef: React.RefObject<ReactList>;
}

/**
* Custom hook to handle scrolling to selected row in react-list
* Only scrolls if the selected item is not currently visible
* When scrolling, positions the item in the middle of the viewport
*/
export function useScrollToSelected({selectedRow, rows, reactListRef}: UseScrollToSelectedParams) {
React.useEffect(() => {
if (selectedRow && rows && reactListRef.current) {
const selectedIndex = rows.findIndex((row) => isEqual(row, selectedRow));
if (selectedIndex !== -1) {
const reactList = reactListRef.current;

try {
const visibleRange = reactList.getVisibleRange();
const [firstVisible, lastVisible] = visibleRange;

// Check if selected item is already visible
const isVisible = selectedIndex >= firstVisible && selectedIndex <= lastVisible;

if (!isVisible) {
reactList.scrollTo(selectedIndex - 1);
}
} catch {
// Fallback to scrollAround if getVisibleRange fails
reactList.scrollAround(selectedIndex);
}
}
}
}, [selectedRow, rows, reactListRef]);
}
1 change: 1 addition & 0 deletions src/containers/Tenant/Diagnostics/TopQueries/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const TOP_QUERIES_TABLE_SETTINGS: Settings = {
...QUERY_TABLE_SETTINGS,
disableSortReset: true,
externalSort: true,
dynamicRenderType: 'uniform', // All rows have fixed height due to FixedHeightQuery
};

export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] {
Expand Down
50 changes: 50 additions & 0 deletions tests/suites/tenant/diagnostics/Diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ export class Table {
});
return true;
}

async clickRow(row: number) {
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
await rowElement.click();
}

async getRowPosition(row: number) {
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
return await rowElement.boundingBox();
}

async isRowVisible(row: number) {
const rowElement = this.table.locator(`tr.data-table__row:nth-child(${row})`);
const boundingBox = await rowElement.boundingBox();
if (!boundingBox) {
return false;
}

const viewportHeight = await rowElement.page().evaluate(() => window.innerHeight);
return boundingBox.y >= 0 && boundingBox.y + boundingBox.height <= viewportHeight;
}
}

export enum QueriesSwitch {
Expand Down Expand Up @@ -222,6 +243,8 @@ export class Diagnostics {
private memoryCard: Locator;
private healthcheckCard: Locator;
private tableRadioButton: Locator;
private fixedHeightQueryElements: Locator;
private copyLinkButton: Locator;

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

// Info tab cards
this.cpuCard = page.locator('.metrics-cards__tab:has-text("CPU")');
Expand Down Expand Up @@ -373,4 +398,29 @@ export class Diagnostics {
.textContent();
return selectedText?.trim() || '';
}

async getFixedHeightQueryElementsCount(): Promise<number> {
return await this.fixedHeightQueryElements.count();
}

async getFixedHeightQueryElementHeight(index: number): Promise<string> {
const element = this.fixedHeightQueryElements.nth(index);
return await element.evaluate((el) => {
return window.getComputedStyle(el).height;
});
}

async clickCopyLinkButton(): Promise<void> {
await this.copyLinkButton.first().click();
}

async isCopyLinkButtonVisible(): Promise<boolean> {
return await this.copyLinkButton.first().isVisible();
}

async isRowActive(rowIndex: number): Promise<boolean> {
const rowElement = this.dataTable.locator(`tr.data-table__row:nth-child(${rowIndex})`);
const rowElementClass = await rowElement.getAttribute('class');
return rowElementClass?.includes('kv-top-queries__row_active') || false;
}
}
Loading
Loading