From e2f7a2557e4699f2737b7f660579fec47580f48a Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 28 Oct 2024 16:53:10 +0300 Subject: [PATCH] fix: very bad performance when scrolling paginated tables (#1513) --- .../PaginatedTable/PaginatedTable.scss | 6 +- .../PaginatedTable/PaginatedTable.tsx | 40 ++++---- src/components/PaginatedTable/TableChunk.tsx | 29 +++--- src/components/PaginatedTable/constants.ts | 2 +- .../PaginatedTable/useScrollBasedChunks.ts | 95 ++++++++++++------- src/components/PaginatedTable/utils.ts | 25 ----- src/components/PaginatedTable/utils.tsx | 57 +++++++++++ src/containers/Node/Node.tsx | 4 +- src/containers/Nodes/NodesWrapper.tsx | 2 +- src/containers/Nodes/PaginatedNodes.tsx | 2 +- src/containers/PDiskPage/PDiskPage.tsx | 5 +- src/containers/Storage/PaginatedStorage.tsx | 2 +- .../Storage/PaginatedStorageGroups.tsx | 3 +- .../Storage/PaginatedStorageNodes.tsx | 2 + .../PaginatedStorageGroupsTable.tsx | 2 +- .../PaginatedStorageNodesTable.tsx | 2 +- src/containers/Storage/StorageWrapper.tsx | 2 +- .../StorageGroupPage/StorageGroupPage.tsx | 5 +- src/containers/VDiskPage/VDiskPage.tsx | 4 +- src/services/settings.ts | 2 +- tests/suites/nodes/NodesPage.ts | 3 +- tests/suites/storage/StoragePage.ts | 3 +- 22 files changed, 178 insertions(+), 119 deletions(-) delete mode 100644 src/components/PaginatedTable/utils.ts create mode 100644 src/components/PaginatedTable/utils.tsx diff --git a/src/components/PaginatedTable/PaginatedTable.scss b/src/components/PaginatedTable/PaginatedTable.scss index 707d21a2b..a9060b723 100644 --- a/src/components/PaginatedTable/PaginatedTable.scss +++ b/src/components/PaginatedTable/PaginatedTable.scss @@ -29,6 +29,10 @@ position: relative; z-index: 1; + // Performance optimization for row hovering. + // it actually works. + transform: translateZ(0); + &:hover { background: var(--paginated-table-hover-color); } @@ -177,6 +181,6 @@ } &__row-skeleton::after { - animation-delay: 200ms; + animation: none !important; } } diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index f874d4a55..ad794127f 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import {getArray} from '../../utils'; import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout'; import {TableChunk} from './TableChunk'; @@ -32,7 +31,7 @@ export interface PaginatedTableProps { columns: Column[]; getRowClassName?: GetRowClassName; rowHeight?: number; - parentRef?: React.RefObject; + parentRef: React.RefObject; initialSortParams?: SortParams; onColumnsResize?: HandleTableColumnsResize; renderControls?: RenderControls; @@ -42,7 +41,7 @@ export interface PaginatedTableProps { } export const PaginatedTable = ({ - limit, + limit: chunkSize, initialEntitiesCount, fetchData, filters, @@ -58,8 +57,8 @@ export const PaginatedTable = ({ renderEmptyDataMessage, containerClassName, }: PaginatedTableProps) => { - const initialTotal = initialEntitiesCount || limit; - const initialFound = initialEntitiesCount || 0; + const initialTotal = initialEntitiesCount || 0; + const initialFound = initialEntitiesCount || 1; const [sortParams, setSortParams] = React.useState(initialSortParams); const [totalEntities, setTotalEntities] = React.useState(initialTotal); @@ -69,12 +68,18 @@ export const PaginatedTable = ({ const tableRef = React.useRef(null); const activeChunks = useScrollBasedChunks({ - containerRef: parentRef ?? tableRef, + parentRef, + tableRef, totalItems: foundEntities, - itemHeight: rowHeight, - chunkSize: limit, + rowHeight, + chunkSize, }); + const lastChunkSize = React.useMemo( + () => foundEntities % chunkSize || chunkSize, + [foundEntities, chunkSize], + ); + const handleDataFetched = React.useCallback((total: number, found: number) => { setTotalEntities(total); setFoundEntities(found); @@ -88,10 +93,8 @@ export const PaginatedTable = ({ setIsInitialLoad(true); if (parentRef?.current) { parentRef.current.scrollTo(0, 0); - } else { - tableRef.current?.scrollTo(0, 0); } - }, [filters, initialFound, initialTotal, limit, parentRef]); + }, [filters, initialFound, initialTotal, parentRef]); const renderChunks = () => { if (!isInitialLoad && foundEntities === 0) { @@ -104,15 +107,12 @@ export const PaginatedTable = ({ ); } - const totalLength = foundEntities || limit; - const chunksCount = Math.ceil(totalLength / limit); - - return getArray(chunksCount).map((value) => ( + return activeChunks.map((isActive, index) => ( - key={value} - id={value} - limit={limit} - totalLength={totalLength} + key={index} + id={index} + calculatedCount={index === activeChunks.length - 1 ? lastChunkSize : chunkSize} + chunkSize={chunkSize} rowHeight={rowHeight} columns={columns} fetchData={fetchData} @@ -122,7 +122,7 @@ export const PaginatedTable = ({ getRowClassName={getRowClassName} renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} - isActive={activeChunks.includes(value)} + isActive={isActive} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index 75fc2a5c2..347c11025 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -8,14 +8,15 @@ import {ResponseError} from '../Errors/ResponseError'; import {EmptyTableRow, LoadingTableRow, TableRow} from './TableRow'; import type {Column, FetchData, GetRowClassName, SortParams} from './types'; +import {typedMemo} from './utils'; const DEBOUNCE_TIMEOUT = 200; interface TableChunkProps { id: number; - limit: number; - totalLength: number; + chunkSize: number; rowHeight: number; + calculatedCount: number; columns: Column[]; filters?: F; sortParams?: SortParams; @@ -29,10 +30,10 @@ interface TableChunkProps { } // Memoisation prevents chunks rerenders that could cause perfomance issues on big tables -export const TableChunk = ({ +export const TableChunk = typedMemo(function TableChunk({ id, - limit, - totalLength, + chunkSize, + calculatedCount, rowHeight, columns, fetchData, @@ -43,15 +44,15 @@ export const TableChunk = ({ renderErrorMessage, onDataFetched, isActive, -}: TableChunkProps) => { +}: TableChunkProps) { const [isTimeoutActive, setIsTimeoutActive] = React.useState(true); const [autoRefreshInterval] = useAutoRefreshInterval(); const columnsIds = columns.map((column) => column.name); const queryParams = { - offset: id * limit, - limit, + offset: id * chunkSize, + limit: chunkSize, fetchData: fetchData as FetchData, filters, sortParams, @@ -87,11 +88,7 @@ export const TableChunk = ({ } }, [currentData, isActive, onDataFetched]); - const chunkOffset = id * limit; - const remainingLength = totalLength - chunkOffset; - const calculatedChunkLength = remainingLength < limit ? remainingLength : limit; - - const dataLength = currentData?.data?.length || calculatedChunkLength; + const dataLength = currentData?.data?.length || calculatedCount; const renderContent = () => { if (!isActive) { @@ -134,13 +131,11 @@ export const TableChunk = ({ )); }; - const chunkHeight = dataLength ? dataLength * rowHeight : limit * rowHeight; - return ( ({ {renderContent()} ); -}; +}); diff --git a/src/components/PaginatedTable/constants.ts b/src/components/PaginatedTable/constants.ts index 03dfbc058..355d65933 100644 --- a/src/components/PaginatedTable/constants.ts +++ b/src/components/PaginatedTable/constants.ts @@ -13,6 +13,6 @@ export const DEFAULT_SORT_ORDER = DESCENDING; // Time in ms after which request will be sent export const DEFAULT_REQUEST_TIMEOUT = 200; -export const DEFAULT_TABLE_ROW_HEIGHT = 40; +export const DEFAULT_TABLE_ROW_HEIGHT = 41; export const DEFAULT_INTERSECTION_OBSERVER_MARGIN = '100%'; diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 4b96812e3..e44dc1768 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -2,68 +2,91 @@ import React from 'react'; import {throttle} from 'lodash'; -import {getArray} from '../../utils'; +import {calculateElementOffsetTop} from './utils'; interface UseScrollBasedChunksProps { - containerRef: React.RefObject; + parentRef: React.RefObject; + tableRef: React.RefObject; totalItems: number; - itemHeight: number; + rowHeight: number; chunkSize: number; + overscanCount?: number; } +const DEFAULT_OVERSCAN_COUNT = 1; const THROTTLE_DELAY = 100; -const CHUNKS_AHEAD_COUNT = 1; export const useScrollBasedChunks = ({ - containerRef, + parentRef, + tableRef, totalItems, - itemHeight, + rowHeight, chunkSize, -}: UseScrollBasedChunksProps): number[] => { - const [activeChunks, setActiveChunks] = React.useState( - getArray(1 + CHUNKS_AHEAD_COUNT).map((index) => index), + overscanCount = DEFAULT_OVERSCAN_COUNT, +}: UseScrollBasedChunksProps): boolean[] => { + const chunksCount = React.useMemo( + () => Math.ceil(totalItems / chunkSize), + [chunkSize, totalItems], ); - const calculateActiveChunks = React.useCallback(() => { - const container = containerRef.current; - if (!container) { - return; - } + const [startChunk, setStartChunk] = React.useState(0); + const [endChunk, setEndChunk] = React.useState( + Math.min(overscanCount, Math.max(chunksCount - 1, 0)), + ); - const {scrollTop, clientHeight} = container; - const visibleStartIndex = Math.floor(scrollTop / itemHeight); - const visibleEndIndex = Math.min( - Math.ceil((scrollTop + clientHeight) / itemHeight), - totalItems - 1, - ); + const calculateVisibleRange = React.useCallback(() => { + const container = parentRef?.current; + const table = tableRef.current; + if (!container || !table) { + return null; + } - const startChunk = Math.floor(visibleStartIndex / chunkSize); - const endChunk = Math.floor(visibleEndIndex / chunkSize); + const tableOffset = calculateElementOffsetTop(table, container); + const containerScroll = container.scrollTop; + const visibleStart = Math.max(containerScroll - tableOffset, 0); + const visibleEnd = visibleStart + container.clientHeight; - const newActiveChunks = getArray(endChunk - startChunk + 1 + CHUNKS_AHEAD_COUNT).map( - (index) => startChunk + index, + const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0); + const end = Math.min( + Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount, + Math.max(chunksCount - 1, 0), ); - setActiveChunks(newActiveChunks); - }, [chunkSize, containerRef, itemHeight, totalItems]); + return {start, end}; + }, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]); - const throttledCalculateActiveChunks = React.useMemo( - () => throttle(calculateActiveChunks, THROTTLE_DELAY), - [calculateActiveChunks], - ); + const handleScroll = React.useCallback(() => { + const newRange = calculateVisibleRange(); + if (newRange) { + setStartChunk(newRange.start); + setEndChunk(newRange.end); + } + }, [calculateVisibleRange]); React.useEffect(() => { - const container = containerRef.current; + const container = parentRef?.current; if (!container) { return undefined; } - container.addEventListener('scroll', throttledCalculateActiveChunks); + const throttledHandleScroll = throttle(handleScroll, THROTTLE_DELAY, { + leading: true, + trailing: true, + }); + + container.addEventListener('scroll', throttledHandleScroll); return () => { - container.removeEventListener('scroll', throttledCalculateActiveChunks); - throttledCalculateActiveChunks.cancel(); + container.removeEventListener('scroll', throttledHandleScroll); + throttledHandleScroll.cancel(); }; - }, [containerRef, throttledCalculateActiveChunks]); + }, [handleScroll, parentRef]); - return activeChunks; + return React.useMemo(() => { + // boolean array that represents active chunks + const activeChunks = Array(chunksCount).fill(false); + for (let i = startChunk; i <= endChunk; i++) { + activeChunks[i] = true; + } + return activeChunks; + }, [chunksCount, startChunk, endChunk]); }; diff --git a/src/components/PaginatedTable/utils.ts b/src/components/PaginatedTable/utils.ts deleted file mode 100644 index e546d8e3e..000000000 --- a/src/components/PaginatedTable/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -// invoke passed function at most once per animation frame -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function rafThrottle any>(fn: Fn) { - let rafId: number | null = null; - let latestArgs: Parameters; - - return function throttled(...args: Parameters) { - // call throttled function with latest args - latestArgs = args; - - if (typeof rafId === 'number') { - return; - } - - rafId = requestAnimationFrame(() => { - fn(...latestArgs); - rafId = null; - }); - }; -} - -// 40px minWidth so sort icon won't overlap wrapped column title -export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = Infinity) { - return Math.max(minWidth, Math.min(newWidth, maxWidth)); -} diff --git a/src/components/PaginatedTable/utils.tsx b/src/components/PaginatedTable/utils.tsx new file mode 100644 index 000000000..f272ae159 --- /dev/null +++ b/src/components/PaginatedTable/utils.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +// invoke passed function at most once per animation frame +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function rafThrottle any>(fn: Fn) { + let rafId: number | null = null; + let latestArgs: Parameters; + + return function throttled(...args: Parameters) { + // call throttled function with latest args + latestArgs = args; + + if (typeof rafId === 'number') { + return; + } + + rafId = requestAnimationFrame(() => { + fn(...latestArgs); + rafId = null; + }); + }; +} + +// 40px minWidth so sort icon won't overlap wrapped column title +export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = Infinity) { + return Math.max(minWidth, Math.min(newWidth, maxWidth)); +} + +export const typedMemo: (Component: T) => T = React.memo; + +/** + * Calculates the total vertical offset (distance from top) of an element relative to its container + * or the document body if no container is specified. + * + * This function traverses up through the DOM tree, accumulating offsetTop values + * from each parent element until it reaches either the specified container or + * the top of the document. + * + * @param element - The HTML element to calculate the offset for + * @param container - Optional container element to stop the calculation at + * @returns The total vertical offset in pixels + * + * Example: + * const offset = calculateElementOffsetTop(myElement, myContainer); + * // Returns the distance in pixels from myElement to the top of myContainer + */ +export function calculateElementOffsetTop(element: HTMLElement, container?: HTMLElement): number { + let currentElement = element; + let offsetTop = 0; + + while (currentElement && currentElement !== container) { + offsetTop += currentElement.offsetTop; + currentElement = currentElement.offsetParent as HTMLElement; + } + + return offsetTop; +} diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 35b237e1b..7424d777c 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -126,7 +126,7 @@ export function Node(props: NodeProps) { switch (activeTabVerified.id) { case STORAGE: { return ( -
+
); @@ -159,7 +159,7 @@ export function Node(props: NodeProps) { if (node) { return ( -
+
; + parentRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/Nodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes.tsx index 105552126..a0966ede0 100644 --- a/src/containers/Nodes/PaginatedNodes.tsx +++ b/src/containers/Nodes/PaginatedNodes.tsx @@ -44,7 +44,7 @@ const b = cn('ydb-nodes'); interface NodesProps { path?: string; database?: string; - parentRef?: React.RefObject; + parentRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index d07035b0d..435a5b7be 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -63,6 +63,7 @@ export function PDiskPage() { const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const newDiskApiAvailable = useDiskPagesAvailable(); + const containerRef = React.useRef(null); const [{nodeId, pDiskId, activeTab}] = useQueryParams({ activeTab: StringParam, @@ -245,7 +246,7 @@ export function PDiskPage() { } case 'storage': { return pDiskParamsDefined ? ( - + ) : null; } default: @@ -261,7 +262,7 @@ export function PDiskPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} diff --git a/src/containers/Storage/PaginatedStorage.tsx b/src/containers/Storage/PaginatedStorage.tsx index 6a01ce895..8ec205bc5 100644 --- a/src/containers/Storage/PaginatedStorage.tsx +++ b/src/containers/Storage/PaginatedStorage.tsx @@ -12,7 +12,7 @@ export interface PaginatedStorageProps { viewContext: StorageViewContext; - parentRef?: React.RefObject; + parentRef: React.RefObject; initialEntitiesCount?: number; } diff --git a/src/containers/Storage/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups.tsx index 03735f3e1..b4dc1048c 100644 --- a/src/containers/Storage/PaginatedStorageGroups.tsx +++ b/src/containers/Storage/PaginatedStorageGroups.tsx @@ -104,7 +104,7 @@ function GroupedStorageGroupsComponent({ nodeId, groupId, pDiskId, - + parentRef, viewContext, }: PaginatedStorageProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -167,6 +167,7 @@ function GroupedStorageGroupsComponent({ > ; + parentRef: React.RefObject; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx index 44985f5d6..be768a838 100644 --- a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx +++ b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx @@ -29,7 +29,7 @@ interface PaginatedStorageNodesTableProps { nodesUptimeFilter: NodesUptimeFilterValues; onShowAll: VoidFunction; - parentRef?: React.RefObject; + parentRef: React.RefObject; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageWrapper.tsx b/src/containers/Storage/StorageWrapper.tsx index d2c5a264e..5b1b7120b 100644 --- a/src/containers/Storage/StorageWrapper.tsx +++ b/src/containers/Storage/StorageWrapper.tsx @@ -11,7 +11,7 @@ interface StorageWrapperProps { pDiskId?: string | number; groupId?: string | number; vDiskSlotId?: string | number; - parentRef?: React.RefObject; + parentRef: React.RefObject; } export const StorageWrapper = ({parentRef, ...props}: StorageWrapperProps) => { diff --git a/src/containers/StorageGroupPage/StorageGroupPage.tsx b/src/containers/StorageGroupPage/StorageGroupPage.tsx index 752b5eb72..68058f4ea 100644 --- a/src/containers/StorageGroupPage/StorageGroupPage.tsx +++ b/src/containers/StorageGroupPage/StorageGroupPage.tsx @@ -29,6 +29,7 @@ const storageGroupPageCn = cn('ydb-storage-group-page'); export function StorageGroupPage() { const dispatch = useTypedDispatch(); + const containerRef = React.useRef(null); const [{groupId}] = useQueryParams({groupId: StringParam}); @@ -109,7 +110,7 @@ export function StorageGroupPage() {
{storageGroupPageKeyset('storage')}
- + ); }; @@ -122,7 +123,7 @@ export function StorageGroupPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index ab6249cd6..f98d4e341 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -32,6 +32,7 @@ const vDiskPageCn = cn('ydb-vdisk-page'); export function VDiskPage() { const dispatch = useTypedDispatch(); + const containerRef = React.useRef(null); const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const newDiskApiAvailable = useDiskPagesAvailable(); @@ -182,6 +183,7 @@ export function VDiskPage() { nodeId={nodeId} pDiskId={pDiskId ?? undefined} vDiskSlotId={vDiskSlotId ?? undefined} + parentRef={containerRef} /> ); @@ -205,7 +207,7 @@ export function VDiskPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} diff --git a/src/services/settings.ts b/src/services/settings.ts index 978a26f52..069583036 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -36,7 +36,7 @@ export const DEFAULT_USER_SETTINGS = { [LAST_USED_QUERY_ACTION_KEY]: QUERY_ACTIONS.execute, [ASIDE_HEADER_COMPACT_KEY]: true, [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], - [USE_PAGINATED_TABLES_KEY]: false, + [USE_PAGINATED_TABLES_KEY]: true, [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, [ENABLE_AUTOCOMPLETE]: true, [AUTOCOMPLETE_ON_ENTER]: true, diff --git a/tests/suites/nodes/NodesPage.ts b/tests/suites/nodes/NodesPage.ts index f163e3757..e81119d96 100644 --- a/tests/suites/nodes/NodesPage.ts +++ b/tests/suites/nodes/NodesPage.ts @@ -2,7 +2,6 @@ import type {Locator, Page} from '@playwright/test'; import {PageModel} from '../../models/PageModel'; import {nodesPage} from '../../utils/constants'; -import {selectContentTable} from '../../utils/selectContentTable'; export class NodesPage extends PageModel { readonly table: Locator; @@ -10,6 +9,6 @@ export class NodesPage extends PageModel { constructor(page: Page) { super(page, nodesPage); - this.table = selectContentTable(this.selector); + this.table = this.selector.locator('.ydb-paginated-table__table'); } } diff --git a/tests/suites/storage/StoragePage.ts b/tests/suites/storage/StoragePage.ts index 99e63df80..4ea8b3c14 100644 --- a/tests/suites/storage/StoragePage.ts +++ b/tests/suites/storage/StoragePage.ts @@ -2,7 +2,6 @@ import type {Locator, Page} from '@playwright/test'; import {PageModel} from '../../models/PageModel'; import {storagePage} from '../../utils/constants'; -import {selectContentTable} from '../../utils/selectContentTable'; type EntityType = 'Groups' | 'Nodes'; @@ -15,7 +14,7 @@ export class StoragePage extends PageModel { constructor(page: Page) { super(page, storagePage); - this.table = selectContentTable(this.selector); + this.table = this.selector.locator('.ydb-paginated-table__table'); this.entityTypeSelector = this.selector.getByTestId(storageTypeFilterQa); }