(USE_PAGINATED_TABLES_KEY);
-
- const viewContext: StorageViewContext = {
- nodeId: props.nodeId?.toString(),
- pDiskId: props.pDiskId?.toString(),
- groupId: props.groupId?.toString(),
- vDiskSlotId: props.vDiskSlotId?.toString(),
- };
-
- if (usePaginatedTables) {
- return ;
- }
-
- return ;
-};
diff --git a/src/containers/Storage/utils/index.ts b/src/containers/Storage/utils/index.ts
index 7606288d3..52e448eba 100644
--- a/src/containers/Storage/utils/index.ts
+++ b/src/containers/Storage/utils/index.ts
@@ -89,12 +89,14 @@ const DEFAULT_ENTITIES_COUNT = 10;
// GroupPage - DEFAULT_ENTITIES_COUNT nodes
// PDiskPage - 1 node
// VDiskPage - 1 node
-export function getStorageNodesInitialEntitiesCount({
- nodeId,
- pDiskId,
- vDiskSlotId,
-}: StorageViewContext): number | undefined {
- if (valueIsDefined(nodeId) || valueIsDefined(pDiskId) || valueIsDefined(vDiskSlotId)) {
+export function getStorageNodesInitialEntitiesCount(
+ context?: StorageViewContext,
+): number | undefined {
+ if (
+ valueIsDefined(context?.nodeId) ||
+ valueIsDefined(context?.pDiskId) ||
+ valueIsDefined(context?.vDiskSlotId)
+ ) {
return 1;
}
@@ -105,14 +107,13 @@ export function getStorageNodesInitialEntitiesCount({
// GroupPage - 1 group
// PDiskPage - DEFAULT_ENTITIES_COUNT groups
// VDiskPage - 1 group
-export function getStorageGroupsInitialEntitiesCount({
- vDiskSlotId,
- groupId,
-}: StorageViewContext): number | undefined {
- if (valueIsDefined(groupId)) {
+export function getStorageGroupsInitialEntitiesCount(
+ context?: StorageViewContext,
+): number | undefined {
+ if (valueIsDefined(context?.groupId)) {
return 1;
}
- if (valueIsDefined(vDiskSlotId)) {
+ if (valueIsDefined(context?.vDiskSlotId)) {
return 1;
}
diff --git a/src/containers/StorageGroupPage/StorageGroupPage.tsx b/src/containers/StorageGroupPage/StorageGroupPage.tsx
index 68058f4ea..edfc9cb66 100644
--- a/src/containers/StorageGroupPage/StorageGroupPage.tsx
+++ b/src/containers/StorageGroupPage/StorageGroupPage.tsx
@@ -19,7 +19,7 @@ import {EFlag} from '../../types/api/enums';
import {valueIsDefined} from '../../utils';
import {cn} from '../../utils/cn';
import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks';
-import {StorageWrapper} from '../Storage/StorageWrapper';
+import {PaginatedStorage} from '../Storage/PaginatedStorage';
import {storageGroupPageKeyset} from './i18n';
@@ -110,7 +110,13 @@ export function StorageGroupPage() {
{storageGroupPageKeyset('storage')}
-
+
);
};
diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx
index 75a32bb13..98cddb400 100644
--- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx
+++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx
@@ -16,7 +16,7 @@ import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
import {Heatmap} from '../../Heatmap';
import {Nodes} from '../../Nodes/Nodes';
import {Operations} from '../../Operations';
-import {StorageWrapper} from '../../Storage/StorageWrapper';
+import {PaginatedStorage} from '../../Storage/PaginatedStorage';
import {Tablets} from '../../Tablets';
import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer';
import {TenantTabsGroups, getTenantPath} from '../TenantPages';
@@ -114,7 +114,7 @@ function Diagnostics(props: DiagnosticsProps) {
return ;
}
case TENANT_DIAGNOSTICS_TABS_IDS.storage: {
- return ;
+ return ;
}
case TENANT_DIAGNOSTICS_TABS_IDS.network: {
return (
diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json
index 86f8e52dc..541c692b5 100644
--- a/src/containers/UserSettings/i18n/en.json
+++ b/src/containers/UserSettings/i18n/en.json
@@ -12,7 +12,7 @@
"section.about": "About",
"settings.editor.autocomplete.title": "Enable autocomplete",
- "settings.editor.autocomplete.description": "You’re always able to get suggestions by pressing Ctrl+Space.",
+ "settings.editor.autocomplete.description": "You're always able to get suggestions by pressing Ctrl+Space.",
"settings.editor.autocomplete-on-enter.title": "Accept suggestion on Enter",
"settings.editor.autocomplete-on-enter.description": "Controls whether suggestions should be accepted on Enter, in addition to Tab. Helps to avoid ambiguity between inserting new lines or accepting suggestions.",
@@ -30,9 +30,6 @@
"settings.invertedDisks.title": "Inverted disks space indicators",
- "settings.usePaginatedTables.title": "Use paginated tables",
- "settings.usePaginatedTables.description": " Use table with data load on scroll for Nodes and Storage tabs. It will increase performance, but could work unstable",
-
"settings.enableNetworkTable.title": "Enable network table",
"settings.useShowPlanToSvg.title": "Execution plan",
diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx
index e23e12c92..eccf42e40 100644
--- a/src/containers/UserSettings/settings.tsx
+++ b/src/containers/UserSettings/settings.tsx
@@ -12,7 +12,6 @@ import {
SHOW_DOMAIN_DATABASE_KEY,
THEME_KEY,
USE_CLUSTER_BALANCER_AS_BACKEND_KEY,
- USE_PAGINATED_TABLES_KEY,
USE_SHOW_PLAN_SVG_KEY,
} from '../../utils/constants';
import {Lang, defaultLang} from '../../utils/i18n';
@@ -92,11 +91,6 @@ export const invertedDisksSetting: SettingProps = {
settingKey: INVERTED_DISKS_KEY,
title: i18n('settings.invertedDisks.title'),
};
-export const usePaginatedTables: SettingProps = {
- settingKey: USE_PAGINATED_TABLES_KEY,
- title: i18n('settings.usePaginatedTables.title'),
- description: i18n('settings.usePaginatedTables.description'),
-};
export const enableNetworkTable: SettingProps = {
settingKey: ENABLE_NETWORK_TABLE_KEY,
@@ -148,11 +142,13 @@ export const appearanceSection: SettingsSection = {
showDomainDatabase,
],
};
+
export const experimentsSection: SettingsSection = {
id: 'experimentsSection',
title: i18n('section.experiments'),
- settings: [usePaginatedTables, enableNetworkTable, useShowPlanToSvgTables],
+ settings: [enableNetworkTable, useShowPlanToSvgTables],
};
+
export const devSettingsSection: SettingsSection = {
id: 'devSettingsSection',
title: i18n('section.dev-setting'),
@@ -172,6 +168,7 @@ export const generalPage: SettingsPage = {
sections: [appearanceSection],
showTitle: false,
};
+
export const experimentsPage: SettingsPage = {
id: 'experimentsPage',
title: i18n('page.experiments'),
@@ -179,6 +176,7 @@ export const experimentsPage: SettingsPage = {
sections: [experimentsSection],
showTitle: false,
};
+
export const editorPage: SettingsPage = {
id: 'editorPage',
title: i18n('page.editor'),
diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx
index 3e5de71b8..49591f284 100644
--- a/src/containers/VDiskPage/VDiskPage.tsx
+++ b/src/containers/VDiskPage/VDiskPage.tsx
@@ -21,7 +21,7 @@ import {valueIsDefined} from '../../utils';
import {cn} from '../../utils/cn';
import {getSeverityColor, getVDiskSlotBasedId} from '../../utils/disks/helpers';
import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks';
-import {StorageWrapper} from '../Storage/StorageWrapper';
+import {PaginatedStorage} from '../Storage/PaginatedStorage';
import {vDiskPageKeyset} from './i18n';
@@ -180,12 +180,17 @@ export function VDiskPage() {
return (
{vDiskPageKeyset('storage')}
-
);
diff --git a/src/services/settings.ts b/src/services/settings.ts
index 5485ed177..2f8855888 100644
--- a/src/services/settings.ts
+++ b/src/services/settings.ts
@@ -20,7 +20,6 @@ import {
TENANT_INITIAL_PAGE_KEY,
THEME_KEY,
USE_CLUSTER_BALANCER_AS_BACKEND_KEY,
- USE_PAGINATED_TABLES_KEY,
USE_SHOW_PLAN_SVG_KEY,
} from '../utils/constants';
import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../utils/query';
@@ -39,7 +38,6 @@ 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]: true,
[ENABLE_NETWORK_TABLE_KEY]: false,
[USE_SHOW_PLAN_SVG_KEY]: false,
[USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true,
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 5bfacc13f..682a08c72 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -132,10 +132,6 @@ export const PARTITIONS_HIDDEN_COLUMNS_KEY = 'partitionsHiddenColumns';
// Remain "tab" in key name for backward compatibility
export const TENANT_INITIAL_PAGE_KEY = 'saved_tenant_initial_tab';
-// Setting to use paginated tables
-// Old key value for backward compatibility
-export const USE_PAGINATED_TABLES_KEY = 'useBackendParamsForTables';
-
export const ENABLE_NETWORK_TABLE_KEY = 'enableNetworkTable';
export const USE_SHOW_PLAN_SVG_KEY = 'useShowPlanToSvg';
diff --git a/tests/suites/memoryViewer/memoryViewer.test.ts b/tests/suites/memoryViewer/memoryViewer.test.ts
index 1a8c3ceac..e02088608 100644
--- a/tests/suites/memoryViewer/memoryViewer.test.ts
+++ b/tests/suites/memoryViewer/memoryViewer.test.ts
@@ -1,7 +1,7 @@
import {expect, test} from '@playwright/test';
import {NodesPage} from '../nodes/NodesPage';
-import {PaginatedTable} from '../paginatedTable/paginatedTable';
+import {ClusterNodesTable} from '../paginatedTable/paginatedTable';
import {MemoryViewer} from './MemoryViewer';
@@ -11,13 +11,14 @@ test.describe('Memory Viewer Widget', () => {
const memoryViewer = new MemoryViewer(page);
await nodesPage.goto();
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableVisible();
await paginatedTable.waitForTableData();
if (!(await memoryViewer.isVisible())) {
- await paginatedTable.openColumnSetup();
- await paginatedTable.setColumnChecked('Memory');
- await paginatedTable.applyColumnVisibility();
+ const controls = paginatedTable.getControls();
+ await controls.openColumnSetup();
+ await controls.setColumnChecked('Memory');
+ await controls.applyColumnVisibility();
}
await memoryViewer.waitForVisible();
});
diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts
index 58086ed48..1084900cc 100644
--- a/tests/suites/nodes/nodes.test.ts
+++ b/tests/suites/nodes/nodes.test.ts
@@ -2,7 +2,7 @@ import {expect, test} from '@playwright/test';
import {backend} from '../../utils/constants';
import {NodesPage} from '../nodes/NodesPage';
-import {PaginatedTable} from '../paginatedTable/paginatedTable';
+import {ClusterNodesTable} from '../paginatedTable/paginatedTable';
test.describe('Test Nodes page', async () => {
test('Nodes page is OK', async ({page}) => {
@@ -30,7 +30,7 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Table loads and displays data', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -40,13 +40,13 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Search by hostname filters the table', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
const initialRowCount = await paginatedTable.getRowCount();
- await paginatedTable.search('localhost');
+ await paginatedTable.getControls().search('localhost');
await page.waitForTimeout(1000); // Wait for the table to update
@@ -56,7 +56,7 @@ test.describe('Test Nodes Paginated Table', async () => {
test('Table groups displayed correctly if group by option is selected', async ({page}) => {
const nodesPage = new NodesPage(page);
- const nodesTable = new PaginatedTable(page);
+ const nodesTable = new ClusterNodesTable(page);
await nodesTable.waitForTableToLoad();
await nodesTable.waitForTableData();
@@ -74,19 +74,19 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Node count is displayed correctly', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
- const nodeCount = await paginatedTable.getCount();
+ const nodeCount = await paginatedTable.getControls().getCount();
const rowCount = await paginatedTable.getRowCount();
expect(nodeCount).toBe(rowCount);
});
test('Uptime values are displayed in correct format', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -99,14 +99,14 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Refresh button updates the table data', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
const initialUptimeValues = await paginatedTable.getColumnValues('Uptime');
await page.waitForTimeout(2000); // Wait for some time to pass
- await paginatedTable.clickRefreshButton();
+ await paginatedTable.getControls().clickRefreshButton();
await paginatedTable.waitForTableData();
const updatedUptimeValues = await paginatedTable.getColumnValues('Uptime');
@@ -114,12 +114,12 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Row data can be retrieved correctly', async ({page}) => {
- const storageTable = new PaginatedTable(page);
+ const nodesTable = new ClusterNodesTable(page);
- await storageTable.waitForTableToLoad();
- await storageTable.waitForTableData();
+ await nodesTable.waitForTableToLoad();
+ await nodesTable.waitForTableData();
- const rowData = await storageTable.getRowData(0);
+ const rowData = await nodesTable.getRowData(0);
expect(rowData).toHaveProperty('Host');
expect(rowData).toHaveProperty('Uptime');
@@ -130,7 +130,7 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Column values can be retrieved correctly', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -144,12 +144,12 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Table displays empty data message when no entities', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
- await paginatedTable.search('Some Invalid search string !%#@[]');
+ await paginatedTable.getControls().search('Some Invalid search string !%#@[]');
await paginatedTable.waitForTableData();
@@ -158,19 +158,19 @@ test.describe('Test Nodes Paginated Table', async () => {
});
test('Autorefresh updates data when initially empty data', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterNodesTable(page);
const emptyRequest = page.route(`${backend}/viewer/json/nodes?*`, async (route) => {
await route.fulfill({json: {FoundNodes: 0, TotalNodes: 0, Nodes: []}});
});
- await paginatedTable.clickRefreshButton();
+ await paginatedTable.getControls().clickRefreshButton();
await emptyRequest;
const emptyDataMessage = await paginatedTable.getEmptyDataMessageLocator();
await expect(emptyDataMessage).toContainText('No such nodes');
- await paginatedTable.setRefreshInterval('15 sec');
+ await paginatedTable.getControls().setRefreshInterval('15 sec');
const requestWithData = page.route(`${backend}/viewer/json/nodes?*`, async (route) => {
await route.continue();
diff --git a/tests/suites/paginatedTable/mocks.ts b/tests/suites/paginatedTable/mocks.ts
new file mode 100644
index 000000000..cfafbe894
--- /dev/null
+++ b/tests/suites/paginatedTable/mocks.ts
@@ -0,0 +1,114 @@
+import type {Page} from '@playwright/test';
+
+import {backend} from '../../utils/constants';
+
+const MOCK_DELAY = 200; // 200ms delay to simulate network latency
+
+interface NodeMockOptions {
+ offset: number;
+ limit: number;
+}
+
+export const generateNodeMock = async ({offset, limit}: NodeMockOptions) => {
+ return Array.from({length: limit}, (_, i) => ({
+ NodeId: offset + i + 1,
+ SystemState: {
+ Host: `host-${offset + i}.test`,
+ DataCenter: `dc-${Math.floor((offset + i) / 10)}`,
+ Rack: `rack-${Math.floor((offset + i) / 5)}`,
+ Version: 'main.b7cfb36',
+ StartTime: (Date.now() - 4 * 60 * 60 * 1000).toString(), // 4 hours ago
+ LoadAverage: [0.1, 0.2, 0.3],
+ NumberOfCpus: 8,
+ SystemState: 'Green',
+ MemoryUsed: ((8 + (i % 4)) * 1024 * 1024 * 1024).toString(), // 8-12GB
+ MemoryLimit: (16 * 1024 * 1024 * 1024).toString(), // 16GB
+ TotalSessions: 100,
+ Tenants: ['local'],
+ },
+ CpuUsage: 10 + (i % 20),
+ UptimeSeconds: 4 * 60 * 60, // 4 hours
+ Disconnected: false,
+ Tablets: [
+ {
+ TabletId: `tablet-${i}-1`,
+ Type: 'DataShard',
+ State: 'Active',
+ Leader: true,
+ },
+ {
+ TabletId: `tablet-${i}-2`,
+ Type: 'DataShard',
+ State: 'Active',
+ Leader: false,
+ },
+ ],
+ }));
+};
+
+export const setupNodesMock = async (page: Page) => {
+ await page.route(`${backend}/viewer/json/nodes?*`, async (route) => {
+ const url = new URL(route.request().url());
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
+
+ const nodes = await generateNodeMock({offset, limit});
+
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ Overall: 'Green',
+ Nodes: nodes,
+ TotalNodes: '100',
+ FoundNodes: '100',
+ }),
+ });
+ });
+};
+
+export const setupEmptyNodesMock = async (page: Page) => {
+ await page.route(`${backend}/viewer/json/nodes?*`, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ Overall: 'Green',
+ Nodes: [],
+ TotalNodes: '0',
+ FoundNodes: '0',
+ }),
+ });
+ });
+};
+
+export const setupLargeNodesMock = async (page: Page, totalNodes = 1000) => {
+ await page.route(`${backend}/viewer/json/nodes?*`, async (route) => {
+ const url = new URL(route.request().url());
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
+ const limit = parseInt(url.searchParams.get('limit') || '100', 10);
+
+ // Generate nodes for the requested chunk
+ const nodes = await generateNodeMock({
+ offset,
+ limit: Math.min(limit, totalNodes - offset), // Ensure we don't generate more than total
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ Overall: 'Green',
+ Nodes: nodes,
+ TotalNodes: totalNodes.toString(),
+ FoundNodes: totalNodes.toString(),
+ }),
+ });
+ });
+};
diff --git a/tests/suites/paginatedTable/paginatedTable.test.ts b/tests/suites/paginatedTable/paginatedTable.test.ts
new file mode 100644
index 000000000..2422c8a09
--- /dev/null
+++ b/tests/suites/paginatedTable/paginatedTable.test.ts
@@ -0,0 +1,143 @@
+import {expect, test} from '@playwright/test';
+
+import {NodesPage} from '../nodes/NodesPage';
+
+import {setupEmptyNodesMock, setupLargeNodesMock, setupNodesMock} from './mocks';
+import {ClusterNodesTable} from './paginatedTable';
+
+test.describe('PaginatedTable', () => {
+ test('loads data in chunks when scrolling', async ({page}) => {
+ // Setup mocks
+ await setupNodesMock(page);
+
+ // Navigate to nodes page which uses PaginatedTable
+ const nodesPage = new NodesPage(page);
+ await nodesPage.goto();
+
+ const paginatedTable = new ClusterNodesTable(page);
+ await paginatedTable.waitForTableVisible();
+ await paginatedTable.waitForTableData();
+
+ // Get initial row count (should be first chunk)
+ const initialVisibleRows = await paginatedTable.getRowCount();
+ expect(initialVisibleRows).toBeGreaterThan(0);
+ expect(initialVisibleRows).toBeLessThan(100); // Should not show all rows initially
+
+ // Get data from first visible row to verify initial chunk
+ const firstRowData = await paginatedTable.getRowData(0);
+ expect(firstRowData['Host']).toBe('host-0.test');
+ expect(firstRowData['Version']).toBe('main.b7cfb36');
+
+ await paginatedTable.scrollToBottom();
+
+ await paginatedTable.waitForTableData();
+
+ // Get data from last row to verify second chunk loaded
+ const rowCount = await paginatedTable.getRowCount();
+ const lastRowData = await paginatedTable.getRowData(rowCount - 1);
+ expect(lastRowData['Host']).toBe('host-99.test');
+ expect(lastRowData['Version']).toBe('main.b7cfb36');
+
+ // Verify uptime format matches the pattern from nodes.test.ts
+ const uptimeValues = await paginatedTable.getColumnValues('Uptime');
+ for (const uptime of uptimeValues) {
+ expect(uptime).toMatch(/^(\d+d\s)?(\d+):(\d{2}):(\d{2})$/); // Format: DDd? HH:MM:SS
+ }
+ });
+
+ test('loads data when scrolling to middle of table', async ({page}) => {
+ // Setup mocks with large dataset
+ await setupLargeNodesMock(page);
+
+ // Navigate to nodes page which uses PaginatedTable
+ const nodesPage = new NodesPage(page);
+ await nodesPage.goto();
+
+ const paginatedTable = new ClusterNodesTable(page);
+ await paginatedTable.waitForTableVisible();
+ await paginatedTable.waitForTableData();
+
+ // Get initial row count
+ const initialVisibleRows = await paginatedTable.getRowCount();
+ expect(initialVisibleRows).toBeGreaterThan(0);
+ expect(initialVisibleRows).toBeLessThan(1000); // Should not show all rows initially
+
+ // Scroll to middle of container
+ await paginatedTable.scrollToMiddle();
+ await paginatedTable.waitForTableData();
+
+ // Get data from middle rows to verify middle chunk loaded
+ const rowCount = await paginatedTable.getRowCount();
+ const middleRowIndex = Math.floor(rowCount / 2);
+ const middleRowData = await paginatedTable.getRowData(middleRowIndex);
+ expect(middleRowData['Host']).toBe('host-500.test');
+ expect(middleRowData['Version']).toBe('main.b7cfb36');
+ });
+
+ test('displays empty state when no data is present', async ({page}) => {
+ // Setup mocks with empty data
+ await setupEmptyNodesMock(page);
+
+ const nodesPage = new NodesPage(page);
+ await nodesPage.goto();
+
+ const paginatedTable = new ClusterNodesTable(page);
+ await paginatedTable.waitForTableVisible();
+
+ // Verify empty state
+ const rowCount = await paginatedTable.getRowCount();
+ expect(rowCount).toBe(1);
+ const emptyDataMessage = await paginatedTable.getEmptyDataMessageLocator();
+ await expect(emptyDataMessage).toContainText('No such nodes');
+ });
+
+ test('handles 10 pages of data correctly', async ({page}) => {
+ // Setup mocks with 1000 nodes (100 per page * 10 pages)
+ await setupLargeNodesMock(page);
+
+ const nodesPage = new NodesPage(page);
+ await nodesPage.goto();
+
+ const paginatedTable = new ClusterNodesTable(page);
+ await paginatedTable.waitForTableVisible();
+ await paginatedTable.waitForTableData();
+
+ // Verify initial data load
+ const initialRowCount = await paginatedTable.getRowCount();
+ expect(initialRowCount).toBeGreaterThan(0);
+ expect(initialRowCount).toBeLessThan(1000); // Should not load all rows at once
+
+ await paginatedTable.scrollToBottom();
+ await paginatedTable.waitForTableData();
+
+ // Verify we can load data from the last page
+ const finalRowCount = await paginatedTable.getRowCount();
+ const lastRowData = await paginatedTable.getRowData(finalRowCount - 1);
+ expect(lastRowData['Host']).toBe('host-999.test'); // Last node in 1000 nodes (0-999)
+ });
+
+ test('handles 100 pages of data correctly', async ({page}) => {
+ // Setup mocks with 10000 nodes (100 per page * 10 pages)
+ await setupLargeNodesMock(page, 10000);
+
+ const nodesPage = new NodesPage(page);
+ await nodesPage.goto();
+
+ const paginatedTable = new ClusterNodesTable(page);
+ await paginatedTable.waitForTableVisible();
+ await paginatedTable.waitForTableData();
+
+ // Verify initial data load
+ const initialRowCount = await paginatedTable.getRowCount();
+ expect(initialRowCount).toBeGreaterThan(0);
+ expect(initialRowCount).toBeLessThan(10000); // Should not load all rows at once
+
+ await paginatedTable.scrollToBottom();
+ await paginatedTable.waitForTableData();
+
+ // Verify we can load data from the last page
+ const finalRowCount = await paginatedTable.getRowCount();
+ const lastRowData = await paginatedTable.getRowData(finalRowCount - 1);
+ expect(lastRowData['Host']).toBe('host-9999.test'); // Last node in 1000 nodes (0-999)
+ });
+});
diff --git a/tests/suites/paginatedTable/paginatedTable.ts b/tests/suites/paginatedTable/paginatedTable.ts
index 8ec0d5c71..2a9f95533 100644
--- a/tests/suites/paginatedTable/paginatedTable.ts
+++ b/tests/suites/paginatedTable/paginatedTable.ts
@@ -2,29 +2,23 @@ import type {Locator, Page} from '@playwright/test';
import {VISIBILITY_TIMEOUT} from '../tenant/TenantPage';
-export class PaginatedTable {
- private page: Page;
- private tableSelector: Locator;
- private searchInput: Locator;
- private radioButtons: Locator;
- private countLabel: Locator;
- private tableRows: Locator;
- private emptyTableRows: Locator;
- private refreshButton: Locator;
- private refreshIntervalSelect: Locator;
- private headCells: Locator;
- private columnSetupButton: Locator;
- private columnSetupPopup: Locator;
-
- constructor(page: Page) {
+export class TableControls {
+ protected page: Page;
+ protected tableSelector: Locator;
+ protected searchInput: Locator;
+ protected radioButtons: Locator;
+ protected countLabel: Locator;
+ protected refreshButton: Locator;
+ protected refreshIntervalSelect: Locator;
+ protected columnSetupButton: Locator;
+ protected columnSetupPopup: Locator;
+
+ constructor(page: Page, tableSelector: Locator) {
this.page = page;
- this.tableSelector = page.locator('.ydb-table-with-controls-layout');
+ this.tableSelector = tableSelector;
this.searchInput = this.tableSelector.locator('.ydb-search input');
this.radioButtons = this.tableSelector.locator('.g-radio-button');
this.countLabel = this.tableSelector.locator('.ydb-entities-count .g-label__content');
- this.headCells = this.tableSelector.locator('.ydb-paginated-table__head-cell');
- this.tableRows = this.tableSelector.locator('.ydb-paginated-table__row');
- this.emptyTableRows = this.tableSelector.locator('.ydb-paginated-table__row_empty');
this.refreshButton = page.locator('.auto-refresh-control button[aria-label="Refresh"]');
this.refreshIntervalSelect = page.getByTestId('ydb-autorefresh-select');
this.columnSetupButton = this.tableSelector.locator(
@@ -33,10 +27,6 @@ export class PaginatedTable {
this.columnSetupPopup = page.locator('.g-popup .g-select-popup.g-tree-select__popup');
}
- async waitForTableVisible() {
- await this.tableSelector.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
- }
-
async search(searchTerm: string) {
await this.searchInput.fill(searchTerm);
}
@@ -44,7 +34,6 @@ export class PaginatedTable {
async selectRadioOption(groupIndex: number, optionText: string) {
const radioGroup = this.radioButtons.nth(groupIndex);
const option = radioGroup.locator(`.g-radio-button__option:has-text("${optionText}")`);
-
await option.evaluate((el) => (el as HTMLElement).click());
}
@@ -54,6 +43,100 @@ export class PaginatedTable {
return match ? parseInt(match[1], 10) : 0;
}
+ async clickRefreshButton() {
+ await this.refreshButton.click();
+ }
+
+ async setRefreshInterval(interval: string) {
+ await this.refreshIntervalSelect.click();
+ await this.page.locator('.g-select-list__option', {hasText: interval}).click();
+ }
+
+ async getRefreshInterval(): Promise {
+ const text = await this.refreshIntervalSelect
+ .locator('.g-select-control__option-text')
+ .innerText();
+ return text;
+ }
+
+ async openColumnSetup() {
+ await this.columnSetupButton.click();
+ await this.columnSetupPopup.waitFor({state: 'visible'});
+ }
+
+ async setColumnChecked(columnName: string) {
+ const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
+ const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
+ const isVisible = await checkIcon.isVisible();
+ if (!isVisible) {
+ await columnOption.click();
+ }
+ }
+
+ async setColumnUnchecked(columnName: string) {
+ const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
+ const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
+ const isVisible = await checkIcon.isVisible();
+ if (isVisible) {
+ await columnOption.click();
+ }
+ }
+
+ async applyColumnVisibility() {
+ const applyButton = this.columnSetupPopup.locator('button:has-text("Apply")');
+ await applyButton.click();
+ await this.columnSetupPopup.waitFor({state: 'hidden'});
+ }
+
+ async getVisibleColumnsCount(): Promise {
+ const statusText = await this.columnSetupButton
+ .locator('.g-table-column-setup__status')
+ .innerText();
+ return statusText;
+ }
+
+ async isColumnVisible(columnName: string): Promise {
+ const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
+ const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
+ return await checkIcon.isVisible();
+ }
+}
+
+export class PaginatedTable {
+ protected controls: TableControls;
+ protected page: Page;
+ private tableSelector: Locator;
+ private tableRows: Locator;
+ private emptyTableRows: Locator;
+ private headCells: Locator;
+ private scrollContainer: string;
+
+ constructor(page: Page, scrollContainer = '.ydb-cluster') {
+ this.page = page;
+ this.tableSelector = page.locator('.ydb-table-with-controls-layout');
+ this.headCells = this.tableSelector.locator('.ydb-paginated-table__head-cell');
+ this.tableRows = this.tableSelector.locator('.ydb-paginated-table__row');
+ this.emptyTableRows = this.tableSelector.locator('.ydb-paginated-table__row_empty');
+ this.scrollContainer = scrollContainer;
+ this.controls = new TableControls(page, this.tableSelector);
+ }
+
+ getControls(): TableControls {
+ return this.controls;
+ }
+
+ async waitForTableVisible() {
+ await this.tableSelector.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ }
+
+ async getCount(): Promise {
+ return this.controls.getCount();
+ }
+
+ async search(searchTerm: string) {
+ await this.controls.search(searchTerm);
+ }
+
async getColumnValues(columnName: string): Promise {
const columnIndex = await this.getColumnIndex(columnName);
return this.tableRows.evaluateAll(
@@ -109,19 +192,6 @@ export class PaginatedTable {
await this.page.waitForTimeout(1000);
}
- async clickRefreshButton() {
- await this.refreshButton.click();
- }
-
- async setRefreshInterval(interval: string) {
- await this.refreshIntervalSelect.click();
- await this.page.locator('.g-select-list__option', {hasText: interval}).click();
- }
-
- async getRefreshInterval(): Promise {
- return this.refreshIntervalSelect.locator('.g-select-control__option-text').innerText();
- }
-
async sortByColumn(columnName: string) {
const columnHeader = this.tableSelector.locator(
`.ydb-paginated-table__head-cell:has-text("${columnName}")`,
@@ -131,46 +201,25 @@ export class PaginatedTable {
await this.waitForTableData();
}
- async openColumnSetup() {
- await this.columnSetupButton.click();
- await this.columnSetupPopup.waitFor({state: 'visible'});
- }
-
- async setColumnChecked(columnName: string) {
- const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
- const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
- const isVisible = await checkIcon.isVisible();
- if (!isVisible) {
- await columnOption.click();
- }
- }
-
- async setColumnUnchecked(columnName: string) {
- const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
- const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
- const isVisible = await checkIcon.isVisible();
- if (isVisible) {
- await columnOption.click();
- }
- }
-
- async applyColumnVisibility() {
- const applyButton = this.columnSetupPopup.locator('button:has-text("Apply")');
- await applyButton.click();
- await this.columnSetupPopup.waitFor({state: 'hidden'});
- }
-
- async getVisibleColumnsCount(): Promise {
- const statusText = await this.columnSetupButton
- .locator('.g-table-column-setup__status')
- .innerText();
- return statusText;
+ async scrollToBottom() {
+ await this.page.evaluate((selector) => {
+ const container = document.querySelector(selector);
+ if (container) {
+ container.scrollTo({top: container.scrollHeight, behavior: 'instant'});
+ }
+ }, this.scrollContainer);
}
- async isColumnVisible(columnName: string): Promise {
- const columnOption = this.columnSetupPopup.locator(`[data-list-item="${columnName}"]`);
- const checkIcon = columnOption.locator('.g-icon.g-color-text_color_info');
- return await checkIcon.isVisible();
+ async scrollToMiddle() {
+ await this.page.evaluate((selector) => {
+ const container = document.querySelector(selector);
+ if (container) {
+ container.scrollTo({
+ top: Math.floor(container.scrollHeight / 2),
+ behavior: 'instant',
+ });
+ }
+ }, this.scrollContainer);
}
private async getColumnIndex(columnName: string): Promise {
@@ -184,3 +233,15 @@ export class PaginatedTable {
throw new Error(`Column "${columnName}" not found`);
}
}
+
+export class ClusterNodesTable extends PaginatedTable {
+ constructor(page: Page) {
+ super(page, '.ydb-cluster');
+ }
+}
+
+export class ClusterStorageTable extends PaginatedTable {
+ constructor(page: Page) {
+ super(page, '.ydb-cluster');
+ }
+}
diff --git a/tests/suites/storage/storage.test.ts b/tests/suites/storage/storage.test.ts
index 95e64efa4..bd99bf846 100644
--- a/tests/suites/storage/storage.test.ts
+++ b/tests/suites/storage/storage.test.ts
@@ -1,6 +1,6 @@
import {expect, test} from '@playwright/test';
-import {PaginatedTable} from '../paginatedTable/paginatedTable';
+import {ClusterStorageTable} from '../paginatedTable/paginatedTable';
import {StoragePage} from './StoragePage';
@@ -46,7 +46,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Table loads and displays data', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterStorageTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -56,7 +56,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Search by pool name filters the table', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterStorageTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -71,13 +71,13 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Radio button selection changes displayed data', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterStorageTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
const initialRowCount = await paginatedTable.getRowCount();
- await paginatedTable.selectRadioOption(0, 'Nodes');
+ await paginatedTable.getControls().selectRadioOption(0, 'Nodes');
await page.waitForTimeout(1000); // Wait for the table to update
@@ -86,7 +86,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Groups count is displayed correctly', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterStorageTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -98,7 +98,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Row data can be retrieved correctly', async ({page}) => {
- const storageTable = new PaginatedTable(page);
+ const storageTable = new ClusterStorageTable(page);
await storageTable.waitForTableToLoad();
await storageTable.waitForTableData();
@@ -113,7 +113,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Column values can be retrieved correctly', async ({page}) => {
- const paginatedTable = new PaginatedTable(page);
+ const paginatedTable = new ClusterStorageTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();
@@ -127,7 +127,7 @@ test.describe('Test Storage Paginated Table', async () => {
});
test('Clicking on Group ID header sorts the table', async ({page}) => {
- const storageTable = new PaginatedTable(page);
+ const storageTable = new ClusterStorageTable(page);
await storageTable.waitForTableToLoad();
await storageTable.waitForTableData();