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]