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 159fb646672 HDDS-12710. UI changes for fixing counts and container 
pagination. (#8862).
159fb646672 is described below

commit 159fb646672b626548dcd64193c4fab435252d1d
Author: Arafat2198 <[email protected]>
AuthorDate: Sun Aug 17 17:14:11 2025 +0530

    HDDS-12710. UI changes for fixing counts and container pagination. (#8862).
---
 .../views/missingContainers/missingContainers.tsx  | 333 +++++++++++++--------
 1 file changed, 205 insertions(+), 128 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/missingContainers/missingContainers.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/missingContainers/missingContainers.tsx
index db30871a5a4..9156d68326e 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/missingContainers/missingContainers.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/missingContainers/missingContainers.tsx
@@ -7,7 +7,7 @@
  * "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
+ * 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,
@@ -22,6 +22,7 @@ import filesize from 'filesize';
 import {Table, Tabs, Tooltip} from 'antd';
 import {TablePaginationConfig} from 'antd/es/table';
 import {InfoCircleOutlined} from '@ant-design/icons';
+import { Link } from 'react-router-dom';
 
 import {ColumnSearch} from '@/utils/columnSearch';
 import {showDataFetchError, timeFormat} from '@/utils/common';
@@ -67,6 +68,8 @@ interface IUnhealthyContainersResponse {
   overReplicatedCount: number;
   misReplicatedCount: number;
   containers: IContainerResponse[];
+  lastKey: string;
+  firstKey: string;
 }
 
 interface IKeyResponse {
@@ -149,7 +152,7 @@ const CONTAINER_TAB_COLUMNS = [
     render: (expectedReplicaCount: number, record: IContainerResponse) => {
       const actualReplicaCount = record.actualReplicaCount;
       return (
-        <span>
+          <span>
           {actualReplicaCount} / {expectedReplicaCount}
         </span>
       );
@@ -160,30 +163,30 @@ const CONTAINER_TAB_COLUMNS = [
     dataIndex: 'replicas',
     key: 'replicas',
     render: (replicas: IContainerReplicas[]) => (
-      <div>
-        {replicas && replicas.map(replica => {
-          const tooltip = (
-            <div>
-              <div>First Report Time: {timeFormat(replica.firstSeenTime)}</div>
-              <div>Last Report Time: {timeFormat(replica.lastSeenTime)}</div>
-            </div>
-          );
-          return (
-            <div key={replica.datanodeHost}>
-              <Tooltip
-                placement='left'
-                title={tooltip}
-              >
-                <InfoCircleOutlined className='icon-small' />
-              </Tooltip>
-              <span className='pl-5'>
+        <div>
+          {replicas && replicas.map(replica => {
+                const tooltip = (
+                    <div>
+                      <div>First Report Time: 
{timeFormat(replica.firstSeenTime)}</div>
+                      <div>Last Report Time: 
{timeFormat(replica.lastSeenTime)}</div>
+                    </div>
+                );
+                return (
+                    <div key={replica.datanodeHost}>
+                      <Tooltip
+                          placement='left'
+                          title={tooltip}
+                      >
+                        <InfoCircleOutlined className='icon-small' />
+                      </Tooltip>
+                      <span className='pl-5'>
                 {replica.datanodeHost}
               </span>
-            </div>
-          );
-        }
-        )}
-      </div>
+                    </div>
+                );
+              }
+          )}
+        </div>
     )
   },
   {
@@ -214,11 +217,16 @@ interface IExpandedRowState {
 
 interface IMissingContainersState {
   loading: boolean;
-  missingDataSource: IContainerResponse[];
-  underReplicatedDataSource: IContainerResponse[];
-  overReplicatedDataSource: IContainerResponse[];
-  misReplicatedDataSource: IContainerResponse[];
+  containerDataSource: IContainerResponse[];
+  missingCount: number;
+  underReplicatedCount: number;
+  overReplicatedCount: number;
+  misReplicatedCount: number;
   expandedRowData: IExpandedRow;
+  currentState: string;
+  pageSize: number;
+  firstSeenKey: number;
+  lastSeenKey: number;
 }
 
 let cancelContainerSignal: AbortController;
@@ -229,47 +237,50 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
     super(props);
     this.state = {
       loading: false,
-      missingDataSource: [],
-      underReplicatedDataSource: [],
-      overReplicatedDataSource: [],
-      misReplicatedDataSource: [],
-      expandedRowData: {}
+      containerDataSource: [],
+      expandedRowData: {},
+      missingCount: 0,
+      underReplicatedCount: 0,
+      overReplicatedCount: 0,
+      misReplicatedCount: 0,
+      currentState: "MISSING",
+      pageSize: 10,
+      firstSeenKey: 0,
+      lastSeenKey: 0
     };
   }
 
-  componentDidMount(): void {
-    // Fetch missing containers on component mount
-    this.setState({
-      loading: true
-    });
-
-    const { request, controller } = 
AxiosGetHelper('/api/v1/containers/unhealthy', cancelContainerSignal);
-    cancelContainerSignal = controller;
-
-    request.then(allContainersResponse => {
+  // --- MODIFIED FUNCTION ---
+  // This function now accepts its parameters instead of reading from 
`this.state`
+  fetchUnhealthyContainers = (direction: boolean, options: { pageSize: number, 
lastSeenKey: number, firstSeenKey: number, currentState: string }) => {
+    this.setState({ loading: true });
 
-      const allContainersResponseData: IUnhealthyContainersResponse = 
allContainersResponse.data;
-      const allContainers: IContainerResponse[] = 
allContainersResponseData.containers;
+    const { pageSize, lastSeenKey, firstSeenKey, currentState } = options;
 
-      const missingContainersResponseData = allContainers && 
allContainers.filter(item => item.containerState === 'MISSING');
-      const mContainers: IContainerResponse[] = missingContainersResponseData;
+    let baseUrl = 
`/api/v1/containers/unhealthy/${encodeURIComponent(currentState)}?limit=${pageSize}`;
+    let queryParam = direction
+        ? `&minContainerId=${encodeURIComponent(lastSeenKey)}`
+        : `&maxContainerId=${encodeURIComponent(firstSeenKey)}`;
+    let urlVal = baseUrl + queryParam;
 
-      const underReplicatedResponseData = allContainers && 
allContainers.filter(item => item.containerState === 'UNDER_REPLICATED');
-      const uContainers: IContainerResponse[] = underReplicatedResponseData;
-
-      const overReplicatedResponseData = allContainers && 
allContainers.filter(item => item.containerState === 'OVER_REPLICATED');
-      const oContainers: IContainerResponse[] = overReplicatedResponseData;
+    const { request, controller } = AxiosGetHelper(urlVal, 
cancelContainerSignal);
+    cancelContainerSignal = controller;
 
-      const misReplicatedResponseData = allContainers && 
allContainers.filter(item => item.containerState === 'MIS_REPLICATED');
-      const mrContainers: IContainerResponse[] = misReplicatedResponseData;
+    request.then(allContainersResponse => {
+      const responseData: IUnhealthyContainersResponse = 
allContainersResponse.data;
+      const newContainers = responseData.containers;
 
-      this.setState({
+      // Use functional setState to avoid race conditions and correctly handle 
empty responses
+      this.setState(prevState => ({
         loading: false,
-        missingDataSource: mContainers,
-        underReplicatedDataSource: uContainers,
-        overReplicatedDataSource: oContainers,
-        misReplicatedDataSource: mrContainers
-      });
+        containerDataSource: newContainers,
+        missingCount: responseData.missingCount,
+        misReplicatedCount: responseData.misReplicatedCount,
+        underReplicatedCount: responseData.underReplicatedCount,
+        overReplicatedCount: responseData.overReplicatedCount,
+        firstSeenKey: newContainers.length > 0 ? Number(responseData.firstKey) 
: prevState.firstSeenKey,
+        lastSeenKey: newContainers.length > 0 ? Number(responseData.lastKey) : 
prevState.lastSeenKey
+      }));
     }).catch(error => {
       this.setState({
         loading: false
@@ -278,6 +289,10 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
     });
   }
 
+  componentDidMount(): void {
+    this.changeTab('1');
+  }
+
   componentWillUnmount(): void {
     cancelRequests([
       cancelContainerSignal,
@@ -285,23 +300,19 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
     ]);
   }
 
-  onShowSizeChange = (current: number, pageSize: number) => {
-    console.log(current, pageSize);
-  };
-
   onRowExpandClick = (expanded: boolean, record: IContainerResponse) => {
     if (expanded) {
       this.setState(({ expandedRowData }) => {
         const expandedRowState: IExpandedRowState = 
expandedRowData[record.containerID]
-          ? Object.assign({}, expandedRowData[record.containerID], { loading: 
true })
-          : {
-            containerId: record.containerID,
-            loading: true,
-            dataSource: [],
-            totalCount: 0
-          };
+            ? { ...expandedRowData[record.containerID], loading: true }
+            : {
+              containerId: record.containerID,
+              loading: true,
+              dataSource: [],
+              totalCount: 0
+            };
         return {
-          expandedRowData: Object.assign({}, expandedRowData, { 
[record.containerID]: expandedRowState })
+          expandedRowData: { ...expandedRowData, [record.containerID]: 
expandedRowState }
         };
       });
 
@@ -311,20 +322,24 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
       request.then(response => {
         const containerKeysResponse: IContainerKeysResponse = response.data;
         this.setState(({ expandedRowData }) => {
-          const expandedRowState: IExpandedRowState =
-            Object.assign({}, expandedRowData[record.containerID],
-              { loading: false, dataSource: containerKeysResponse.keys, 
totalCount: containerKeysResponse.totalCount });
+          const expandedRowState: IExpandedRowState = {
+            ...expandedRowData[record.containerID],
+            loading: false,
+            dataSource: containerKeysResponse.keys,
+            totalCount: containerKeysResponse.totalCount
+          };
           return {
-            expandedRowData: Object.assign({}, expandedRowData, { 
[record.containerID]: expandedRowState })
+            expandedRowData: { ...expandedRowData, [record.containerID]: 
expandedRowState }
           };
         });
       }).catch(error => {
         this.setState(({ expandedRowData }) => {
-          const expandedRowState: IExpandedRowState =
-            Object.assign({}, expandedRowData[record.containerID],
-              { loading: false });
+          const expandedRowState: IExpandedRowState = {
+            ...expandedRowData[record.containerID],
+            loading: false
+          };
           return {
-            expandedRowData: Object.assign({}, expandedRowData, { 
[record.containerID]: expandedRowState })
+            expandedRowData: { ...expandedRowData, [record.containerID]: 
expandedRowState }
           };
         });
         showDataFetchError(error.toString());
@@ -341,23 +356,32 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
     if (expandedRowData[containerId]) {
       const containerKeys: IExpandedRowState = expandedRowData[containerId];
       const dataSource = containerKeys.dataSource.map(record => (
-        { ...record, uid: `${record.Volume}/${record.Bucket}/${record.Key}` }
+          { ...record, uid: `${record.Volume}/${record.Bucket}/${record.Key}` }
       ));
       const paginationConfig: TablePaginationConfig = {
         showTotal: (total: number, range) => `${range[0]}-${range[1]} of 
${total} keys`
       };
       return (
-        <Table
-          loading={containerKeys.loading} dataSource={dataSource}
-          columns={KEY_TABLE_COLUMNS} pagination={paginationConfig}
-          rowKey='uid'
-          locale={{ filterTitle: '' }} />
+          <Table
+              loading={containerKeys.loading} dataSource={dataSource}
+              columns={KEY_TABLE_COLUMNS} pagination={paginationConfig}
+              rowKey='uid'
+              locale={{ filterTitle: '' }} />
       );
     }
 
     return <div>Loading...</div>;
   };
 
+  onShowSizeChange = (_: number, pageSize: number) => {
+    this.setState((prevState) => ({
+      pageSize: pageSize,
+      lastSeenKey: prevState.firstSeenKey - 1 // We want to stay on the 
current page range and only change size
+    }), () => {
+      this.fetchNextRecords();
+    });
+  }
+
   searchColumn = () => {
     return CONTAINER_TAB_COLUMNS.reduce<any[]>((filtered, column) => {
       if (column.isSearchable) {
@@ -374,58 +398,111 @@ export class MissingContainers extends 
React.Component<Record<string, object>, I
     }, [])
   };
 
+
+  fetchPreviousRecords = () => {
+    const { pageSize, lastSeenKey, firstSeenKey, currentState } = this.state;
+    this.fetchUnhealthyContainers(false, { pageSize, lastSeenKey, 
firstSeenKey, currentState });
+  }
+
+  fetchNextRecords = () => {
+    const { pageSize, lastSeenKey, firstSeenKey, currentState } = this.state;
+    this.fetchUnhealthyContainers(true, { pageSize, lastSeenKey, firstSeenKey, 
currentState });
+  }
+
+  itemRender = (_: any, type: string, originalElement: any) => {
+    if (type === 'prev') {
+      return <div><Link to="#" 
onClick={this.fetchPreviousRecords}>{'<'}</Link></div>;
+    }
+    if (type === 'next') {
+      return <div><Link to="#" onClick={this.fetchNextRecords}> {'>'} 
</Link></div>;
+    }
+    return originalElement;
+  };
+
+
+  changeTab = (activeKey: any) => {
+    let currentState = "MISSING";
+    if (activeKey === '2') {
+      currentState = "UNDER_REPLICATED";
+    } else if (activeKey === '3') {
+      currentState = "OVER_REPLICATED";
+    } else if (activeKey === '4') {
+      currentState = "MIS_REPLICATED";
+    }
+    this.setState({
+      containerDataSource: [],
+      expandedRowData: {},
+      missingCount: 0,
+      underReplicatedCount: 0,
+      overReplicatedCount: 0,
+      misReplicatedCount: 0,
+      currentState: currentState,
+      firstSeenKey: 0,
+      lastSeenKey: 0
+    }, () => {
+      this.fetchNextRecords();
+    });
+  }
+
+
   render() {
-    const { missingDataSource, loading, underReplicatedDataSource, 
overReplicatedDataSource, misReplicatedDataSource } = this.state;
+    const {containerDataSource, loading,
+      missingCount, misReplicatedCount, overReplicatedCount, 
underReplicatedCount} = this.state;
+
+    // --- MODIFIED PAGINATION CONFIG ---
     const paginationConfig: TablePaginationConfig = {
-      showTotal: (total: number, range) => `${range[0]}-${range[1]} of 
${total} missing containers`,
+      pageSize: this.state.pageSize,
+      pageSizeOptions: ['10', '20', '30', '50'],
       showSizeChanger: true,
-      onShowSizeChange: this.onShowSizeChange
+      itemRender: this.itemRender,
+      onShowSizeChange: this.onShowSizeChange,
+      // Removed current and onChange as they are not needed for cursor-based 
pagination
     };
 
-    const generateTable = (dataSource) => {
+    const generateTable = (dataSource: IContainerResponse[]) => {
       return <Table
-        expandable={{
-          expandRowByClick: true,
-          expandedRowRender: this.expandedRowRender,
-          onExpand: this.onRowExpandClick
-        }}
-        dataSource={dataSource}
-        columns={this.searchColumn()}
-        loading={loading}
-        pagination={paginationConfig} rowKey='containerID'
-        locale={{ filterTitle: "" }} />
+          expandable={{
+            expandRowByClick: true,
+            expandedRowRender: this.expandedRowRender,
+            onExpand: this.onRowExpandClick
+          }}
+          dataSource={dataSource}
+          columns={this.searchColumn()}
+          loading={loading}
+          pagination={paginationConfig} rowKey='containerID'
+          locale={{ filterTitle: "" }} />
     }
 
     return (
-      <div className='missing-containers-container'>
-        <div className='page-header'>
-          Containers
-        </div>
-        <div className='content-div'>
-          <Tabs defaultActiveKey='1'>
-            <Tabs.TabPane
-              key='1'
-              tab={`Missing (${missingDataSource?.length ?? 0})`}>
-              {generateTable(missingDataSource)}
-            </Tabs.TabPane>
-            <Tabs.TabPane
-              key='2'
-              tab={`Under-Replicated (${underReplicatedDataSource?.length ?? 
0})`}>
-              {generateTable(underReplicatedDataSource)}
-            </Tabs.TabPane>
-            <Tabs.TabPane
-              key='3'
-              tab={`Over-Replicated (${overReplicatedDataSource?.length ?? 
0})`}>
-              {generateTable(overReplicatedDataSource)}
-            </Tabs.TabPane>
-            <Tabs.TabPane
-              key='4'
-              tab={`Mis-Replicated (${misReplicatedDataSource?.length ?? 0})`}>
-              {generateTable(misReplicatedDataSource)}
-            </Tabs.TabPane>
-          </Tabs>
+        <div className='missing-containers-container'>
+          <div className='page-header'>
+            Containers
+          </div>
+          <div className='content-div'>
+            <Tabs defaultActiveKey='1' onChange={this.changeTab}>
+              <Tabs.TabPane
+                  key='1'
+                  tab={`Missing${(missingCount > 0) ? ` (${missingCount})` : 
''}`}>
+                {generateTable(containerDataSource)}
+              </Tabs.TabPane>
+              <Tabs.TabPane
+                  key='2'
+                  tab={`Under-Replicated${(underReplicatedCount > 0) ? ` 
(${underReplicatedCount})` : ''}`}>
+                {generateTable(containerDataSource)}
+              </Tabs.TabPane>
+              <Tabs.TabPane
+                  key='3'
+                  tab={`Over-Replicated${(overReplicatedCount > 0) ? ` 
(${overReplicatedCount})` : ''}`}>
+                {generateTable(containerDataSource)}
+              </Tabs.TabPane>
+              <Tabs.TabPane
+                  key='4'
+                  tab={`Mis-Replicated${(misReplicatedCount > 0) ? ` 
(${misReplicatedCount})` : ''}`}>
+                {generateTable(containerDataSource)}
+              </Tabs.TabPane>
+            </Tabs>
+          </div>
         </div>
-      </div>
     );
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to