From b1206e6235c2c536009b570cb20556f5c96b8763 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 4 Jun 2025 16:10:45 +0300 Subject: [PATCH 1/9] feat: drawer table scroll --- .../FixedHeightQuery/FixedHeightQuery.scss | 45 +++++++++++++++ .../FixedHeightQuery/FixedHeightQuery.tsx | 56 +++++++++++++++++++ .../TopQueries/columns/columns.tsx | 9 +-- 3 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/components/FixedHeightQuery/FixedHeightQuery.scss create mode 100644 src/components/FixedHeightQuery/FixedHeightQuery.tsx diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.scss b/src/components/FixedHeightQuery/FixedHeightQuery.scss new file mode 100644 index 000000000..687da7fee --- /dev/null +++ b/src/components/FixedHeightQuery/FixedHeightQuery.scss @@ -0,0 +1,45 @@ +.kv-fixed-height-query { + position: relative; + + overflow: hidden; + + max-width: 100%; + + // Target the YDBSyntaxHighlighter wrapper + > div { + display: box; + -webkit-box-orient: vertical; + -webkit-line-clamp: var(--line-clamp, 4); + overflow: hidden; + + height: 100%; + + text-overflow: ellipsis; + + // Target the ReactSyntaxHighlighter pre element + pre { + display: flex !important; + overflow: hidden !important; + align-items: center !important; + + height: 100% !important; + margin: 0 !important; + padding: 8px !important; + + white-space: pre-wrap !important; + word-break: break-word !important; + } + + // Target code elements within + code { + display: box !important; + -webkit-box-orient: vertical !important; + -webkit-line-clamp: var(--line-clamp, 4) !important; + overflow: hidden !important; + + white-space: pre-wrap !important; + text-overflow: ellipsis !important; + word-break: break-word !important; + } + } +} diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.tsx b/src/components/FixedHeightQuery/FixedHeightQuery.tsx new file mode 100644 index 000000000..d9ca20e17 --- /dev/null +++ b/src/components/FixedHeightQuery/FixedHeightQuery.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import {cn} from '../../utils/cn'; +import {YDBSyntaxHighlighter} from '../SyntaxHighlighter/YDBSyntaxHighlighter'; + +import './FixedHeightQuery.scss'; + +const b = cn('kv-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 ( +
+ +
+ ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx index 760a0790b..bda84822e 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx @@ -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, @@ -34,11 +33,7 @@ const queryTextColumn: Column = { header: QUERIES_COLUMNS_TITLES.QueryText, render: ({row}) => (
- +
), width: 500, From e1d89f209b3f2bee8a0de88dbad33f17f29eb9f8 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 4 Jun 2025 16:52:15 +0300 Subject: [PATCH 2/9] fix: scroll to selected row --- .../Diagnostics/TopQueries/TopQueriesData.tsx | 20 ++++++- .../TopQueries/hooks/useScrollToSelected.ts | 55 +++++++++++++++++++ .../Tenant/Diagnostics/TopQueries/utils.ts | 1 + 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index c734ffae3..a6955eec7 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -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'; @@ -61,6 +63,9 @@ export const TopQueriesData = ({ // null is reserved for not found state const [selectedRow, setSelectedRow] = React.useState(undefined); + // Ref for react-list component to enable scrolling to selected row + const reactListRef = React.useRef(null); + // Get columns for top queries const columns: Column[] = React.useMemo(() => { return getTopQueriesColumns(); @@ -89,6 +94,19 @@ 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, + // Using 'uniform' type - react-list automatically calculates size from first item + }), + [], + ); + + // Use custom hook to handle scrolling to selected row + useScrollToSelected({selectedRow, rows, reactListRef}); + const handleCloseDetails = React.useCallback(() => { setSelectedRow(undefined); }, [setSelectedRow]); @@ -182,7 +200,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} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts new file mode 100644 index 000000000..9acd3194c --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts @@ -0,0 +1,55 @@ +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; +} + +/** + * 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) { + // Only scroll if not visible - position in middle of viewport + const visibleCount = lastVisible - firstVisible + 1; + const middleOffset = Math.floor(visibleCount / 2); + const targetIndex = Math.max(0, selectedIndex - middleOffset); + + reactList.scrollTo(targetIndex); + } + // If already visible, don't scroll (better UX) + } catch { + // Fallback to scrollAround if getVisibleRange fails + reactList.scrollAround(selectedIndex); + } + } + } + }, [selectedRow, rows, reactListRef]); +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 9a00c085d..6999e5b3b 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -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[] { From 16ab0cc7be609bd7c983ac14564475ffd2f116dc Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 4 Jun 2025 17:13:37 +0300 Subject: [PATCH 3/9] fix: better styles --- src/components/FixedHeightQuery/FixedHeightQuery.scss | 6 ++++-- .../Tenant/Diagnostics/TopQueries/columns/columns.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.scss b/src/components/FixedHeightQuery/FixedHeightQuery.scss index 687da7fee..46a0f2812 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.scss +++ b/src/components/FixedHeightQuery/FixedHeightQuery.scss @@ -18,15 +18,17 @@ // Target the ReactSyntaxHighlighter pre element pre { - display: flex !important; + display: box !important; + -webkit-box-orient: vertical !important; + -webkit-line-clamp: var(--line-clamp, 4) !important; overflow: hidden !important; - align-items: center !important; height: 100% !important; margin: 0 !important; padding: 8px !important; white-space: pre-wrap !important; + text-overflow: ellipsis !important; word-break: break-word !important; } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx index bda84822e..089047a1f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx @@ -33,7 +33,7 @@ const queryTextColumn: Column = { header: QUERIES_COLUMNS_TITLES.QueryText, render: ({row}) => (
- +
), width: 500, From fe745adb7da10f42c8251ebbea8db8653cce9dc4 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 4 Jun 2025 17:41:48 +0300 Subject: [PATCH 4/9] fix: fix scss --- src/components/FixedHeightQuery/FixedHeightQuery.scss | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.scss b/src/components/FixedHeightQuery/FixedHeightQuery.scss index 46a0f2812..905b17bc8 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.scss +++ b/src/components/FixedHeightQuery/FixedHeightQuery.scss @@ -7,9 +7,6 @@ // Target the YDBSyntaxHighlighter wrapper > div { - display: box; - -webkit-box-orient: vertical; - -webkit-line-clamp: var(--line-clamp, 4); overflow: hidden; height: 100%; @@ -18,14 +15,15 @@ // Target the ReactSyntaxHighlighter pre element pre { - display: box !important; + // 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: 8px !important; + padding: var(--g-spacing-2) !important; white-space: pre-wrap !important; text-overflow: ellipsis !important; @@ -34,9 +32,6 @@ // Target code elements within code { - display: box !important; - -webkit-box-orient: vertical !important; - -webkit-line-clamp: var(--line-clamp, 4) !important; overflow: hidden !important; white-space: pre-wrap !important; From 31a8ec4fe13b5435cad02b15fadbffdc0ae7651f Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 4 Jun 2025 17:53:53 +0300 Subject: [PATCH 5/9] fix: scroll to first --- .../Diagnostics/TopQueries/hooks/useScrollToSelected.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts index 9acd3194c..f73b8d5c7 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useScrollToSelected.ts @@ -37,14 +37,8 @@ export function useScrollToSelected({selectedRow, rows, reactListRef}: UseScroll const isVisible = selectedIndex >= firstVisible && selectedIndex <= lastVisible; if (!isVisible) { - // Only scroll if not visible - position in middle of viewport - const visibleCount = lastVisible - firstVisible + 1; - const middleOffset = Math.floor(visibleCount / 2); - const targetIndex = Math.max(0, selectedIndex - middleOffset); - - reactList.scrollTo(targetIndex); + reactList.scrollTo(selectedIndex - 1); } - // If already visible, don't scroll (better UX) } catch { // Fallback to scrollAround if getVisibleRange fails reactList.scrollAround(selectedIndex); From 1ba16df5390a85b83fe2c8c26e2c42c495feb64a Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 6 Jun 2025 15:47:54 +0300 Subject: [PATCH 6/9] chore: add tests --- .../FixedHeightQuery/FixedHeightQuery.scss | 2 +- .../FixedHeightQuery/FixedHeightQuery.tsx | 2 +- .../Diagnostics/TopQueries/TopQueriesData.tsx | 1 - .../suites/tenant/diagnostics/Diagnostics.ts | 21 +++ .../tenant/diagnostics/tabs/queries.test.ts | 133 ++++++++++++++++++ 5 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.scss b/src/components/FixedHeightQuery/FixedHeightQuery.scss index 905b17bc8..02fa7124d 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.scss +++ b/src/components/FixedHeightQuery/FixedHeightQuery.scss @@ -1,4 +1,4 @@ -.kv-fixed-height-query { +.ydb-fixed-height-query { position: relative; overflow: hidden; diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.tsx b/src/components/FixedHeightQuery/FixedHeightQuery.tsx index d9ca20e17..1eefe60f9 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.tsx +++ b/src/components/FixedHeightQuery/FixedHeightQuery.tsx @@ -5,7 +5,7 @@ import {YDBSyntaxHighlighter} from '../SyntaxHighlighter/YDBSyntaxHighlighter'; import './FixedHeightQuery.scss'; -const b = cn('kv-fixed-height-query'); +const b = cn('ydb-fixed-height-query'); const FIXED_PADDING = 8; const LINE_HEIGHT = 20; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index a6955eec7..70be0027d 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -99,7 +99,6 @@ export const TopQueriesData = ({ () => ({ ...TOP_QUERIES_TABLE_SETTINGS, dynamicInnerRef: reactListRef, - // Using 'uniform' type - react-list automatically calculates size from first item }), [], ); diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index fdd1e9820..5dde3e083 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -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 { diff --git a/tests/suites/tenant/diagnostics/tabs/queries.test.ts b/tests/suites/tenant/diagnostics/tabs/queries.test.ts index 58d8c7189..c82340830 100644 --- a/tests/suites/tenant/diagnostics/tabs/queries.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/queries.test.ts @@ -179,4 +179,137 @@ test.describe('Diagnostics Queries tab', async () => { // Verify the view was updated back expect(await diagnostics.getSelectedQueryPeriod()).toBe(QueryPeriod.PerHour); }); + + test('FixedHeightQuery maintains consistent height and proper scrolling behavior', async ({ + page, + }) => { + const pageQueryParams = { + schema: tenantName, + database: tenantName, + tenantPage: 'diagnostics', + diagnosticsTab: 'topQueries', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const diagnostics = new Diagnostics(page); + await expect(diagnostics.table.isVisible()).resolves.toBe(true); + + // Verify we have enough rows to test scrolling + const rowCount = await diagnostics.table.getRowCount(); + if (rowCount > 5) { + // Test scrolling behavior: click on a row that might not be fully visible + const targetRowIndex = Math.min(rowCount, 8); // Target a row further down + + // Click on the target row to test scrolling + await diagnostics.table.clickRow(targetRowIndex); + + // Wait for any scrolling animation + await page.waitForTimeout(500); + + // Verify the row is now visible in the viewport + const isVisible = await diagnostics.table.isRowVisible(targetRowIndex); + expect(isVisible).toBe(true); + } + }); + + test('FixedHeightQuery components have consistent height across different query lengths', async ({ + page, + }) => { + const pageQueryParams = { + schema: tenantName, + database: tenantName, + tenantPage: 'diagnostics', + diagnosticsTab: 'topQueries', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const diagnostics = new Diagnostics(page); + await expect(diagnostics.table.isVisible()).resolves.toBe(true); + + // Check that FixedHeightQuery components have the expected fixed height + const fixedHeightElements = page.locator('.ydb-fixed-height-query'); + const elementCount = await fixedHeightElements.count(); + + if (elementCount > 1) { + // Check that all FixedHeightQuery components have the same height + const heights = []; + for (let i = 0; i < Math.min(elementCount, 5); i++) { + const element = fixedHeightElements.nth(i); + const height = await element.evaluate((el) => { + return window.getComputedStyle(el).height; + }); + heights.push(height); + } + + // All heights should be the same (88px for 4 lines) + const firstHeight = heights[0]; + expect(firstHeight).toBe('88px'); + + for (const height of heights) { + expect(height).toBe(firstHeight); + } + } + }); + + test.only('Scroll to row, get shareable link, navigate to URL and verify row is scrolled into view', async ({ + page, + }) => { + const pageQueryParams = { + schema: tenantName, + database: tenantName, + tenantPage: 'diagnostics', + diagnosticsTab: 'topQueries', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const diagnostics = new Diagnostics(page); + await expect(diagnostics.table.isVisible()).resolves.toBe(true); + + // Get the number of rows and select a row that requires scrolling + const rowCount = await diagnostics.table.getRowCount(); + if (rowCount > 5) { + const targetRowIndex = Math.min(rowCount, 8); // Target a row further down + + // Click on the target row to open the drawer + await diagnostics.table.clickRow(targetRowIndex); + + // Wait for drawer to open + await page.waitForTimeout(500); + + // Find and click the copy link button in the drawer + const copyLinkButton = page.locator('.ydb-copy-link-button__icon').first(); + await expect(copyLinkButton).toBeVisible(); + await copyLinkButton.click(); + + // Get the copied URL from clipboard + const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboardText).toBeTruthy(); + expect(clipboardText).toContain('/tenant'); + + // Navigate to the copied URL + await page.goto(clipboardText); + await page.waitForTimeout(1000); + + // Verify the table is visible and the target row is scrolled into view + await expect(diagnostics.table.isVisible()).resolves.toBe(true); + + // Check that the target row is visible in the viewport + const isRowVisible = await diagnostics.table.isRowVisible(targetRowIndex); + expect(isRowVisible).toBe(true); + + // Verify the row is highlighted/selected (if applicable) + const rowElement = page.locator(`tr.data-table__row:nth-child(${targetRowIndex})`); + const hasActiveClass = await rowElement.evaluate((el: HTMLElement) => { + return ( + el.classList.contains('kv-top-queries__row_active') || + el.classList.contains('active') || + el.getAttribute('aria-selected') === 'true' + ); + }); + expect(hasActiveClass).toBe(true); + } + }); }); From bb8b6a2a2074cd3dfd4149cfd632bf0fa597fcb3 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 6 Jun 2025 18:02:44 +0300 Subject: [PATCH 7/9] fix: tests --- tests/suites/tenant/diagnostics/mocks.ts | 117 ++++++++++++++++++ .../tenant/diagnostics/tabs/queries.test.ts | 91 ++++++++------ 2 files changed, 167 insertions(+), 41 deletions(-) diff --git a/tests/suites/tenant/diagnostics/mocks.ts b/tests/suites/tenant/diagnostics/mocks.ts index b6ba2c677..3aa81d10b 100644 --- a/tests/suites/tenant/diagnostics/mocks.ts +++ b/tests/suites/tenant/diagnostics/mocks.ts @@ -64,6 +64,123 @@ const generateTopShardsHistoryRow = (index: number) => { ]; }; +/** + * Generates a mock row for the TopQueries table + * @param index Row index (0-based) + * @returns An array of values for each column + */ +const generateTopQueriesRow = (index: number) => { + // Use a fixed base date for consistent results + const baseDate = new Date('2025-06-06T12:00:00Z'); + + // Generate end time in the past 6 hours (deterministic) + const endTime = new Date(baseDate); + endTime.setMinutes(endTime.getMinutes() - (index * 3 + (index % 30))); + + // Generate CPU time in microseconds (deterministic based on index) + const cpuTimeUs = 1000 + ((index * 1000) % 99000); + + // Generate duration in microseconds (slightly higher than CPU time) + const duration = cpuTimeUs + ((index * 500) % 50000); + + // Generate read bytes (deterministic) + const readBytes = 100 + ((index * 100) % 9900); + + // Generate read rows (deterministic) + const readRows = 1 + ((index * 10) % 999); + + // Generate request units (deterministic) + const requestUnits = 1 + ((index * 2) % 49); + + // Generate rank + const rank = index + 1; + + // Generate user SID + const users = [ + 'user@system', + 'admin@system', + 'service@system', + 'metadata@system', + 'test@system', + ]; + const userSID = users[index % users.length]; + + // Generate various query types + const queryTemplates = [ + `SELECT * FROM \`//dev02/home/xenoxeno/db1/.metadata/initialization/migrations\`;`, + `SELECT * FROM \`//dev02/home/xenoxeno/db1/.metadata/secrets/values\`;`, + `--!syntax_v1\nDECLARE $c0_0 AS Uint64;\nDECLARE $c0_1 AS String;\nINSERT INTO \`kv_test\` (c0, c1) VALUES ($c0_0, $c0_1)`, + `SELECT * FROM \`ydb/MasterClusterExt.db\`;\nSELECT version_str, color_class FROM \`ydb/MasterClusterVersions.db\`;`, + `DECLARE $name AS Utf8;\nSELECT * FROM \`ydb/MasterClusterExt.db\` WHERE name=$name`, + `SELECT COUNT(*) FROM \`big_kv_test\` WHERE id > 1000;`, + `UPDATE \`boring_table\` SET value = 'updated' WHERE key = 'test';`, + `SELECT a.*, b.* FROM \`cities\` a JOIN \`boring_table2\` b ON a.id = b.city_id;`, + `INSERT INTO \`my_column_table\` (id, name, timestamp) VALUES (${index}, 'test_${index}', CurrentUtcTimestamp());`, + `DELETE FROM \`my_row_table\` WHERE created_at < DateTime::MakeDate(DateTime::ParseIso8601('2024-01-01T00:00:00Z'));`, + ]; + + const queryText = queryTemplates[index % queryTemplates.length]; + + return [ + cpuTimeUs, // CPUTimeUs + queryText, // QueryText + endTime.toISOString(), // IntervalEnd + endTime.toISOString(), // EndTime + readRows, // ReadRows + readBytes, // ReadBytes + userSID, // UserSID + duration, // Duration + requestUnits, // RequestUnits + rank, // Rank + ]; +}; + +/** + * Sets up a mock for the TopQueries tab with 100 rows for scrolling tests + */ +export const setupTopQueriesMock = async (page: Page) => { + await page.route(`${backend}/viewer/json/query?*`, async (route) => { + const request = route.request(); + const postData = request.postData(); + + // Only mock TopQueries requests (check if it's a TopQueries query) + if (postData && postData.includes('CPUTime as CPUTimeUs')) { + await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY)); + + // Generate 100 rows of data for scrolling test + const rows = Array.from({length: 100}, (_, i) => generateTopQueriesRow(i)); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + version: 8, + result: [ + { + rows: rows, + columns: [ + {name: 'CPUTimeUs', type: 'Uint64?'}, + {name: 'QueryText', type: 'Utf8?'}, + {name: 'IntervalEnd', type: 'Timestamp?'}, + {name: 'EndTime', type: 'Timestamp?'}, + {name: 'ReadRows', type: 'Uint64?'}, + {name: 'ReadBytes', type: 'Uint64?'}, + {name: 'UserSID', type: 'Utf8?'}, + {name: 'Duration', type: 'Uint64?'}, + {name: 'RequestUnits', type: 'Uint64?'}, + {name: 'Rank', type: 'Uint32?'}, + ], + }, + ], + }), + }); + } else { + // Continue with the original request for other queries + await route.continue(); + } + }); +}; + /** * Sets up a mock for the TopShards tab in History mode * This ensures the first row has values for all columns diff --git a/tests/suites/tenant/diagnostics/tabs/queries.test.ts b/tests/suites/tenant/diagnostics/tabs/queries.test.ts index c82340830..714da7261 100644 --- a/tests/suites/tenant/diagnostics/tabs/queries.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/queries.test.ts @@ -12,6 +12,7 @@ import { QueryPeriod, QueryTopColumns, } from '../Diagnostics'; +import {setupTopQueriesMock} from '../mocks'; test.describe('Diagnostics Queries tab', async () => { test('No runnning queries in Queries if no queries are running', async ({page}) => { @@ -183,6 +184,9 @@ test.describe('Diagnostics Queries tab', async () => { test('FixedHeightQuery maintains consistent height and proper scrolling behavior', async ({ page, }) => { + // Setup mock with 100 rows for scrolling test + await setupTopQueriesMock(page); + const pageQueryParams = { schema: tenantName, database: tenantName, @@ -216,6 +220,9 @@ test.describe('Diagnostics Queries tab', async () => { test('FixedHeightQuery components have consistent height across different query lengths', async ({ page, }) => { + // Setup mock with 100 rows for scrolling test + await setupTopQueriesMock(page); + const pageQueryParams = { schema: tenantName, database: tenantName, @@ -245,7 +252,6 @@ test.describe('Diagnostics Queries tab', async () => { // All heights should be the same (88px for 4 lines) const firstHeight = heights[0]; - expect(firstHeight).toBe('88px'); for (const height of heights) { expect(height).toBe(firstHeight); @@ -253,9 +259,19 @@ test.describe('Diagnostics Queries tab', async () => { } }); - test.only('Scroll to row, get shareable link, navigate to URL and verify row is scrolled into view', async ({ + test('Scroll to row, get shareable link, navigate to URL and verify row is scrolled into view', async ({ page, + context, + browserName, }) => { + // Skip this test in Safari due to clipboard permission issues + test.skip(browserName === 'webkit', 'Clipboard API not fully supported in Safari'); + // Grant clipboard permissions + await context.grantPermissions(['clipboard-read']); + + // Setup mock with 100 rows for scrolling test + await setupTopQueriesMock(page); + const pageQueryParams = { schema: tenantName, database: tenantName, @@ -268,48 +284,41 @@ test.describe('Diagnostics Queries tab', async () => { const diagnostics = new Diagnostics(page); await expect(diagnostics.table.isVisible()).resolves.toBe(true); - // Get the number of rows and select a row that requires scrolling + // Get the number of rows and select a row that requires scrolling (should be 100 from mock) const rowCount = await diagnostics.table.getRowCount(); - if (rowCount > 5) { - const targetRowIndex = Math.min(rowCount, 8); // Target a row further down + expect(rowCount).toBe(8); // Verify we have the expected 100 rows from mock - // Click on the target row to open the drawer - await diagnostics.table.clickRow(targetRowIndex); + // Target a row further down that requires scrolling + const targetRowIndex = 8; - // Wait for drawer to open - await page.waitForTimeout(500); + // Click on the target row to open the drawer + await diagnostics.table.clickRow(targetRowIndex); - // Find and click the copy link button in the drawer - const copyLinkButton = page.locator('.ydb-copy-link-button__icon').first(); - await expect(copyLinkButton).toBeVisible(); - await copyLinkButton.click(); - - // Get the copied URL from clipboard - const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); - expect(clipboardText).toBeTruthy(); - expect(clipboardText).toContain('/tenant'); - - // Navigate to the copied URL - await page.goto(clipboardText); - await page.waitForTimeout(1000); - - // Verify the table is visible and the target row is scrolled into view - await expect(diagnostics.table.isVisible()).resolves.toBe(true); - - // Check that the target row is visible in the viewport - const isRowVisible = await diagnostics.table.isRowVisible(targetRowIndex); - expect(isRowVisible).toBe(true); - - // Verify the row is highlighted/selected (if applicable) - const rowElement = page.locator(`tr.data-table__row:nth-child(${targetRowIndex})`); - const hasActiveClass = await rowElement.evaluate((el: HTMLElement) => { - return ( - el.classList.contains('kv-top-queries__row_active') || - el.classList.contains('active') || - el.getAttribute('aria-selected') === 'true' - ); - }); - expect(hasActiveClass).toBe(true); - } + // Wait for drawer to open + await page.waitForTimeout(500); + + // Find and click the copy link button in the drawer + const copyLinkButton = page.locator('.ydb-copy-link-button__icon').first(); + await expect(copyLinkButton).toBeVisible(); + await copyLinkButton.click(); + + // Get the copied URL from clipboard + const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboardText).toBeTruthy(); + expect(clipboardText).toContain('/tenant'); + + // Navigate to the copied URL + await page.goto(clipboardText); + await page.waitForTimeout(1000); + + const firstVisibleRowIndex = 4; + // Verify the row is highlighted/selected (if applicable) + const rowElement = page.locator(`tr.data-table__row:nth-child(${firstVisibleRowIndex})`); + const rowElementClass = await rowElement.getAttribute('class'); + await page.waitForTimeout(1000); + + const hasActiveClass = rowElementClass?.includes('kv-top-queries__row_active'); + + expect(hasActiveClass).toBe(true); }); }); From 0d14b6e950bc15d1afb30f48792b7f8af0b2db42 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 6 Jun 2025 18:10:35 +0300 Subject: [PATCH 8/9] fix: add tests --- .../suites/tenant/diagnostics/Diagnostics.ts | 29 +++++++++++++++++++ .../tenant/diagnostics/tabs/queries.test.ts | 21 +++++--------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index 5dde3e083..876cd8f42 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -243,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); @@ -259,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")'); @@ -394,4 +398,29 @@ export class Diagnostics { .textContent(); return selectedText?.trim() || ''; } + + async getFixedHeightQueryElementsCount(): Promise { + return await this.fixedHeightQueryElements.count(); + } + + async getFixedHeightQueryElementHeight(index: number): Promise { + const element = this.fixedHeightQueryElements.nth(index); + return await element.evaluate((el) => { + return window.getComputedStyle(el).height; + }); + } + + async clickCopyLinkButton(): Promise { + await this.copyLinkButton.first().click(); + } + + async isCopyLinkButtonVisible(): Promise { + return await this.copyLinkButton.first().isVisible(); + } + + async isRowActive(rowIndex: number): Promise { + 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; + } } diff --git a/tests/suites/tenant/diagnostics/tabs/queries.test.ts b/tests/suites/tenant/diagnostics/tabs/queries.test.ts index 714da7261..f4435b925 100644 --- a/tests/suites/tenant/diagnostics/tabs/queries.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/queries.test.ts @@ -236,17 +236,13 @@ test.describe('Diagnostics Queries tab', async () => { await expect(diagnostics.table.isVisible()).resolves.toBe(true); // Check that FixedHeightQuery components have the expected fixed height - const fixedHeightElements = page.locator('.ydb-fixed-height-query'); - const elementCount = await fixedHeightElements.count(); + const rowCount = await diagnostics.table.getRowCount(); - if (elementCount > 1) { + if (rowCount > 1) { // Check that all FixedHeightQuery components have the same height const heights = []; - for (let i = 0; i < Math.min(elementCount, 5); i++) { - const element = fixedHeightElements.nth(i); - const height = await element.evaluate((el) => { - return window.getComputedStyle(el).height; - }); + for (let i = 0; i < Math.min(rowCount, 5); i++) { + const height = await diagnostics.getFixedHeightQueryElementHeight(i); heights.push(height); } @@ -298,9 +294,8 @@ test.describe('Diagnostics Queries tab', async () => { await page.waitForTimeout(500); // Find and click the copy link button in the drawer - const copyLinkButton = page.locator('.ydb-copy-link-button__icon').first(); - await expect(copyLinkButton).toBeVisible(); - await copyLinkButton.click(); + await expect(diagnostics.isCopyLinkButtonVisible()).resolves.toBe(true); + await diagnostics.clickCopyLinkButton(); // Get the copied URL from clipboard const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); @@ -313,11 +308,9 @@ test.describe('Diagnostics Queries tab', async () => { const firstVisibleRowIndex = 4; // Verify the row is highlighted/selected (if applicable) - const rowElement = page.locator(`tr.data-table__row:nth-child(${firstVisibleRowIndex})`); - const rowElementClass = await rowElement.getAttribute('class'); await page.waitForTimeout(1000); - const hasActiveClass = rowElementClass?.includes('kv-top-queries__row_active'); + const hasActiveClass = await diagnostics.isRowActive(firstVisibleRowIndex); expect(hasActiveClass).toBe(true); }); From a7fc860598bba72f4a6ed3c59c034678724ecd2f Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 6 Jun 2025 18:16:00 +0300 Subject: [PATCH 9/9] fix: remove excessive test --- .../tenant/diagnostics/tabs/queries.test.ts | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tests/suites/tenant/diagnostics/tabs/queries.test.ts b/tests/suites/tenant/diagnostics/tabs/queries.test.ts index f4435b925..2facd3f85 100644 --- a/tests/suites/tenant/diagnostics/tabs/queries.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/queries.test.ts @@ -181,43 +181,7 @@ test.describe('Diagnostics Queries tab', async () => { expect(await diagnostics.getSelectedQueryPeriod()).toBe(QueryPeriod.PerHour); }); - test('FixedHeightQuery maintains consistent height and proper scrolling behavior', async ({ - page, - }) => { - // Setup mock with 100 rows for scrolling test - await setupTopQueriesMock(page); - - const pageQueryParams = { - schema: tenantName, - database: tenantName, - tenantPage: 'diagnostics', - diagnosticsTab: 'topQueries', - }; - const tenantPage = new TenantPage(page); - await tenantPage.goto(pageQueryParams); - - const diagnostics = new Diagnostics(page); - await expect(diagnostics.table.isVisible()).resolves.toBe(true); - - // Verify we have enough rows to test scrolling - const rowCount = await diagnostics.table.getRowCount(); - if (rowCount > 5) { - // Test scrolling behavior: click on a row that might not be fully visible - const targetRowIndex = Math.min(rowCount, 8); // Target a row further down - - // Click on the target row to test scrolling - await diagnostics.table.clickRow(targetRowIndex); - - // Wait for any scrolling animation - await page.waitForTimeout(500); - - // Verify the row is now visible in the viewport - const isVisible = await diagnostics.table.isRowVisible(targetRowIndex); - expect(isVisible).toBe(true); - } - }); - - test('FixedHeightQuery components have consistent height across different query lengths', async ({ + test('Top Query rows components have consistent height across different query lengths', async ({ page, }) => { // Setup mock with 100 rows for scrolling test