This is an automated email from the ASF dual-hosted git repository.
arafat2198 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 9aae7a5c0b HDDS-9791. Add tests for Datanodes page (#7626)
9aae7a5c0b is described below
commit 9aae7a5c0ba19df1777c2bb4ef2329d5a5b79911
Author: Abhishek Pal <[email protected]>
AuthorDate: Thu Jan 2 16:40:26 2025 +0530
HDDS-9791. Add tests for Datanodes page (#7626)
---
.../webapps/recon/ozone-recon-web/package.json | 1 +
.../webapps/recon/ozone-recon-web/pnpm-lock.yaml | 12 ++
.../src/__tests__/datanodes/Datanodes.test.tsx | 191 +++++++++++++++++++
.../__tests__/datanodes/DatanodesTable.test.tsx | 151 +++++++++++++++
.../src/__tests__/locators/locators.ts | 13 +-
.../mocks/datanodeMocks/datanodeResponseMocks.ts | 212 +++++++++++++++++++++
.../mocks/datanodeMocks/datanodeServer.ts | 72 +++++++
.../src/__tests__/utils/datanodes.utils.tsx | 23 +++
.../src/v2/components/search/search.tsx | 6 +-
.../src/v2/components/tables/datanodesTable.tsx | 8 +-
.../src/v2/pages/datanodes/datanodes.tsx | 13 +-
11 files changed, 691 insertions(+), 11 deletions(-)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
index c2c046f112..2b407eaebd 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
@@ -58,6 +58,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^12.1.5",
+ "@testing-library/user-event": "^14.5.2",
"@types/react": "16.8.15",
"@types/react-dom": "16.8.4",
"@types/react-router-dom": "^5.3.3",
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
index dfdbc7cedc..2b28bbf3d5 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
@@ -67,6 +67,9 @@ devDependencies:
'@testing-library/react':
specifier: ^12.1.5
version: 12.1.5([email protected])([email protected])
+ '@testing-library/user-event':
+ specifier: ^14.5.2
+ version: 14.5.2(@testing-library/[email protected])
'@types/react':
specifier: 16.8.15
version: 16.8.15
@@ -1222,6 +1225,15 @@ packages:
react-dom: 16.14.0([email protected])
dev: true
+ /@testing-library/[email protected](@testing-library/[email protected]):
+ resolution: {integrity:
sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+ dependencies:
+ '@testing-library/dom': 8.20.1
+ dev: true
+
/@types/[email protected]:
resolution: {integrity:
sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
dev: true
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
new file mode 100644
index 0000000000..a169e1ce34
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { rest } from "msw";
+import { vi } from 'vitest';
+
+import Datanodes from '@/v2/pages/datanodes/datanodes';
+import * as commonUtils from '@/utils/common';
+import { datanodeServer } from
'@/__tests__/mocks/datanodeMocks/datanodeServer';
+import { datanodeLocators, searchInputLocator } from
'@/__tests__/locators/locators';
+import { waitForDNTable } from '@/__tests__/utils/datanodes.utils';
+
+// Mock utility functions
+vi.spyOn(commonUtils, 'showDataFetchError');
+
+vi.mock('@/components/autoReloadPanel/autoReloadPanel', () => ({
+ default: () => <div data-testid="auto-reload-panel" />,
+}));
+vi.mock('@/v2/components/select/multiSelect.tsx', () => ({
+ default: ({ onChange }: { onChange: Function }) => (
+ <select data-testid="multi-select" onChange={(e) =>
onChange(e.target.value)}>
+ <option value="hostname">Hostname</option>
+ <option value="uuid">UUID</option>
+ </select>
+ ),
+}));
+
+describe('Datanodes Component', () => {
+ // Start and stop MSW server before and after all tests
+ beforeAll(() => datanodeServer.listen());
+ afterEach(() => datanodeServer.resetHandlers());
+ afterAll(() => datanodeServer.close());
+
+ test('renders component correctly', () => {
+ render(<Datanodes />);
+
+ expect(screen.getByText(/Datanodes/)).toBeInTheDocument();
+ expect(screen.getByTestId('auto-reload-panel')).toBeInTheDocument();
+ expect(screen.getByTestId('multi-select')).toBeInTheDocument();
+ expect(screen.getByTestId(searchInputLocator)).toBeInTheDocument();
+ });
+
+ test('Renders table with correct number of rows', async () => {
+ render(<Datanodes />);
+
+ // Wait for the data to load
+ const rows = await waitFor(() =>
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+ expect(rows).toHaveLength(5); // Based on the mocked DatanodeResponse
+ });
+
+ test('Loads data on mount', async () => {
+ render(<Datanodes />);
+ // Wait for the data to be loaded into the table
+ const dnTable = await waitForDNTable();
+
+ // Ensure the correct data is displayed in the table
+ expect(dnTable).toHaveTextContent('ozone-datanode-1.ozone_default');
+ expect(dnTable).toHaveTextContent('HEALTHY');
+ });
+
+ test('Displays no data message if the datanodes API returns an empty array',
async () => {
+ datanodeServer.use(
+ rest.get('api/v1/datanodes', (req, res, ctx) => {
+ return res(ctx.status(200), ctx.json({ totalCount: 0, datanodes: []
}));
+ })
+ );
+
+ render(<Datanodes />);
+
+ // Wait for the no data message
+ await waitFor(() => expect(screen.getByText('No
Data')).toBeInTheDocument());
+ });
+
+ test('Handles search input change', async () => {
+ render(<Datanodes />);
+ await waitForDNTable();
+
+ const searchInput = screen.getByTestId(searchInputLocator);
+ fireEvent.change(searchInput, {
+ target: { value: 'ozone-datanode-1' }
+ });
+ // Sleep for 310ms to allow debounced search to take effect
+ await new Promise((r) => { setTimeout(r, 310) });
+ const rows = await waitFor(() =>
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+ await waitFor(() => expect(rows).toHaveLength(1));
+ });
+
+ test('Handles case-sensitive search', async () => {
+ render(<Datanodes />);
+ await waitForDNTable();
+
+ const searchInput = screen.getByTestId(searchInputLocator);
+ fireEvent.change(searchInput, {
+ target: { value: 'DataNode' }
+ });
+ await waitFor(() => expect(searchInput).toHaveValue('DataNode'));
+ // Sleep for 310ms to allow debounced search to take effect
+ await new Promise((r) => { setTimeout(r, 310) })
+
+ const rows = await waitFor(() =>
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+ expect(rows).toHaveLength(1);
+ })
+
+ test('Displays a message when no results match the search term', async () =>
{
+ render(<Datanodes />);
+ const searchInput = screen.getByTestId(searchInputLocator);
+
+ // Type a term that doesn't match any datanode
+ fireEvent.change(searchInput, {
+ target: { value: 'nonexistent-datanode' }
+ });
+
+ // Verify that no results message is displayed
+ await waitFor(() => expect(screen.getByText('No
Data')).toBeInTheDocument());
+ });
+
+ // Since this is a static response, even if we remove we will not get the
truncated response from backend
+ // i.e response with the removed DN. So the table will always have the value
even if we remove it
+ // causing this test to fail
+ test.skip('Shows modal on row selection and confirms removal', async () => {
+ render(<Datanodes />);
+
+ // Wait for the data to be loaded into the table
+ await waitForDNTable();
+
+ // Simulate selecting a row
+ // The first checkbox is for the table header "Select All" checkbox -> idx 0
+ // Second checkbox is for the healthy DN -> idx
1
+ // Third checkbox is the active one for Dead DN -> idx
2
+ const checkbox = document.querySelectorAll('input.ant-checkbox-input');
+ userEvent.click(checkbox[0]);
+ // Click the "Remove" button to open the modal
+ await waitFor(() => {
+ // Wait for the button to appear in screen
+ screen.getByTestId(datanodeLocators.datanodeRemoveButton);
+ }).then(() => {
+ userEvent.click(screen.getByText(/Remove/));
+ })
+
+ // Confirm removal in the modal
+ await waitFor(() => {
+ // Wait for the button to appear in screen
+ screen.getByTestId(datanodeLocators.datanodeRemoveModal);
+ }).then(() => {
+ userEvent.click(screen.getByText(/OK/));
+ })
+
+ // Wait for the removal operation to complete
+ await waitFor(() =>
+
expect(screen.queryByText('ozone-datanode-3.ozone_default')).not.toBeInTheDocument()
+ );
+ });
+
+ test('Handles API errors gracefully by showing error message', async () => {
+ // Set up MSW to return an error for the datanode API
+ datanodeServer.use(
+ rest.get('api/v1/datanodes', (req, res, ctx) => {
+ return res(ctx.status(500), ctx.json({ error: 'Internal Server Error'
}));
+ })
+ );
+
+ render(<Datanodes />);
+
+ // Wait for the error to be handled
+ await waitFor(() =>
+ expect(commonUtils.showDataFetchError).toHaveBeenCalledWith('AxiosError:
Request failed with status code 500')
+ );
+ });
+});
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
new file mode 100644
index 0000000000..f1be5362ec
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { vi } from 'vitest';
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor
+} from '@testing-library/react';
+
+import { DatanodeTableProps } from '@/v2/types/datanode.types';
+import DatanodesTable from '@/v2/components/tables/datanodesTable';
+import { datanodeServer } from
'@/__tests__/mocks/datanodeMocks/datanodeServer';
+import { waitForDNTable } from '@/__tests__/utils/datanodes.utils';
+
+const defaultProps: DatanodeTableProps = {
+ loading: false,
+ selectedRows: [],
+ data: [],
+ decommissionUuids: [],
+ searchColumn: 'hostname',
+ searchTerm: '',
+ selectedColumns: [
+ { label: 'Hostname', value: 'hostname' },
+ { label: 'State', value: 'state' },
+ ],
+ handleSelectionChange: vi.fn(),
+};
+
+function getDataWith(name: string, state: "HEALTHY" | "STALE" | "DEAD", uuid:
number) {
+ return {
+ hostname: name,
+ uuid: uuid,
+ state: state,
+ opState: 'IN_SERVICE',
+ lastHeartbeat: 1728280581608,
+ storageUsed: 4096,
+ storageTotal: 125645656770,
+ storageCommitted: 0,
+ storageRemaining: 114225606656,
+ pipelines: [
+ {
+ "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+ "replicationType": "RATIS",
+ "replicationFactor": "THREE",
+ "leaderNode": "ozone-datanode-3.ozone_default"
+ },
+ {
+ "pipelineID": "2c23e76e-3f18-4b86-9541-e48bdc152fda",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-1.ozone_default"
+ }
+ ],
+ containers: 8192,
+ openContainers: 8182,
+ leaderCount: 2,
+ version: '0.6.0-SNAPSHOT',
+ setupTime: 1728280539733,
+ revision: '3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1',
+ buildDate: '2024-10-06T16:41Z',
+ networkLocation: '/default-rack'
+ }
+}
+
+describe('DatanodesTable Component', () => {
+ // Start and stop MSW server before and after all tests
+ beforeAll(() => datanodeServer.listen());
+ afterEach(() => datanodeServer.resetHandlers());
+ afterAll(() => datanodeServer.close());
+
+ test('renders table with data', async () => {
+ render(<DatanodesTable {...defaultProps} data={[]} />);
+
+ // Wait for the table to render
+ waitForDNTable();
+
+ expect(screen.getByTestId('dn-table')).toBeInTheDocument();
+ });
+
+ test('filters data based on search term', async () => {
+ render(
+ <DatanodesTable
+ {...defaultProps}
+ searchTerm="ozone-datanode-1"
+ data={[
+ getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+ getDataWith('ozone-datanode-2', 'STALE', 2)
+ ]}
+ />
+ );
+
+ // Only the matching datanode should be visible
+ expect(screen.getByText('ozone-datanode-1')).toBeInTheDocument();
+ expect(screen.queryByText('ozone-datanode-2')).not.toBeInTheDocument();
+ });
+
+ test('handles row selection', async () => {
+ render(
+ <DatanodesTable
+ {...defaultProps}
+ data={[
+ getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+ getDataWith('ozone-datanode-2', 'DEAD', 2)
+ ]}
+ />
+ );
+
+ // The first checkbox is for the table header "Select All" checkbox -> idx 0
+ // Second checkbox is for the healthy DN -> idx
1
+ // Third checkbox is the active one for Dead DN -> idx
2
+ const checkbox = document.querySelectorAll('input.ant-checkbox-input')[2];
+ fireEvent.click(checkbox);
+
+ expect(defaultProps.handleSelectionChange).toHaveBeenCalledWith([2]);
+ });
+
+ test('disables selection for non-DEAD nodes', async () => {
+ render(
+ <DatanodesTable
+ {...defaultProps}
+ data={[
+ getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+ getDataWith('ozone-datanode-2', 'DEAD', 2)
+ ]}
+ />
+ );
+
+ // Check disabled and enabled rows
+ const checkboxes = document.querySelectorAll('input.ant-checkbox-input');
+ expect(checkboxes[1]).toBeDisabled(); // HEALTHY node
+ expect(checkboxes[2]).not.toBeDisabled(); // DEAD node
+ });
+});
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
index 23fbc76870..83b2bc5077 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
@@ -37,8 +37,15 @@ export const overviewLocators = {
}
export const datanodeLocators = {
- 'datanodeContainer': 'datanodes-container',
- 'datanodeMultiSelect': 'datanodes-multiselect'
+ 'datanodeMultiSelect': 'dn-multi-select',
+ 'datanodeSearchcDropdown': 'search-dropdown',
+ 'datanodeSearchInput': 'search-input',
+ 'datanodeRemoveButton': 'dn-remove-btn',
+ 'datanodeRemoveModal': 'dn-remove-modal',
+ 'datanodeTable': 'dn-table',
+ 'datanodeRowRegex': /dntable-/,
+ datanodeSearchOption: (label: string) => `search-opt-${label}`,
+ datanodeTableRow: (uuid: string) => `dntable-${uuid}`
}
export const autoReloadPanelLocators = {
@@ -46,3 +53,5 @@ export const autoReloadPanelLocators = {
'refreshButton': 'autoreload-panel-refresh',
'toggleSwitch': 'autoreload-panel-switch'
}
+
+export const searchInputLocator = 'search-input';
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
new file mode 100644
index 0000000000..887d0b4a27
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const DatanodeResponse = {
+ "totalCount": 5,
+ "datanodes": [
+ {
+ "uuid": "1",
+ "hostname": "ozone-datanode-1.ozone_default",
+ "state": "HEALTHY",
+ "opState": "IN_SERVICE",
+ "lastHeartbeat": 1728280581608,
+ "storageReport": {
+ "capacity": 125645656770,
+ "used": 4096,
+ "remaining": 114225606656,
+ "committed": 0
+ },
+ "pipelines": [
+ {
+ "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+ "replicationType": "RATIS",
+ "replicationFactor": "THREE",
+ "leaderNode": "ozone-datanode-3.ozone_default"
+ },
+ {
+ "pipelineID": "2c23e76e-3f18-4b86-9541-e48bdc152fda",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-1.ozone_default"
+ }
+ ],
+ "leaderCount": 1,
+ "version": "2.0.0-SNAPSHOT",
+ "setupTime": 1728280539733,
+ "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+ "buildDate": "2024-10-06T16:41Z",
+ "layoutVersion": 8,
+ "networkLocation": "/default-rack"
+ },
+ {
+ "uuid": "3",
+ "hostname": "ozone-datanode-3.ozone_default",
+ "state": "DEAD",
+ "opState": "IN_SERVICE",
+ "lastHeartbeat": 1728280582060,
+ "storageReport": {
+ "capacity": 125645656770,
+ "used": 4096,
+ "remaining": 114225623040,
+ "committed": 0
+ },
+ "pipelines": [
+ {
+ "pipelineID": "9c5bbf5e-62da-4d4a-a6ad-cb63d9f6aa6f",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-3.ozone_default"
+ },
+ {
+ "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+ "replicationType": "RATIS",
+ "replicationFactor": "THREE",
+ "leaderNode": "ozone-datanode-3.ozone_default"
+ }
+ ],
+ "leaderCount": 2,
+ "version": "1.5.0-SNAPSHOT",
+ "setupTime": 1728280539726,
+ "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+ "buildDate": "2024-10-06T16:41Z",
+ "layoutVersion": 8,
+ "networkLocation": "/default-rack"
+ },
+ {
+ "uuid": "4",
+ "hostname": "ozone-datanode-4.ozone_default",
+ "state": "HEALTHY",
+ "opState": "DECOMMISSIONING",
+ "lastHeartbeat": 1728280581614,
+ "storageReport": {
+ "capacity": 125645656770,
+ "used": 4096,
+ "remaining": 114225541120,
+ "committed": 0
+ },
+ "pipelines": [
+ {
+ "pipelineID": "4092a584-5c2f-40c6-98e5-ce9a9246e65d",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-4.ozone_default"
+ }
+ ],
+ "leaderCount": 1,
+ "version": "2.0.0-SNAPSHOT",
+ "setupTime": 1728280540325,
+ "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+ "buildDate": "2024-10-06T16:41Z",
+ "layoutVersion": 8,
+ "networkLocation": "/default-rack"
+ },
+ {
+ "uuid": "2",
+ "hostname": "ozone-datanode-2.ozone_default",
+ "state": "STALE",
+ "opState": "IN_SERVICE",
+ "lastHeartbeat": 1728280581594,
+ "storageReport": {
+ "capacity": 125645656770,
+ "used": 4096,
+ "remaining": 114225573888,
+ "committed": 0
+ },
+ "pipelines": [
+ {
+ "pipelineID": "20a874e4-790b-4312-8fc2-ca53846dba0f",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-2.ozone_default"
+ }
+ ],
+ "leaderCount": 1,
+ "version": "2.0.0-SNAPSHOT",
+ "setupTime": 1728280539745,
+ "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+ "buildDate": "2024-10-06T16:41Z",
+ "layoutVersion": 8,
+ "networkLocation": "/default-rack"
+ },
+ {
+ "uuid": "5",
+ "hostname": "ozone-DataNode-5.ozone_default",
+ "state": "DEAD",
+ "opState": "DECOMMISSIONED",
+ "lastHeartbeat": 1728280582055,
+ "storageReport": {
+ "capacity": 125645656770,
+ "used": 4096,
+ "remaining": 114225614848,
+ "committed": 0
+ },
+ "pipelines": [
+ {
+ "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+ "replicationType": "RATIS",
+ "replicationFactor": "THREE",
+ "leaderNode": "ozone-datanode-3.ozone_default"
+ },
+ {
+ "pipelineID": "67c973a0-722a-403a-8893-b8a5faaed7f9",
+ "replicationType": "RATIS",
+ "replicationFactor": "ONE",
+ "leaderNode": "ozone-datanode-5.ozone_default"
+ }
+ ],
+ "leaderCount": 1,
+ "version": "2.0.0-SNAPSHOT",
+ "setupTime": 1728280539866,
+ "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+ "buildDate": "2024-10-06T16:41Z",
+ "layoutVersion": 8,
+ "networkLocation": "/default-rack"
+ }
+ ]
+}
+
+export const NullDatanodeResponse = {
+ "totalCount": null,
+ "datanodes": [
+ {
+ "uuid": null,
+ "hostname": null,
+ "state": null,
+ "opState": null,
+ "lastHeartbeat": null,
+ "storageReport": null,
+ "pipelines": null,
+ "leaderCount": null,
+ "version": null,
+ "setupTime": null,
+ "revision": null,
+ "buildDate": null,
+ "layoutVersion": null,
+ "networkLocation": null
+ }
+ ]
+}
+
+export const NullDatanodes = {
+ "totalCount": null,
+ "datanodes": null
+}
+
+export const DecommissionInfo = {
+ "DatanodesDecommissionInfo": []
+}
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
new file mode 100644
index 0000000000..a7b11f4297
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { setupServer } from "msw/node";
+import { rest } from "msw";
+
+import * as mockResponses from "./datanodeResponseMocks";
+
+const handlers = [
+ rest.get("api/v1/datanodes", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.DatanodeResponse)
+ );
+ }),
+ rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.DecommissionInfo)
+ );
+ })
+];
+
+const nullDatanodeResponseHandler = [
+ rest.get("api/v1/datanodes", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.NullDatanodeResponse)
+ );
+ }),
+ rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.DecommissionInfo)
+ );
+ })
+]
+
+const nullDatanodeHandler = [
+ rest.get("api/v1/datanodes", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.NullDatanodes)
+ );
+ }),
+ rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json(mockResponses.DecommissionInfo)
+ );
+ })
+]
+
+//This will configure a request mocking server using MSW
+export const datanodeServer = setupServer(...handlers);
+export const nullDatanodeResponseServer =
setupServer(...nullDatanodeResponseHandler);
+export const nullDatanodeServer = setupServer(...nullDatanodeHandler);
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
new file mode 100644
index 0000000000..721a09b895
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { waitFor, screen } from "@testing-library/react";
+
+export const waitForDNTable = async () => {
+ return waitFor(() => screen.getByTestId('dn-table'));
+}
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
index d320fd659a..ed09b21817 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
@@ -55,7 +55,8 @@ const Search: React.FC<SearchProps> = ({
suffixIcon={(searchOptions.length > 1) ? <DownOutlined/> : null}
defaultValue={searchColumn}
options={searchOptions}
- onChange={onChange} />)
+ onChange={onChange}
+ data-testid='search-dropdown'/>)
: null
return (
@@ -69,7 +70,8 @@ const Search: React.FC<SearchProps> = ({
size='middle'
style={{
maxWidth: 400
- }}/>
+ }}
+ data-testid='search-input'/>
)
}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
index 494d898509..17e6048f7e 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { HTMLAttributes } from 'react';
import moment from 'moment';
import { Popover, Tooltip } from 'antd'
import {
@@ -306,7 +306,11 @@ const DatanodesTable: React.FC<DatanodeTableProps> = ({
rowKey='uuid'
pagination={paginationConfig}
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
- locale={{ filterTitle: '' }} />
+ locale={{ filterTitle: '' }}
+ onRow={(record: Datanode) => ({
+ 'data-testid': `dntable-${record.uuid}`
+ } as HTMLAttributes<HTMLElement>)}
+ data-testid='dn-table' />
</div>
);
}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
index fe22d08daf..33dd661d97 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
@@ -246,7 +246,8 @@ const Datanodes: React.FC<{}> = () => {
onChange={handleColumnChange}
onTagClose={() => { }}
fixedColumn='hostname'
- columnLength={columnOptions.length} />
+ columnLength={columnOptions.length}
+ data-testid='dn-multi-select' />
{selectedRows.length > 0 &&
<Button
type="primary"
@@ -256,7 +257,8 @@ const Datanodes: React.FC<{}> = () => {
borderColor: '#FF4D4E'
}}
loading={loading}
- onClick={() => { setModalOpen(true) }}> Remove
+ onClick={() => { setModalOpen(true) }}
+ data-testid='dn-remove-btn'> Remove
</Button>
}
</div>
@@ -271,7 +273,7 @@ const Datanodes: React.FC<{}> = () => {
onChange={(value) => {
setSearchTerm('');
setSearchColumn(value as 'hostname' | 'uuid' | 'version' |
'revision')
- }} />
+ }}/>
</div>
<DatanodesTable
loading={loading}
@@ -281,7 +283,7 @@ const Datanodes: React.FC<{}> = () => {
searchColumn={searchColumn}
searchTerm={debouncedSearch}
handleSelectionChange={handleSelectionChange}
- decommissionUuids={decommissionUuids} />
+ decommissionUuids={decommissionUuids}/>
</div>
</div>
<Modal
@@ -296,7 +298,8 @@ const Datanodes: React.FC<{}> = () => {
margin: '0px 0px 5px 0px',
fontSize: '16px',
fontWeight: 'bold'
- }}>
+ }}
+ data-testid='dn-remove-modal'>
<WarningFilled className='icon-warning' style={{paddingRight:
'8px'}}/>
Stop Tracking Datanode
</div>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]