Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/containers/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,16 @@ export function Node() {
if (isDiskPagesAvailable) {
actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'structure');
}
// Filter out threads tab if there's no thread data in the API response
if (!node?.Threads || node.Threads.length === 0) {
actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'threads');
}

const actualActiveTab =
actulaNodeTabs.find(({id}) => id === activeTabId) ?? actulaNodeTabs[0];

return {activeTab: actualActiveTab, nodeTabs: actulaNodeTabs};
}, [isStorageNode, isDiskPagesAvailable, activeTabId]);
}, [isStorageNode, isDiskPagesAvailable, activeTabId, node?.Threads]);

const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString();

Expand Down
5 changes: 4 additions & 1 deletion src/store/reducers/node/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {TThreadPoolInfo} from '../../../types/api/threads';
import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types';
import type {PreparedNodeSystemState} from '../../../utils/nodes';

Expand All @@ -18,4 +19,6 @@ export interface PreparedStructurePDisk extends PreparedPDisk {

export type PreparedNodeStructure = Record<string, PreparedStructurePDisk>;

export interface PreparedNode extends Partial<PreparedNodeSystemState> {}
export interface PreparedNode extends Partial<PreparedNodeSystemState> {
Threads?: TThreadPoolInfo[];
}
8 changes: 7 additions & 1 deletion src/store/reducers/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ export const prepareNodeData = (data: TEvSystemStateResponse): PreparedNode => {

const nodeData = data.SystemStateInfo[0];

return prepareNodeSystemState(nodeData);
const preparedSystemState = prepareNodeSystemState(nodeData);

// Include Threads from the top-level response for the tab filtering logic
return {
...preparedSystemState,
Threads: data.Threads,
};
};
42 changes: 42 additions & 0 deletions tests/suites/nodes/NodePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type {Locator, Page} from '@playwright/test';

import {PageModel} from '../../models/PageModel';

export class NodePage extends PageModel {
readonly tabs: Locator;
readonly threadsTab: Locator;
readonly tabletsTab: Locator;
readonly storageTab: Locator;

constructor(page: Page, nodeId: string) {
super(page, `node/${nodeId}`);

this.tabs = this.selector.locator('.node__tab-list');
this.threadsTab = this.tabs.locator('[value="threads"]');
this.tabletsTab = this.tabs.locator('[value="tablets"]');
this.storageTab = this.tabs.locator('[value="storage"]');
}

async waitForNodePageLoad() {
// Wait for the page to load and tabs to be visible
await this.tabs.waitFor({state: 'visible'});
}

async isThreadsTabVisible() {
try {
await this.threadsTab.waitFor({state: 'visible', timeout: 1000});
return true;
} catch {
return false;
}
}

async clickThreadsTab() {
await this.threadsTab.click();
}

async getAllTabNames() {
const tabs = await this.tabs.locator('.g-tabs__item').allTextContents();
return tabs;
}
}
165 changes: 165 additions & 0 deletions tests/suites/nodes/nodes.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {expect, test} from '@playwright/test';

import {backend} from '../../utils/constants';
import {NodePage} from '../nodes/NodePage';
import {NodesPage} from '../nodes/NodesPage';
import {ClusterNodesTable} from '../paginatedTable/paginatedTable';

Expand Down Expand Up @@ -193,3 +194,167 @@ test.describe('Test Nodes Paginated Table', async () => {
expect(hostValues.length).toBeGreaterThan(0);
});
});

test.describe('Test Node Page Threads Tab', async () => {
test('Threads tab is hidden when node has no thread data', async ({page}) => {
// Mock the node API to return no thread data
await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => {
await route.fulfill({
json: {
SystemStateInfo: [
{
Host: 'localhost',
NodeId: 1,
SystemState: 1, // Green
Version: 'test-version',
},
],
// No Threads property
},
});
});

const nodesPage = new NodesPage(page);
await nodesPage.goto();

// Get first node ID to navigate to
const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();

// Click on first node to navigate to node page
const firstRowLink = page
.locator('.ydb-paginated-table__table tbody tr:first-child a')
.first();
await firstRowLink.click();

// Wait for navigation to complete
await page.waitForURL(/\/node\/\d+/);

const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1];
expect(nodeId).toBeDefined();

const nodePage = new NodePage(page, nodeId!);
await nodePage.waitForNodePageLoad();

// Verify threads tab is not visible
const isThreadsTabVisible = await nodePage.isThreadsTabVisible();
expect(isThreadsTabVisible).toBe(false);

// Verify other tabs are still visible
const tabNames = await nodePage.getAllTabNames();
expect(tabNames).toContain('Tablets');
expect(tabNames).not.toContain('Threads');
});

test('Threads tab is visible when node has thread data', async ({page}) => {
// Mock the node API to return thread data
await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => {
await route.fulfill({
json: {
SystemStateInfo: [
{
Host: 'localhost',
NodeId: 1,
SystemState: 1, // Green
Version: 'test-version',
},
],
Threads: [
{
Name: 'TestPool',
Threads: 4,
},
],
},
});
});

const nodesPage = new NodesPage(page);
await nodesPage.goto();

// Get first node ID to navigate to
const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();

// Click on first node to navigate to node page
const firstRowLink = page
.locator('.ydb-paginated-table__table tbody tr:first-child a')
.first();
await firstRowLink.click();

// Wait for navigation to complete
await page.waitForURL(/\/node\/\d+/);

const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1];
expect(nodeId).toBeDefined();

const nodePage = new NodePage(page, nodeId!);
await nodePage.waitForNodePageLoad();

// Verify threads tab is visible
const isThreadsTabVisible = await nodePage.isThreadsTabVisible();
expect(isThreadsTabVisible).toBe(true);

// Verify can click on threads tab
await nodePage.clickThreadsTab();
await page.waitForURL(/\/node\/\d+\/threads/);

// Verify other tabs are also visible
const tabNames = await nodePage.getAllTabNames();
expect(tabNames).toContain('Tablets');
expect(tabNames).toContain('Threads');
});

test('Threads tab is hidden when node has empty thread array', async ({page}) => {
// Mock the node API to return empty thread data
await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => {
await route.fulfill({
json: {
SystemStateInfo: [
{
Host: 'localhost',
NodeId: 1,
SystemState: 1, // Green
Version: 'test-version',
},
],
Threads: [], // Empty array
},
});
});

const nodesPage = new NodesPage(page);
await nodesPage.goto();

// Get first node ID to navigate to
const paginatedTable = new ClusterNodesTable(page);
await paginatedTable.waitForTableToLoad();
await paginatedTable.waitForTableData();

// Click on first node to navigate to node page
const firstRowLink = page
.locator('.ydb-paginated-table__table tbody tr:first-child a')
.first();
await firstRowLink.click();

// Wait for navigation to complete
await page.waitForURL(/\/node\/\d+/);

const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1];
expect(nodeId).toBeDefined();

const nodePage = new NodePage(page, nodeId!);
await nodePage.waitForNodePageLoad();

// Verify threads tab is not visible
const isThreadsTabVisible = await nodePage.isThreadsTabVisible();
expect(isThreadsTabVisible).toBe(false);

// Verify other tabs are still visible
const tabNames = await nodePage.getAllTabNames();
expect(tabNames).toContain('Tablets');
expect(tabNames).not.toContain('Threads');
});
});
Loading