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 e00f7ae960 HDDS-11159. Improve Containers page UI (#7267)
e00f7ae960 is described below

commit e00f7ae960ef2c5a5efc8a439e38f5931d8be383
Author: Abhishek Pal <[email protected]>
AuthorDate: Tue Oct 8 16:08:50 2024 +0530

    HDDS-11159. Improve Containers page UI (#7267)
---
 .../webapps/recon/ozone-recon-web/api/db.json      |  14 +-
 .../src/constants/breadcrumbs.constants.tsx        |   1 +
 .../src/v2/components/tables/bucketsTable.tsx      |   2 +-
 .../src/v2/components/tables/containersTable.tsx   | 259 +++++++++++++++++++
 .../src/v2/pages/buckets/buckets.tsx               |   2 +-
 .../src/v2/pages/containers/containers.less        |  50 ++++
 .../src/v2/pages/containers/containers.tsx         | 283 +++++++++++++++++++++
 .../src/v2/pages/overview/overview.tsx             |  21 +-
 .../recon/ozone-recon-web/src/v2/routes-v2.tsx     |   5 +
 .../src/v2/types/container.types.ts                |  94 +++++++
 .../ozone-recon-web/src/v2/utils/momentUtils.ts    |   5 +
 11 files changed, 713 insertions(+), 23 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
index f1d5dc3670..c620202767 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
@@ -1923,7 +1923,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 1,
         "reason": null,
-        "keys": 1,
+        "keys": 4,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
         "replicas": [
           {
@@ -1997,7 +1997,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 3,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
         "replicas": [
           {
@@ -2071,7 +2071,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 2,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
         "replicas": [
           {
@@ -2108,7 +2108,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 5,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
         "replicas": [
           {
@@ -2145,7 +2145,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 3,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
         "replicas": [
           {
@@ -2182,7 +2182,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 6,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a2",
         "replicas": [
           {
@@ -2219,7 +2219,7 @@
         "actualReplicaCount": 2,
         "replicaDeltaCount": 2,
         "reason": null,
-        "keys": 1,
+        "keys": 2,
         "pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a3",
         "replicas": [
           {
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
index 88953a5ed7..bc81e86dcb 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
@@ -27,6 +27,7 @@ export const breadcrumbNameMap: IBreadcrumbNameMap = {
   '/Datanodes': 'Datanodes',
   '/Pipelines': 'Pipelines',
   '/MissingContainers': 'Missing Containers',
+  '/Containers': 'Containers',
   '/Insights': 'Insights',
   '/DiskUsage': 'Disk Usage',
   '/Heatmap': 'Heatmap',
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
index b26ae251f9..0060177795 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
@@ -255,7 +255,7 @@ const BucketsTable: React.FC<BucketsTableProps> = ({
         dataSource={getFilteredData(data)}
         columns={filterSelectedColumns()}
         loading={loading}
-        rowKey='volume'
+        rowKey={(record: Bucket) => `${record.volumeName}/${record.name}`}
         pagination={paginationConfig}
         scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
         locale={{ filterTitle: '' }}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
new file mode 100644
index 0000000000..1bb1b5456b
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/containersTable.tsx
@@ -0,0 +1,259 @@
+/*
+ * 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, { useRef } from 'react';
+import filesize from 'filesize';
+import { AxiosError } from 'axios';
+import { Popover, Table } from 'antd';
+import {
+  ColumnsType,
+  TablePaginationConfig
+} from 'antd/es/table';
+import { NodeIndexOutlined } from '@ant-design/icons';
+
+import { getFormattedTime } from '@/v2/utils/momentUtils';
+import { showDataFetchError } from '@/utils/common';
+import { AxiosGetHelper } from '@/utils/axiosRequestHelper';
+import {
+  Container, ContainerKeysResponse, ContainerReplica,
+  ContainerTableProps,
+  ExpandedRowState, KeyResponse
+} from '@/v2/types/container.types';
+
+const size = filesize.partial({ standard: 'iec' });
+
+export const COLUMNS: ColumnsType<Container> = [
+  {
+    title: 'Container ID',
+    dataIndex: 'containerID',
+    key: 'containerID',
+    sorter: (a: Container, b: Container) => a.containerID - b.containerID
+  },
+  {
+    title: 'No. of Keys',
+    dataIndex: 'keys',
+    key: 'keys',
+    sorter: (a: Container, b: Container) => a.keys - b.keys
+  },
+  {
+    title: 'Actual/Expected Replica(s)',
+    dataIndex: 'expectedReplicaCount',
+    key: 'expectedReplicaCount',
+    render: (expectedReplicaCount: number, record: Container) => {
+      const actualReplicaCount = record.actualReplicaCount;
+      return (
+        <span>
+          {actualReplicaCount} / {expectedReplicaCount}
+        </span>
+      );
+    }
+  },
+  {
+    title: 'Datanodes',
+    dataIndex: 'replicas',
+    key: 'replicas',
+    render: (replicas: ContainerReplica[]) => {
+      const renderDatanodes = (replicas: ContainerReplica[]) => {
+        return replicas?.map((replica: any, idx: number) => (
+          <div key={idx} className='datanode-container-v2'>
+            <NodeIndexOutlined /> {replica.datanodeHost}
+          </div>
+        ))
+      }
+
+      return (
+        <Popover
+          content={renderDatanodes(replicas)}
+          title='Datanodes'
+          placement='bottomRight'
+          trigger='hover'>
+          <strong>{replicas.length}</strong> datanodes
+        </Popover>
+      )
+    }
+  },
+  {
+    title: 'Pipeline ID',
+    dataIndex: 'pipelineID',
+    key: 'pipelineID'
+  },
+  {
+    title: 'Unhealthy Since',
+    dataIndex: 'unhealthySince',
+    key: 'unhealthySince',
+    render: (unhealthySince: number) => getFormattedTime(unhealthySince, 
'lll'),
+    sorter: (a: Container, b: Container) => a.unhealthySince - b.unhealthySince
+  }
+];
+
+const KEY_TABLE_COLUMNS: ColumnsType<KeyResponse> = [
+  {
+    title: 'Volume',
+    dataIndex: 'Volume',
+    key: 'Volume'
+  },
+  {
+    title: 'Bucket',
+    dataIndex: 'Bucket',
+    key: 'Bucket'
+  },
+  {
+    title: 'Key',
+    dataIndex: 'Key',
+    key: 'Key'
+  },
+  {
+    title: 'Size',
+    dataIndex: 'DataSize',
+    key: 'DataSize',
+    render: (dataSize: number) => <div>{size(dataSize)}</div>
+  },
+  {
+    title: 'Date Created',
+    dataIndex: 'CreationTime',
+    key: 'CreationTime',
+    render: (date: string) => getFormattedTime(date, 'lll')
+  },
+  {
+    title: 'Date Modified',
+    dataIndex: 'ModificationTime',
+    key: 'ModificationTime',
+    render: (date: string) => getFormattedTime(date, 'lll')
+  },
+  {
+    title: 'Path',
+    dataIndex: 'CompletePath',
+    key: 'path'
+  }
+];
+
+const ContainerTable: React.FC<ContainerTableProps> = ({
+  data,
+  loading,
+  selectedColumns,
+  expandedRow,
+  expandedRowSetter,
+  searchColumn = 'containerID',
+  searchTerm = ''
+}) => {
+
+  const cancelSignal = useRef<AbortController>();
+
+  function filterSelectedColumns() {
+    const columnKeys = selectedColumns.map((column) => column.value);
+    return COLUMNS.filter(
+      (column) => columnKeys.indexOf(column.key as string) >= 0
+    );
+  }
+
+  function loadRowData(containerID: number) {
+    const { request, controller } = AxiosGetHelper(
+      `/api/v1/containers/${containerID}/keys`,
+      cancelSignal.current
+    );
+    cancelSignal.current = controller;
+
+    request.then(response => {
+      const containerKeysResponse: ContainerKeysResponse = response.data;
+      expandedRowSetter({
+        ...expandedRow,
+        [containerID]: {
+          ...expandedRow[containerID],
+          loading: false,
+          dataSource: containerKeysResponse.keys,
+          totalCount: containerKeysResponse.totalCount
+        }
+      });
+    }).catch(error => {
+      expandedRowSetter({
+        ...expandedRow,
+        [containerID]: {
+          ...expandedRow[containerID],
+          loading: false
+        }
+      });
+      showDataFetchError((error as AxiosError).toString());
+    });
+  }
+
+  function getFilteredData(data: Container[]) {
+
+    return data?.filter(
+      (container: Container) => {
+        return (searchColumn === 'containerID')
+          ? container[searchColumn].toString().includes(searchTerm)
+          : container[searchColumn].includes(searchTerm)
+      }
+    ) ?? [];
+  }
+
+  function onRowExpandClick(expanded: boolean, record: Container) {
+    if (expanded) {
+      loadRowData(record.containerID);
+    }
+    else {
+      cancelSignal.current && cancelSignal.current.abort();
+    }
+  }
+
+  function expandedRowRender(record: Container) {
+    const containerId = record.containerID
+    const containerKeys: ExpandedRowState = expandedRow[containerId];
+    const dataSource = containerKeys?.dataSource ?? [];
+    const paginationConfig: TablePaginationConfig = {
+      showTotal: (total: number, range) => `${range[0]}-${range[1]} of 
${total} Keys`
+    }
+
+    return (
+      <Table
+        loading={containerKeys?.loading ?? true}
+        dataSource={dataSource}
+        columns={KEY_TABLE_COLUMNS}
+        pagination={paginationConfig}
+        rowKey={(record: KeyResponse) => 
`${record.Volume}/${record.Bucket}/${record.Key}`}
+        locale={{ filterTitle: '' }} />
+    )
+  };
+
+  const paginationConfig: TablePaginationConfig = {
+    showTotal: (total: number, range) => (
+      `${range[0]}-${range[1]} of ${total} Containers`
+    ),
+    showSizeChanger: true
+  };
+
+  return (
+    <div>
+      <Table
+        rowKey='containerID'
+        dataSource={getFilteredData(data)}
+        columns={filterSelectedColumns()}
+        loading={loading}
+        pagination={paginationConfig}
+        scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+        locale={{ filterTitle: '' }}
+        expandable={{
+          expandRowByClick: true,
+          expandedRowRender: expandedRowRender,
+          onExpand: onRowExpandClick
+        }} />
+    </div>
+  );
+}
+
+export default ContainerTable;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
index 1e2de307b1..1590f36a4a 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
@@ -26,6 +26,7 @@ import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
 import Search from '@/v2/components/search/search';
 import MultiSelect from '@/v2/components/select/multiSelect';
 import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
 
 import { AutoReloadHelper } from '@/utils/autoReloadHelper';
 import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper";
@@ -39,7 +40,6 @@ import {
 } from '@/v2/types/bucket.types';
 
 import './buckets.less';
-import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
 
 
 const LIMIT_OPTIONS: Option[] = [
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.less
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.less
new file mode 100644
index 0000000000..f6328ccee6
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.less
@@ -0,0 +1,50 @@
+/*
+* 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.
+*/
+
+.content-div {
+  min-height: unset;
+
+  .table-header-section {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .table-filter-section {
+      font-size: 14px;
+      font-weight: normal;
+      display: flex;
+      column-gap: 8px;
+      padding: 16px 8px;
+      align-items: center;
+    }
+  }
+}
+
+.highlight-content {
+  color: #989898;
+
+  .highlight-content-value {
+    color: #000000;
+    font-weight: 400;
+    font-size: 30px;
+  }
+}
+
+.datanode-container-v2 {
+ padding: 6px 0px; 
+}
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
new file mode 100644
index 0000000000..78f6424c6e
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx
@@ -0,0 +1,283 @@
+/*
+ * 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, { useRef, useState } from "react";
+import moment from "moment";
+import { AxiosError } from "axios";
+import { Card, Row, Tabs } from "antd";
+import { ValueType } from "react-select/src/types";
+
+import Search from "@/v2/components/search/search";
+import MultiSelect, { Option } from "@/v2/components/select/multiSelect";
+import ContainerTable, { COLUMNS } from 
"@/v2/components/tables/containersTable";
+import AutoReloadPanel from "@/components/autoReloadPanel/autoReloadPanel";
+import { showDataFetchError } from "@/utils/common";
+import { AutoReloadHelper } from "@/utils/autoReloadHelper";
+import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper";
+import { useDebounce } from "@/v2/hooks/debounce.hook";
+
+import {
+  Container,
+  ContainerState,
+  ExpandedRow
+} from "@/v2/types/container.types";
+
+import './containers.less';
+
+
+const SearchableColumnOpts = [{
+  label: 'Container ID',
+  value: 'containerID'
+}, {
+  label: 'Pipeline ID',
+  value: 'pipelineID'
+}]
+
+const defaultColumns = COLUMNS.map(column => ({
+  label: column.title as string,
+  value: column.key as string
+}));
+
+const Containers: React.FC<{}> = () => {
+
+  const cancelSignal = useRef<AbortController>();
+
+  const [state, setState] = useState<ContainerState>({
+    lastUpdated: 0,
+    columnOptions: defaultColumns,
+    missingContainerData: [],
+    underReplicatedContainerData: [],
+    overReplicatedContainerData: [],
+    misReplicatedContainerData: [],
+  });
+  const [expandedRow, setExpandedRow] = useState<ExpandedRow>({});
+
+  const [loading, setLoading] = useState<boolean>(false);
+  const [selectedColumns, setSelectedColumns] = 
useState<Option[]>(defaultColumns);
+  const [searchTerm, setSearchTerm] = useState<string>('');
+  const [selectedTab, setSelectedTab] = useState<string>('1');
+  const [searchColumn, setSearchColumn] = useState<'containerID' | 
'pipelineID'>('containerID');
+
+  const debouncedSearch = useDebounce(searchTerm, 300);
+
+  function loadData() {
+    setLoading(true);
+
+    const { request, controller } = AxiosGetHelper(
+      '/api/v1/containers/unhealthy',
+      cancelSignal.current
+    );
+
+    cancelSignal.current = controller;
+
+    request.then(response => {
+      const containers: Container[] = response.data.containers;
+
+      const missingContainerData: Container[] = containers?.filter(
+        container => container.containerState === 'MISSING'
+      ) ?? [];
+      const underReplicatedContainerData: Container[] = containers?.filter(
+        container => container.containerState === 'UNDER_REPLICATED'
+      ) ?? [];
+      const overReplicatedContainerData: Container[] = containers?.filter(
+        container => container.containerState === 'OVER_REPLICATED'
+      ) ?? [];
+      const misReplicatedContainerData: Container[] = containers?.filter(
+        container => container.containerState === 'MIS_REPLICATED'
+      ) ?? [];
+
+      setState({
+        ...state,
+        missingContainerData: missingContainerData,
+        underReplicatedContainerData: underReplicatedContainerData,
+        overReplicatedContainerData: overReplicatedContainerData,
+        misReplicatedContainerData: misReplicatedContainerData,
+        lastUpdated: Number(moment())
+      });
+      setLoading(false)
+    }).catch(error => {
+      setLoading(false);
+      showDataFetchError((error as AxiosError).toString());
+    });
+  }
+
+  function handleColumnChange(selected: ValueType<Option, true>) {
+    setSelectedColumns(selected as Option[]);
+  }
+
+  const autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+  React.useEffect(() => {
+    autoReloadHelper.startPolling();
+    loadData();
+
+    return (() => {
+      autoReloadHelper.stopPolling();
+      cancelRequests([cancelSignal.current!])
+    })
+  }, []);
+
+  const {
+    lastUpdated, columnOptions,
+    missingContainerData, underReplicatedContainerData,
+    overReplicatedContainerData, misReplicatedContainerData
+  } = state;
+
+  // Mapping the data to the Tab keys for enabling/disabling search
+  const dataToTabKeyMap: Record<string, Container[]> = {
+    1: missingContainerData,
+    2: underReplicatedContainerData,
+    3: overReplicatedContainerData,
+    4: misReplicatedContainerData
+  }
+
+  const highlightData = (
+    <div style={{
+        display: 'flex',
+        width: '90%',
+        justifyContent: 'space-between'
+      }}>
+      <div className='highlight-content'>
+        Missing <br/>
+        <span 
className='highlight-content-value'>{missingContainerData?.length ?? 
'N/A'}</span>
+      </div>
+      <div className='highlight-content'>
+        Under-Replicated <br/>
+        <span 
className='highlight-content-value'>{underReplicatedContainerData?.length ?? 
'N/A'}</span>
+      </div>
+      <div className='highlight-content'>
+        Over-Replicated <br/>
+        <span 
className='highlight-content-value'>{overReplicatedContainerData?.length ?? 
'N/A'}</span>
+      </div>
+      <div className='highlight-content'>
+        Mis-Replicated <br/>
+        <span 
className='highlight-content-value'>{misReplicatedContainerData?.length ?? 
'N/A'}</span>
+      </div>
+    </div>
+  )
+
+  return (
+    <>
+      <div className='page-header-v2'>
+        Containers
+        <AutoReloadPanel
+          isLoading={loading}
+          lastRefreshed={lastUpdated}
+          togglePolling={autoReloadHelper.handleAutoReloadToggle}
+          onReload={loadData}
+        />
+      </div>
+      <div style={{ padding: '24px' }}>
+        <div style={{ marginBottom: '12px' }}>
+          <Card
+            title='Highlights'
+            loading={loading}>
+              <Row
+                align='middle'>
+                  {highlightData}
+                </Row>
+          </Card>
+        </div>
+        <div className='content-div'>
+          <div className='table-header-section'>
+            <div className='table-filter-section'>
+              <MultiSelect
+                options={columnOptions}
+                defaultValue={selectedColumns}
+                selected={selectedColumns}
+                placeholder='Columns'
+                onChange={handleColumnChange}
+                fixedColumn='containerID'
+                onTagClose={() => { }}
+                columnLength={columnOptions.length} />
+            </div>
+            <Search
+              disabled={dataToTabKeyMap[selectedTab]?.length < 1}
+              searchOptions={SearchableColumnOpts}
+              searchInput={searchTerm}
+              searchColumn={searchColumn}
+              onSearchChange={
+                (e: React.ChangeEvent<HTMLInputElement>) => 
setSearchTerm(e.target.value)
+              }
+              onChange={(value) => {
+                setSearchTerm('');
+                setSearchColumn(value as 'containerID' | 'pipelineID');
+              }} />
+          </div>
+          <Tabs defaultActiveKey='1'
+            onChange={(activeKey: string) => setSelectedTab(activeKey)}>
+            <Tabs.TabPane
+              key='1'
+              tab='Missing'>
+              <ContainerTable
+                data={missingContainerData}
+                loading={loading}
+                searchColumn={searchColumn}
+                searchTerm={debouncedSearch}
+                selectedColumns={selectedColumns}
+                expandedRow={expandedRow}
+                expandedRowSetter={setExpandedRow}
+              />
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key='2'
+              tab='Under-Replicated'>
+              <ContainerTable
+                data={underReplicatedContainerData}
+                loading={loading}
+                searchColumn={searchColumn}
+                searchTerm={debouncedSearch}
+                selectedColumns={selectedColumns}
+                expandedRow={expandedRow}
+                expandedRowSetter={setExpandedRow}
+              />
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key='3'
+              tab='Over-Replicated'>
+              <ContainerTable
+                data={overReplicatedContainerData}
+                loading={loading}
+                searchColumn={searchColumn}
+                searchTerm={debouncedSearch}
+                selectedColumns={selectedColumns}
+                expandedRow={expandedRow}
+                expandedRowSetter={setExpandedRow}
+              />
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key='4'
+              tab='Mis-Replicated'>
+              <ContainerTable
+                data={misReplicatedContainerData}
+                loading={loading}
+                searchColumn={searchColumn}
+                searchTerm={debouncedSearch}
+                selectedColumns={selectedColumns}
+                expandedRow={expandedRow}
+                expandedRowSetter={setExpandedRow}
+              />
+            </Tabs.TabPane>
+          </Tabs>
+        </div>
+      </div>
+    </>
+  );
+}
+
+export default Containers;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
index 0394c8ac51..1568cb4b3e 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
@@ -322,20 +322,13 @@ const Overview: React.FC<{}> = () => {
     </Button>
   )
 
-  const containersLink = (missingContainersCount > 0)
-    ? (
-      <Button
-        type='link'
-        size='small'>
-        <Link to='/MissingContainers'> View More</Link>
-      </Button>
-    ) : (
-      <Button
-        type='link'
-        size='small'>
-        <Link to='/Containers'> View More</Link>
-      </Button>
-    )
+  const containersLink = (
+    <Button
+      type='link'
+      size='small'>
+      <Link to='/Containers'> View More</Link>
+    </Button>
+  )
 
   return (
     <>
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
index 20907fd3ad..942ad16dcd 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
@@ -23,6 +23,7 @@ const Buckets = lazy(() => 
import('@/v2/pages/buckets/buckets'));
 const Datanodes = lazy(() => import('@/v2/pages/datanodes/datanodes'));
 const Pipelines = lazy(() => import('@/v2/pages/pipelines/pipelines'));
 const DiskUsage = lazy(() => import('@/v2/pages/diskUsage/diskUsage'));
+const Containers = lazy(() => import('@/v2/pages/containers/containers'));
 
 export const routesV2 = [
   {
@@ -48,5 +49,9 @@ export const routesV2 = [
   {
     path: '/DiskUsage',
     component: DiskUsage
+  },
+  {
+    path: '/Containers',
+    component: Containers
   }
 ];
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
new file mode 100644
index 0000000000..2467a0f26f
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/container.types.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { Option } from "@/v2/components/select/multiSelect";
+
+export type ContainerReplica = {
+  containerId: number;
+  datanodeUuid: string;
+  datanodeHost: string;
+  firstSeenTime: number;
+  lastSeenTime: number;
+  lastBcsId: number;
+}
+
+export type Container = {
+  containerID: number;
+  containerState: string;
+  unhealthySince: number;
+  expectedReplicaCount: number;
+  actualReplicaCount: number;
+  replicaDeltaCount: number;
+  reason: string;
+  keys: number;
+  pipelineID: string;
+  replicas: ContainerReplica[];
+}
+
+type KeyResponseBlock = {
+  containerID: number;
+  localID: number;
+}
+
+export type KeyResponse = {
+  Volume: string;
+  Bucket: string;
+  Key: string;
+  DataSize: number;
+  CompletePath: string;
+  Versions: number[];
+  Blocks: Record<number, KeyResponseBlock[]>;
+  CreationTime: string;
+  ModificationTime: string;
+}
+
+export type ContainerKeysResponse = {
+  totalCount: number;
+  keys: KeyResponse[];
+}
+
+export type ContainerTableProps = {
+  loading: boolean;
+  data: Container[];
+  searchColumn: 'containerID' | 'pipelineID';
+  searchTerm: string;
+  selectedColumns: Option[];
+  expandedRow: ExpandedRow;
+  expandedRowSetter: (arg0: ExpandedRow) => void;
+}
+
+
+export type ExpandedRow = {
+  [key: number]: ExpandedRowState;
+}
+
+export type ExpandedRowState = {
+  loading: boolean;
+  containerId: number;
+  dataSource: KeyResponse[];
+  totalCount: number;
+}
+
+export type ContainerState = {
+  lastUpdated: number;
+  columnOptions: Option[];
+  missingContainerData: Container[];
+  underReplicatedContainerData: Container[];
+  overReplicatedContainerData: Container[];
+  misReplicatedContainerData: Container[];
+}
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
index fb553d0db3..daaae2d54d 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
@@ -61,3 +61,8 @@ export function getDurationFromTimestamp(timestamp: number): 
string {
 
   return (elapsedTime.length === 0) ? 'Just now' : elapsedTime.join(' ');
 }
+
+export function getFormattedTime(time: number | string, format: string) {
+  if (typeof time === 'string') return moment(time).format(format);
+  return (time > 0) ? moment(time).format(format) : 'N/A';
+}


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

Reply via email to