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 db30871a5a44..9156d68326ec 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 ( - + {actualReplicaCount} / {expectedReplicaCount} ); @@ -160,30 +163,30 @@ const CONTAINER_TAB_COLUMNS = [ dataIndex: 'replicas', key: 'replicas', render: (replicas: IContainerReplicas[]) => ( -
- {replicas && replicas.map(replica => { - const tooltip = ( -
-
First Report Time: {timeFormat(replica.firstSeenTime)}
-
Last Report Time: {timeFormat(replica.lastSeenTime)}
-
- ); - return ( -
- - - - +
+ {replicas && replicas.map(replica => { + const tooltip = ( +
+
First Report Time: {timeFormat(replica.firstSeenTime)}
+
Last Report Time: {timeFormat(replica.lastSeenTime)}
+
+ ); + return ( +
+ + + + {replica.datanodeHost} -
- ); - } - )} -
+
+ ); + } + )} +
) }, { @@ -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, 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, I }); } + componentDidMount(): void { + this.changeTab('1'); + } + componentWillUnmount(): void { cancelRequests([ cancelContainerSignal, @@ -285,23 +300,19 @@ export class MissingContainers extends React.Component, 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, 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, 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 ( - +
); } return
Loading...
; }; + 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((filtered, column) => { if (column.isSearchable) { @@ -374,58 +398,111 @@ export class MissingContainers extends React.Component, 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
{'<'}
; + } + if (type === 'next') { + return
{'>'}
; + } + 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
+ expandable={{ + expandRowByClick: true, + expandedRowRender: this.expandedRowRender, + onExpand: this.onRowExpandClick + }} + dataSource={dataSource} + columns={this.searchColumn()} + loading={loading} + pagination={paginationConfig} rowKey='containerID' + locale={{ filterTitle: "" }} /> } return ( -
-
- Containers -
-
- - - {generateTable(missingDataSource)} - - - {generateTable(underReplicatedDataSource)} - - - {generateTable(overReplicatedDataSource)} - - - {generateTable(misReplicatedDataSource)} - - +
+
+ Containers +
+
+ + 0) ? ` (${missingCount})` : ''}`}> + {generateTable(containerDataSource)} + + 0) ? ` (${underReplicatedCount})` : ''}`}> + {generateTable(containerDataSource)} + + 0) ? ` (${overReplicatedCount})` : ''}`}> + {generateTable(containerDataSource)} + + 0) ? ` (${misReplicatedCount})` : ''}`}> + {generateTable(containerDataSource)} + + +
-
); } }