diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 861d4e566c..3a33fa0cc9 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -68,19 +68,25 @@ export function Node() { const isStorageNode = node?.Roles?.find((el) => el === STORAGE_ROLE); + const threadsQuantity = node?.Threads?.length; + const {activeTab, nodeTabs} = React.useMemo(() => { - let actulaNodeTabs = isStorageNode + let actualNodeTabs = isStorageNode ? NODE_TABS : NODE_TABS.filter((el) => el.id !== 'storage'); if (isDiskPagesAvailable) { - actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'structure'); + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); + } + // Filter out threads tab if there's no thread data in the API response + if (!threadsQuantity) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); } const actualActiveTab = - actulaNodeTabs.find(({id}) => id === activeTabId) ?? actulaNodeTabs[0]; + actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0]; - return {activeTab: actualActiveTab, nodeTabs: actulaNodeTabs}; - }, [isStorageNode, isDiskPagesAvailable, activeTabId]); + return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs}; + }, [isStorageNode, isDiskPagesAvailable, activeTabId, threadsQuantity]); const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString(); diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts new file mode 100644 index 0000000000..5aa10bc239 --- /dev/null +++ b/tests/suites/nodes/NodePage.ts @@ -0,0 +1,51 @@ +import type {Locator, Page} from '@playwright/test'; + +import {PageModel} from '../../models/PageModel'; +import {VISIBILITY_TIMEOUT} from '../tenant/TenantPage'; + +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 + try { + await this.tabs.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + } catch (error) { + console.error('Failed to load node page tabs:', error); + throw error; + } + } + + async isThreadsTabVisible() { + const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); + try { + await threadsTab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } catch { + console.error('Threads tab is not visible'); + return false; + } + } + + async clickThreadsTab() { + const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); + await threadsTab.click(); + } + + async getAllTabNames() { + const tabs = await this.tabs.locator('.g-tab').allTextContents(); + return tabs; + } +} diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index ba80e4f128..e2b0a65413 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -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'; @@ -193,3 +194,111 @@ 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(`**/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 'Green', + Version: 'test-version', + }, + ], + // No Threads property + }), + }); + }); + + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); + await nodePage.waitForNodePageLoad(); + + // 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(`**/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 'Green', + Version: 'test-version', + Threads: [ + { + Name: 'TestPool', + Threads: 4, + }, + ], + }, + ], + }), + }); + }); + + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); + 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(`**/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 'Green', + Version: 'test-version', + }, + ], + Threads: [], // Empty array + }), + }); + }); + + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); + await nodePage.waitForNodePageLoad(); + + // Verify other tabs are still visible + const tabNames = await nodePage.getAllTabNames(); + expect(tabNames).toContain('Tablets'); + expect(tabNames).not.toContain('Threads'); + }); +});