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 7a26afff5a HDDS-11158. Improve Pipelines page UI (#7171)
7a26afff5a is described below

commit 7a26afff5a06da2876d53cce69bc79efa1953251
Author: Abhishek Pal <[email protected]>
AuthorDate: Wed Sep 18 13:50:00 2024 +0530

    HDDS-11158. Improve Pipelines page UI (#7171)
---
 .../src/v2/components/search/search.tsx            |   2 +
 .../src/v2/components/tables/bucketsTable.tsx      | 267 +++++++++++++++++++++
 .../src/v2/components/tables/pipelinesTable.tsx    | 211 ++++++++++++++++
 .../src/v2/components/tables/volumesTable.tsx      | 179 ++++++++++++++
 .../src/v2/pages/buckets/buckets.tsx               | 236 +-----------------
 .../pipelines/pipelines.less}                      |  48 ++--
 .../src/v2/pages/pipelines/pipelines.tsx           | 160 ++++++++++++
 .../src/v2/pages/volumes/volumes.tsx               | 147 ++----------
 .../recon/ozone-recon-web/src/v2/routes-v2.tsx     |   5 +
 .../ozone-recon-web/src/v2/types/bucket.types.ts   |  12 +-
 .../src/v2/types/pipelines.types.ts                |  62 +++++
 .../ozone-recon-web/src/v2/types/volume.types.ts   |  10 +-
 .../ozone-recon-web/src/v2/utils/momentUtils.ts    |  63 +++++
 13 files changed, 1017 insertions(+), 385 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
index 8cac2a9c04..d320fd659a 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
@@ -20,6 +20,7 @@ import React from 'react';
 import { Input, Select } from 'antd';
 
 import { Option } from '@/v2/components/select/singleSelect';
+import { DownOutlined } from '@ant-design/icons';
 
 // ------------- Types -------------- //
 type SearchProps = {
@@ -51,6 +52,7 @@ const Search: React.FC<SearchProps> = ({
   const selectFilter = searchColumn
     ? (<Select
       disabled={disabled}
+      suffixIcon={(searchOptions.length > 1) ? <DownOutlined/> : null}
       defaultValue={searchColumn}
       options={searchOptions}
       onChange={onChange} />)
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
new file mode 100644
index 0000000000..b26ae251f9
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import moment from 'moment';
+import Table, {
+  ColumnProps,
+  ColumnsType,
+  TablePaginationConfig
+} from 'antd/es/table';
+import Tag from 'antd/es/tag';
+import {
+  CheckCircleOutlined,
+  CloseCircleOutlined,
+  CloudServerOutlined,
+  FileUnknownOutlined,
+  HddOutlined,
+  LaptopOutlined,
+  SaveOutlined
+} from '@ant-design/icons';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import { nullAwareLocaleCompare } from '@/utils/common';
+import {
+  Bucket,
+  BucketLayout,
+  BucketLayoutTypeList,
+  BucketsTableProps,
+  BucketStorage,
+  BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+function renderIsVersionEnabled(isVersionEnabled: boolean) {
+  return isVersionEnabled
+    ? <CheckCircleOutlined
+      style={{ color: '#1da57a' }}
+      className='icon-success' />
+    : <CloseCircleOutlined className='icon-neutral' />
+};
+
+function renderStorageType(bucketStorage: BucketStorage) {
+  const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+    RAM_DISK: <LaptopOutlined />,
+    SSD: <SaveOutlined />,
+    DISK: <HddOutlined />,
+    ARCHIVE: <CloudServerOutlined />
+  };
+  const icon = bucketStorage in bucketStorageIconMap
+    ? bucketStorageIconMap[bucketStorage]
+    : <FileUnknownOutlined />;
+  return <span>{icon} {bucketStorage}</span>;
+};
+
+function renderBucketLayout(bucketLayout: BucketLayout) {
+  const bucketLayoutColorMap = {
+    FILE_SYSTEM_OPTIMIZED: 'green',
+    OBJECT_STORE: 'orange',
+    LEGACY: 'blue'
+  };
+  const color = bucketLayout in bucketLayoutColorMap ?
+    bucketLayoutColorMap[bucketLayout] : '';
+  return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+export const COLUMNS: ColumnsType<Bucket> = [
+  {
+    title: 'Bucket',
+    dataIndex: 'name',
+    key: 'name',
+    sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+    defaultSortOrder: 'ascend' as const
+  },
+  {
+    title: 'Volume',
+    dataIndex: 'volumeName',
+    key: 'volumeName',
+    sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+    defaultSortOrder: 'ascend' as const
+  },
+  {
+    title: 'Owner',
+    dataIndex: 'owner',
+    key: 'owner',
+    sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+  },
+  {
+    title: 'Versioning',
+    dataIndex: 'versioning',
+    key: 'isVersionEnabled',
+    render: (isVersionEnabled: boolean) => 
renderIsVersionEnabled(isVersionEnabled)
+  },
+  {
+    title: 'Storage Type',
+    dataIndex: 'storageType',
+    key: 'storageType',
+    filterMultiple: true,
+    filters: BucketStorageTypeList.map(state => ({ text: state, value: state 
})),
+    onFilter: (value, record: Bucket) => record.storageType === value,
+    sorter: (a: Bucket, b: Bucket) => 
a.storageType.localeCompare(b.storageType),
+    render: (storageType: BucketStorage) => renderStorageType(storageType)
+  },
+  {
+    title: 'Bucket Layout',
+    dataIndex: 'bucketLayout',
+    key: 'bucketLayout',
+    filterMultiple: true,
+    filters: BucketLayoutTypeList.map(state => ({ text: state, value: state 
})),
+    onFilter: (value, record: Bucket) => record.bucketLayout === value,
+    sorter: (a: Bucket, b: Bucket) => 
a.bucketLayout.localeCompare(b.bucketLayout),
+    render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+  },
+  {
+    title: 'Creation Time',
+    dataIndex: 'creationTime',
+    key: 'creationTime',
+    sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+    render: (creationTime: number) => {
+      return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+    }
+  },
+  {
+    title: 'Modification Time',
+    dataIndex: 'modificationTime',
+    key: 'modificationTime',
+    sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+    render: (modificationTime: number) => {
+      return modificationTime > 0 ? moment(modificationTime).format('ll LTS') 
: 'NA';
+    }
+  },
+  {
+    title: 'Storage Capacity',
+    key: 'quotaCapacityBytes',
+    sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+    render: (text: string, record: Bucket) => (
+      <QuotaBar
+        quota={record.quotaInBytes}
+        used={record.usedBytes}
+        quotaType='size'
+      />
+    )
+  },
+  {
+    title: 'Namespace Capacity',
+    key: 'namespaceCapacity',
+    sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+    render: (text: string, record: Bucket) => (
+      <QuotaBar
+        quota={record.quotaInNamespace}
+        used={record.usedNamespace}
+        quotaType='namespace'
+      />
+    )
+  },
+  {
+    title: 'Source Volume',
+    dataIndex: 'sourceVolume',
+    key: 'sourceVolume',
+    render: (sourceVolume: string) => {
+      return sourceVolume ? sourceVolume : 'NA';
+    }
+  },
+  {
+    title: 'Source Bucket',
+    dataIndex: 'sourceBucket',
+    key: 'sourceBucket',
+    render: (sourceBucket: string) => {
+      return sourceBucket ? sourceBucket : 'NA';
+    }
+  }
+];
+
+const BucketsTable: React.FC<BucketsTableProps> = ({
+  loading = false,
+  data,
+  handleAclClick,
+  selectedColumns,
+  searchColumn = 'name',
+  searchTerm = ''
+}) => {
+
+  React.useEffect(() => {
+    const aclColumn: ColumnProps<Bucket> = {
+      title: 'ACLs',
+      dataIndex: 'acls',
+      key: 'acls',
+      render: (_: any, record: Bucket) => {
+        return (
+          <a
+            key='acl'
+            onClick={() => {
+              handleAclClick(record);
+            }}
+          >
+            Show ACL
+          </a>
+        );
+      }
+    };
+
+    if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
+      // Push the ACL column for initial load
+      COLUMNS.push(aclColumn);
+      selectedColumns.push({
+        label: aclColumn.title as string,
+        value: aclColumn.key as string
+      });
+    } else {
+      // Replace old ACL column with new ACL column with correct reference
+      // e.g. After page is reloaded / redirect from other page
+      COLUMNS[COLUMNS.length - 1] = aclColumn;
+      selectedColumns[selectedColumns.length - 1] = {
+        label: aclColumn.title as string,
+        value: aclColumn.key as string
+      }
+    }
+  }, []);
+
+  function filterSelectedColumns() {
+    const columnKeys = selectedColumns.map((column) => column.value);
+    return COLUMNS.filter(
+      (column) => columnKeys.indexOf(column.key as string) >= 0
+    )
+  }
+
+  function getFilteredData(data: Bucket[]) {
+    return data.filter(
+      (bucket: Bucket) => bucket[searchColumn].includes(searchTerm)
+    );
+  }
+
+  const paginationConfig: TablePaginationConfig = {
+    showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} 
buckets`,
+    showSizeChanger: true
+  };
+
+  return (
+    <div>
+      <Table
+        dataSource={getFilteredData(data)}
+        columns={filterSelectedColumns()}
+        loading={loading}
+        rowKey='volume'
+        pagination={paginationConfig}
+        scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+        locale={{ filterTitle: '' }}
+      />
+    </div>
+  )
+}
+
+export default BucketsTable;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
new file mode 100644
index 0000000000..6c07749436
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import Table, {
+  ColumnsType,
+  TablePaginationConfig
+} from 'antd/es/table';
+import Tooltip from 'antd/es/tooltip';
+import { InfoCircleOutlined } from '@ant-design/icons';
+
+import { ReplicationIcon } from '@/utils/themeIcons';
+import { getDurationFromTimestamp, getTimeDiffFromTimestamp } from 
'@/v2/utils/momentUtils';
+import { Pipeline, PipelinesTableProps, PipelineStatusList } from 
'@/v2/types/pipelines.types';
+
+
+// TODO: When Datanodes PR gets merged remove these declarations
+// And import from datanodes.types
+
+type SummaryDatanodeDetails = {
+  level: number;
+  parent: unknown | null;
+  cost: number;
+  uuid: string;
+  uuidString: string;
+  ipAddress: string;
+  hostName: string;
+  ports: {
+    name: string;
+    value: number
+  }[];
+  certSerialId: null,
+  version: string | null;
+  setupTime: number;
+  revision: string | null;
+  buildDate: string;
+  persistedOpState: string;
+  persistedOpStateExpiryEpochSec: number;
+  initialVersion: number;
+  currentVersion: number;
+  signature: number;
+  decommissioned: boolean;
+  networkName: string;
+  networkLocation: string;
+  networkFullPath: string;
+  numOfLeaves: number;
+}
+
+export const COLUMNS: ColumnsType<Pipeline> = [
+  {
+    title: 'Pipeline ID',
+    dataIndex: 'pipelineId',
+    key: 'pipelineId',
+    sorter: (a: Pipeline, b: Pipeline) => 
a.pipelineId.localeCompare(b.pipelineId),
+
+  },
+  {
+    title: 'Replication Type & Factor',
+    dataIndex: 'replicationType',
+    key: 'replicationType',
+    render: (replicationType: string, record: Pipeline) => {
+      const replicationFactor = record.replicationFactor;
+      return (
+        <span>
+          <ReplicationIcon
+            replicationFactor={replicationFactor}
+            replicationType={replicationType}
+            leaderNode={record.leaderNode}
+            isLeader={false} />
+          {replicationType} ({replicationFactor})
+        </span>
+      );
+    },
+    sorter: (a: Pipeline, b: Pipeline) =>
+      (a.replicationType + 
a.replicationFactor.toString()).localeCompare(b.replicationType + 
b.replicationFactor.toString()),
+    defaultSortOrder: 'descend' as const
+  },
+  {
+    title: 'Status',
+    dataIndex: 'status',
+    key: 'status',
+    filterMultiple: true,
+    filters: PipelineStatusList.map(status => ({ text: status, value: status 
})),
+    onFilter: (value, record: Pipeline) => record.status === value,
+    sorter: (a: Pipeline, b: Pipeline) => a.status.localeCompare(b.status)
+  },
+  {
+    title: 'Containers',
+    dataIndex: 'containers',
+    key: 'containers',
+    sorter: (a: Pipeline, b: Pipeline) => a.containers - b.containers
+  },
+  {
+    title: 'Datanodes',
+    dataIndex: 'datanodes',
+    key: 'datanodes',
+    render: (datanodes: SummaryDatanodeDetails[]) => (
+      <div>
+        {datanodes.map(datanode => (
+          <div className='uuid-tooltip'>
+            <Tooltip
+              placement='top'
+              title={`UUID: ${datanode?.uuid ?? 'NA'}`}
+              getPopupContainer={(triggerNode) => triggerNode}>
+              {datanode?.hostName ?? 'N/A'}
+            </Tooltip>
+          </div>
+        ))}
+      </div>
+    )
+  },
+  {
+    title: 'Leader',
+    dataIndex: 'leaderNode',
+    key: 'leaderNode',
+    sorter: (a: Pipeline, b: Pipeline) => 
a.leaderNode.localeCompare(b.leaderNode)
+  },
+  {
+    title: () => (
+      <span>
+        Last Leader Election&nbsp;
+        <Tooltip title='Elapsed time since the current leader got elected. 
Only available if any metrics service providers like Prometheus is configured.'>
+          <InfoCircleOutlined />
+        </Tooltip>
+      </span>
+    ),
+    dataIndex: 'lastLeaderElection',
+    key: 'lastLeaderElection',
+    render: (lastLeaderElection: number) => lastLeaderElection > 0 ?
+      getTimeDiffFromTimestamp(lastLeaderElection) : 'NA',
+    sorter: (a: Pipeline, b: Pipeline) => a.lastLeaderElection - 
b.lastLeaderElection
+  },
+  {
+    title: 'Lifetime',
+    dataIndex: 'duration',
+    key: 'duration',
+    render: (duration: number) => getDurationFromTimestamp(duration),
+    sorter: (a: Pipeline, b: Pipeline) => a.duration - b.duration
+  },
+  {
+    title: () => (
+      <span>
+        No. of Elections&nbsp;
+        <Tooltip title='Number of elections in this pipeline. Only available 
if any metrics service providers like Prometheus is configured.'>
+          <InfoCircleOutlined />
+        </Tooltip>
+      </span>
+    ),
+    dataIndex: 'leaderElections',
+    key: 'leaderElections',
+    render: (leaderElections: number) => leaderElections > 0 ?
+      leaderElections : 'NA',
+    sorter: (a: Pipeline, b: Pipeline) => a.leaderElections - b.leaderElections
+  }
+];
+
+const PipelinesTable: React.FC<PipelinesTableProps> = ({
+  loading = false,
+  data,
+  selectedColumns,
+  searchTerm = ''
+}) => {
+  const paginationConfig: TablePaginationConfig = {
+    showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} 
pipelines`,
+    showSizeChanger: true,
+  };
+
+  function filterSelectedColumns() {
+    const columnKeys = selectedColumns.map((column) => column.value);
+    return COLUMNS.filter(
+      (column) => columnKeys.indexOf(column.key as string) >= 0
+    )
+  }
+
+  function getFilteredData(data: Pipeline[]) {
+    return data.filter(
+      (pipeline: Pipeline) => pipeline['pipelineId'].includes(searchTerm)
+    )
+  }
+
+  return (
+    <div>
+      <Table
+        dataSource={getFilteredData(data)}
+        columns={filterSelectedColumns()}
+        loading={loading}
+        rowKey='pipelineId'
+        pagination={paginationConfig}
+        scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+        locale={{ filterTitle: '' }} />
+    </div>
+  )
+}
+
+export default PipelinesTable;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
new file mode 100644
index 0000000000..4de0d713fc
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 QuotaBar from '@/components/quotaBar/quotaBar';
+import { byteToSize } from '@/utils/common';
+import { Volume, VolumesTableProps } from '@/v2/types/volume.types';
+import Table, { ColumnsType, ColumnType, TablePaginationConfig } from 
'antd/es/table';
+import moment from 'moment';
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+export const COLUMNS: ColumnsType<Volume> = [
+  {
+    title: 'Volume',
+    dataIndex: 'volume',
+    key: 'volume',
+    sorter: (a: Volume, b: Volume) => a.volume.localeCompare(b.volume),
+    defaultSortOrder: 'ascend' as const,
+    width: '15%'
+  },
+  {
+    title: 'Owner',
+    dataIndex: 'owner',
+    key: 'owner',
+    sorter: (a: Volume, b: Volume) => a.owner.localeCompare(b.owner)
+  },
+  {
+    title: 'Admin',
+    dataIndex: 'admin',
+    key: 'admin',
+    sorter: (a: Volume, b: Volume) => a.admin.localeCompare(b.admin)
+  },
+  {
+    title: 'Creation Time',
+    dataIndex: 'creationTime',
+    key: 'creationTime',
+    sorter: (a: Volume, b: Volume) => a.creationTime - b.creationTime,
+    render: (creationTime: number) => {
+      return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+    }
+  },
+  {
+    title: 'Modification Time',
+    dataIndex: 'modificationTime',
+    key: 'modificationTime',
+    sorter: (a: Volume, b: Volume) => a.modificationTime - b.modificationTime,
+    render: (modificationTime: number) => {
+      return modificationTime > 0 ? moment(modificationTime).format('ll LTS') 
: 'NA';
+    }
+  },
+  {
+    title: 'Quota (Size)',
+    dataIndex: 'quotaInBytes',
+    key: 'quotaInBytes',
+    render: (quotaInBytes: number) => {
+      return quotaInBytes && quotaInBytes !== -1 ? byteToSize(quotaInBytes, 3) 
: 'NA';
+    }
+  },
+  {
+    title: 'Namespace Capacity',
+    key: 'namespaceCapacity',
+    sorter: (a: Volume, b: Volume) => a.usedNamespace - b.usedNamespace,
+    render: (text: string, record: Volume) => (
+      <QuotaBar
+        quota={record.quotaInNamespace}
+        used={record.usedNamespace}
+        quotaType='namespace'
+      />
+    )
+  },
+];
+
+const VolumesTable: React.FC<VolumesTableProps> = ({
+  loading = false,
+  data,
+  handleAclClick,
+  selectedColumns,
+  searchColumn = 'volume',
+  searchTerm = ''
+}) => {
+
+  React.useEffect(() => {
+    // On table mount add the actions column
+    console.log("Adding new column");
+    const actionsColumn: ColumnType<Volume> = {
+      title: 'Actions',
+      key: 'actions',
+      render: (_: any, record: Volume) => {
+        const searchParams = new URLSearchParams();
+        searchParams.append('volume', record.volume);
+  
+        return (
+          <>
+            <Link
+              key="listBuckets"
+              to={`/Buckets?${searchParams.toString()}`}
+              style={{
+                marginRight: '16px'
+              }}>
+              Show buckets
+            </Link>
+            <a
+              key='acl'
+              onClick={() => handleAclClick(record)}>
+              Show ACL
+            </a>
+          </>
+        );
+      }
+    }
+
+    if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'actions') {
+      // Push the ACL column for initial
+      COLUMNS.push(actionsColumn);
+      selectedColumns.push({
+        label: actionsColumn.title as string,
+        value: actionsColumn.key as string
+      });
+    } else {
+      // Replace old ACL column with new ACL column with correct reference
+      // e.g. After page is reloaded / redirect from other page
+      COLUMNS[COLUMNS.length - 1] = actionsColumn;
+      selectedColumns[selectedColumns.length - 1] = {
+        label: actionsColumn.title as string,
+        value: actionsColumn.key as string
+      }
+    }
+
+  }, []);
+
+  function filterSelectedColumns() {
+    const columnKeys = selectedColumns.map((column) => column.value);
+    return COLUMNS.filter(
+      (column) => columnKeys.indexOf(column.key as string) >= 0
+    )
+  }
+
+  function getFilteredData(data: Volume[]) {
+    return data.filter(
+      (volume: Volume) => volume[searchColumn].includes(searchTerm)
+    );
+  }
+
+  const paginationConfig: TablePaginationConfig = {
+    showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} 
volumes`,
+    showSizeChanger: true
+  };
+
+  return (
+    <div>
+      <Table
+        dataSource={getFilteredData(data)}
+        columns={filterSelectedColumns()}
+        loading={loading}
+        rowKey='volume'
+        pagination={paginationConfig}
+        scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+        locale={{ filterTitle: '' }}
+      />
+    </div>
+  )
+}
+
+export default VolumesTable;
\ 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 bd8950e54c..12af3bb428 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
@@ -18,25 +18,9 @@
 
 import React, { useEffect, useState } from 'react';
 import moment from 'moment';
-import { Table, Tag } from 'antd';
-import {
-  ColumnProps,
-  ColumnsType,
-  TablePaginationConfig
-} from 'antd/es/table';
-import {
-  CheckCircleOutlined,
-  CloseCircleOutlined,
-  CloudServerOutlined,
-  FileUnknownOutlined,
-  HddOutlined,
-  LaptopOutlined,
-  SaveOutlined
-} from '@ant-design/icons';
 import { ValueType } from 'react-select';
 import { useLocation } from 'react-router-dom';
 
-import QuotaBar from '@/components/quotaBar/quotaBar';
 import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
 import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
 import Search from '@/v2/components/search/search';
@@ -45,20 +29,17 @@ import SingleSelect, { Option } from 
'@/v2/components/select/singleSelect';
 
 import { AutoReloadHelper } from '@/utils/autoReloadHelper';
 import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
-import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { showDataFetchError } from '@/utils/common';
 import { useDebounce } from '@/v2/hooks/debounce.hook';
 
 import {
   Bucket,
-  BucketLayout,
-  BucketLayoutTypeList,
   BucketResponse,
   BucketsState,
-  BucketStorage,
-  BucketStorageTypeList
 } from '@/v2/types/bucket.types';
 
 import './buckets.less';
+import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
 
 
 const LIMIT_OPTIONS: Option[] = [
@@ -80,38 +61,6 @@ const LIMIT_OPTIONS: Option[] = [
   }
 ]
 
-const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
-  return isVersionEnabled
-    ? <CheckCircleOutlined
-      style={{ color: '#1da57a' }}
-      className='icon-success' />
-    : <CloseCircleOutlined className='icon-neutral' />
-};
-
-const renderStorageType = (bucketStorage: BucketStorage) => {
-  const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
-    RAM_DISK: <LaptopOutlined />,
-    SSD: <SaveOutlined />,
-    DISK: <HddOutlined />,
-    ARCHIVE: <CloudServerOutlined />
-  };
-  const icon = bucketStorage in bucketStorageIconMap
-    ? bucketStorageIconMap[bucketStorage]
-    : <FileUnknownOutlined />;
-  return <span>{icon} {bucketStorage}</span>;
-};
-
-const renderBucketLayout = (bucketLayout: BucketLayout) => {
-  const bucketLayoutColorMap = {
-    FILE_SYSTEM_OPTIMIZED: 'green',
-    OBJECT_STORE: 'orange',
-    LEGACY: 'blue'
-  };
-  const color = bucketLayout in bucketLayoutColorMap ?
-    bucketLayoutColorMap[bucketLayout] : '';
-  return <Tag color={color}>{bucketLayout}</Tag>;
-};
-
 const SearchableColumnOpts = [{
   label: 'Bucket',
   value: 'name'
@@ -120,113 +69,6 @@ const SearchableColumnOpts = [{
   value: 'volumeName'
 }]
 
-const COLUMNS: ColumnsType<Bucket> = [
-  {
-    title: 'Bucket',
-    dataIndex: 'name',
-    key: 'name',
-    sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
-    defaultSortOrder: 'ascend' as const
-  },
-  {
-    title: 'Volume',
-    dataIndex: 'volumeName',
-    key: 'volumeName',
-    sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
-    defaultSortOrder: 'ascend' as const
-  },
-  {
-    title: 'Owner',
-    dataIndex: 'owner',
-    key: 'owner',
-    sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
-  },
-  {
-    title: 'Versioning',
-    dataIndex: 'versioning',
-    key: 'isVersionEnabled',
-    render: (isVersionEnabled: boolean) => 
renderIsVersionEnabled(isVersionEnabled)
-  },
-  {
-    title: 'Storage Type',
-    dataIndex: 'storageType',
-    key: 'storageType',
-    filterMultiple: true,
-    filters: BucketStorageTypeList.map(state => ({ text: state, value: state 
})),
-    onFilter: (value, record: Bucket) => record.storageType === value,
-    sorter: (a: Bucket, b: Bucket) => 
a.storageType.localeCompare(b.storageType),
-    render: (storageType: BucketStorage) => renderStorageType(storageType)
-  },
-  {
-    title: 'Bucket Layout',
-    dataIndex: 'bucketLayout',
-    key: 'bucketLayout',
-    filterMultiple: true,
-    filters: BucketLayoutTypeList.map(state => ({ text: state, value: state 
})),
-    onFilter: (value, record: Bucket) => record.bucketLayout === value,
-    sorter: (a: Bucket, b: Bucket) => 
a.bucketLayout.localeCompare(b.bucketLayout),
-    render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
-  },
-  {
-    title: 'Creation Time',
-    dataIndex: 'creationTime',
-    key: 'creationTime',
-    sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
-    render: (creationTime: number) => {
-      return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
-    }
-  },
-  {
-    title: 'Modification Time',
-    dataIndex: 'modificationTime',
-    key: 'modificationTime',
-    sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
-    render: (modificationTime: number) => {
-      return modificationTime > 0 ? moment(modificationTime).format('ll LTS') 
: 'NA';
-    }
-  },
-  {
-    title: 'Storage Capacity',
-    key: 'quotaCapacityBytes',
-    sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
-    render: (text: string, record: Bucket) => (
-      <QuotaBar
-        quota={record.quotaInBytes}
-        used={record.usedBytes}
-        quotaType='size'
-      />
-    )
-  },
-  {
-    title: 'Namespace Capacity',
-    key: 'namespaceCapacity',
-    sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
-    render: (text: string, record: Bucket) => (
-      <QuotaBar
-        quota={record.quotaInNamespace}
-        used={record.usedNamespace}
-        quotaType='namespace'
-      />
-    )
-  },
-  {
-    title: 'Source Volume',
-    dataIndex: 'sourceVolume',
-    key: 'sourceVolume',
-    render: (sourceVolume: string) => {
-      return sourceVolume ? sourceVolume : 'NA';
-    }
-  },
-  {
-    title: 'Source Bucket',
-    dataIndex: 'sourceBucket',
-    key: 'sourceBucket',
-    render: (sourceBucket: string) => {
-      return sourceBucket ? sourceBucket : 'NA';
-    }
-  }
-];
-
 const defaultColumns = COLUMNS.map(column => ({
   label: column.title as string,
   value: column.key as string
@@ -291,21 +133,10 @@ const Buckets: React.FC<{}> = () => {
   const debouncedSearch = useDebounce(searchTerm, 300);
   const { search } = useLocation();
 
-  const paginationConfig: TablePaginationConfig = {
-    showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} 
buckets`,
-    showSizeChanger: true
-  };
-
   function getVolumeSearchParam() {
     return new URLSearchParams(search).get('volume');
   };
 
-  function getFilteredData(data: Bucket[]) {
-    return data.filter(
-      (bucket: Bucket) => bucket[searchColumn].includes(debouncedSearch)
-    );
-  }
-
   function handleVolumeChange(selected: ValueType<Option, true>) {
     const { volumeBucketMap } = state;
     const volumeSelections = (selected as Option[]);
@@ -327,50 +158,6 @@ const Buckets: React.FC<{}> = () => {
     setShowPanel(true);
   }
 
-  function filterSelectedColumns() {
-    const columnKeys = selectedColumns.map((column) => column.value);
-    return COLUMNS.filter(
-      (column) => columnKeys.indexOf(column.key as string) >= 0
-    )
-  }
-
-  function addAclColumn() {
-    // Inside the class component to access the React internal state
-    const aclLinkColumn: ColumnProps<Bucket> = {
-      title: 'ACLs',
-      dataIndex: 'acls',
-      key: 'acls',
-      render: (_: any, record: Bucket) => {
-        return (
-          <a
-            key='acl'
-            onClick={() => {
-              handleAclLinkClick(record);
-            }}
-          >
-            Show ACL
-          </a>
-        );
-      }
-    };
-
-    if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
-      // Push the ACL column for initial
-      COLUMNS.push(aclLinkColumn);
-    } else {
-      // Replace old ACL column with new ACL column with correct reference
-      // e.g. After page is reloaded / redirect from other page
-      COLUMNS[COLUMNS.length - 1] = aclLinkColumn;
-    }
-
-    if (defaultColumns.length > 0 && defaultColumns[defaultColumns.length - 
1].label !== 'acls') {
-      defaultColumns.push({
-        label: aclLinkColumn.title as string,
-        value: aclLinkColumn.key as string
-      });
-    }
-  };
-
   function handleColumnChange(selected: ValueType<Option, true>) {
     setSelectedColumns(selected as Option[]);
   }
@@ -447,7 +234,6 @@ const Buckets: React.FC<{}> = () => {
 
   useEffect(() => {
     autoReloadHelper.startPolling();
-    addAclColumn();
     const initialVolume = getVolumeSearchParam();
     if (initialVolume) {
       setSelectedVolumes([{
@@ -537,17 +323,13 @@ const Buckets: React.FC<{}> = () => {
                 setSearchColumn(value as 'name' | 'volumeName');
               }} />
           </div>
-          <div>
-            <Table
-              dataSource={getFilteredData(bucketsUnderVolume)}
-              columns={filterSelectedColumns()}
-              loading={loading}
-              rowKey='volume'
-              pagination={paginationConfig}
-              scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
-              locale={{ filterTitle: '' }}
-            />
-          </div>
+          <BucketsTable
+            loading={loading}
+            data={bucketsUnderVolume}
+            handleAclClick={handleAclLinkClick}
+            selectedColumns={selectedColumns}
+            searchColumn={searchColumn}
+            searchTerm={debouncedSearch} />
         </div>
         <AclPanel
           visible={showPanel}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
similarity index 59%
copy from 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
copy to 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
index 67f007706a..a2fb93f7da 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
@@ -16,29 +16,33 @@
 * limitations under the License.
 */
 
-import { Acl } from "@/v2/types/acl.types";
-import { Option } from "@/v2/components/select/multiSelect";
+.content-div {
+  min-height: unset;
 
-export type Volume = {
-  volume: string;
-  owner: string;
-  admin: string;
-  creationTime: number;
-  modificationTime: number;
-  quotaInBytes: number;
-  quotaInNamespace: number;
-  usedNamespace: number;
-  acls?: Acl[];
-}
+  .table-header-section {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
 
-export type VolumesResponse = {
-  totalCount: number;
-  volumes: Volume[];
-}
+    .table-filter-section {
+      font-size: 14px;
+      font-weight: normal;
+      display: flex;
+      column-gap: 8px;
+      padding: 16px 8px;
+    }
+  }
+
+  .uuid-tooltip {
+    cursor: pointer;
+    .ant-tooltip-inner {
+      width: max-content;
+    }
+  }
 
-export type VolumesState = {
-  data: Volume[];
-  lastUpdated: number;
-  columnOptions: Option[];
-  currentRow: Volume | Record<string, never>;
+  .tag-block {
+    display: flex;
+    column-gap: 8px;
+    padding: 0px 8px 16px 8px;
+  }
 }
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
new file mode 100644
index 0000000000..9059da91f9
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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, {
+  useEffect,
+  useRef,
+  useState
+} from 'react';
+import moment from 'moment';
+import { ValueType } from 'react-select';
+
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import Search from '@/v2/components/search/search';
+import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
+import PipelinesTable, { COLUMNS } from 
'@/v2/components/tables/pipelinesTable';
+import { showDataFetchError } from '@/utils/common';
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+  Pipeline,
+  PipelinesResponse,
+  PipelinesState
+} from '@/v2/types/pipelines.types';
+
+import './pipelines.less';
+
+
+const defaultColumns = COLUMNS.map(column => ({
+  label: (typeof column.title === 'string')
+    ? column.title
+    : (column.title as Function)().props.children[0],
+  value: column.key as string,
+}));
+
+const Pipelines: React.FC<{}> = () => {
+  const cancelSignal = useRef<AbortController>();
+
+  const [state, setState] = useState<PipelinesState>({
+    activeDataSource: [],
+    columnOptions: defaultColumns,
+    lastUpdated: 0,
+  });
+  const [loading, setLoading] = useState<boolean>(false);
+  const [selectedColumns, setSelectedColumns] = 
useState<Option[]>(defaultColumns);
+  const [searchTerm, setSearchTerm] = useState<string>('');
+
+  const debouncedSearch = useDebounce(searchTerm, 300);
+
+  const loadData = () => {
+    setLoading(true);
+    //Cancel any previous requests
+    cancelRequests([cancelSignal.current!]);
+
+    const { request, controller } = AxiosGetHelper(
+      '/api/v1/pipelines',
+      cancelSignal.current
+    );
+
+    cancelSignal.current = controller;
+    request.then(response => {
+      const pipelinesResponse: PipelinesResponse = response.data;
+      const pipelines: Pipeline[] = pipelinesResponse?.pipelines ?? {};
+      setState({
+        ...state,
+        activeDataSource: pipelines,
+        lastUpdated: Number(moment())
+      })
+      setLoading(false);
+    }).catch(error => {
+      setLoading(false);
+      showDataFetchError(error.toString());
+    })
+  }
+
+  const autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+  useEffect(() => {
+    autoReloadHelper.startPolling();
+    loadData();
+    return (() => {
+      autoReloadHelper.stopPolling();
+      cancelRequests([cancelSignal.current!]);
+    })
+  }, []);
+
+  function handleColumnChange(selected: ValueType<Option, true>) {
+    setSelectedColumns(selected as Option[]);
+  }
+
+  const {
+    activeDataSource,
+    columnOptions,
+    lastUpdated
+  } = state;
+
+  return (
+    <>
+      <div className='page-header-v2'>
+        Pipelines
+        <AutoReloadPanel
+          isLoading={loading}
+          lastRefreshed={lastUpdated}
+          togglePolling={autoReloadHelper.handleAutoReloadToggle}
+          onReload={loadData} />
+      </div>
+      <div style={{ padding: '24px' }}>
+        <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}
+                onTagClose={() => { }}
+                fixedColumn='pipelineId'
+                columnLength={COLUMNS.length} />
+            </div>
+            <Search
+              disabled={activeDataSource?.length < 1}
+              searchOptions={[{
+                label: 'Pipeline ID',
+                value: 'pipelineId'
+              }]}
+              searchInput={searchTerm}
+              searchColumn={'pipelineId'}
+              onSearchChange={
+                (e: React.ChangeEvent<HTMLInputElement>) => 
setSearchTerm(e.target.value)
+              }
+              onChange={() => { }} />
+          </div>
+          <PipelinesTable
+            loading={loading}
+            data={activeDataSource}
+            selectedColumns={selectedColumns}
+            searchTerm={debouncedSearch} />
+        </div>
+      </div>
+    </>
+  );
+}
+export default Pipelines;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
index 605883caff..cb25cedbce 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
@@ -18,22 +18,16 @@
 
 import React, { useEffect, useRef, useState } from 'react';
 import moment from 'moment';
-import { Table } from 'antd';
-import { Link } from 'react-router-dom';
-import {
-  TablePaginationConfig,
-  ColumnsType
-} from 'antd/es/table';
 import { ValueType } from 'react-select/src/types';
 
-import QuotaBar from '@/components/quotaBar/quotaBar';
 import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
 import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
-import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
 import SingleSelect from '@/v2/components/select/singleSelect';
+import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
+import VolumesTable, { COLUMNS } from '@/v2/components/tables/volumesTable';
 import Search from '@/v2/components/search/search';
 
-import { byteToSize, showDataFetchError } from '@/utils/common';
+import { showDataFetchError } from '@/utils/common';
 import { AutoReloadHelper } from '@/utils/autoReloadHelper';
 import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper";
 import { useDebounce } from '@/v2/hooks/debounce.hook';
@@ -72,93 +66,6 @@ const Volumes: React.FC<{}> = () => {
 
   const cancelSignal = useRef<AbortController>();
 
-  const COLUMNS: ColumnsType<Volume> = [
-    {
-      title: 'Volume',
-      dataIndex: 'volume',
-      key: 'volume',
-      sorter: (a: Volume, b: Volume) => a.volume.localeCompare(b.volume),
-      defaultSortOrder: 'ascend' as const,
-      width: '15%'
-    },
-    {
-      title: 'Owner',
-      dataIndex: 'owner',
-      key: 'owner',
-      sorter: (a: Volume, b: Volume) => a.owner.localeCompare(b.owner)
-    },
-    {
-      title: 'Admin',
-      dataIndex: 'admin',
-      key: 'admin',
-      sorter: (a: Volume, b: Volume) => a.admin.localeCompare(b.admin)
-    },
-    {
-      title: 'Creation Time',
-      dataIndex: 'creationTime',
-      key: 'creationTime',
-      sorter: (a: Volume, b: Volume) => a.creationTime - b.creationTime,
-      render: (creationTime: number) => {
-        return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
-      }
-    },
-    {
-      title: 'Modification Time',
-      dataIndex: 'modificationTime',
-      key: 'modificationTime',
-      sorter: (a: Volume, b: Volume) => a.modificationTime - 
b.modificationTime,
-      render: (modificationTime: number) => {
-        return modificationTime > 0 ? moment(modificationTime).format('ll 
LTS') : 'NA';
-      }
-    },
-    {
-      title: 'Quota (Size)',
-      dataIndex: 'quotaInBytes',
-      key: 'quotaInBytes',
-      render: (quotaInBytes: number) => {
-        return quotaInBytes && quotaInBytes !== -1 ? byteToSize(quotaInBytes, 
3) : 'NA';
-      }
-    },
-    {
-      title: 'Namespace Capacity',
-      key: 'namespaceCapacity',
-      sorter: (a: Volume, b: Volume) => a.usedNamespace - b.usedNamespace,
-      render: (text: string, record: Volume) => (
-        <QuotaBar
-          quota={record.quotaInNamespace}
-          used={record.usedNamespace}
-          quotaType='namespace'
-        />
-      )
-    },
-    {
-      title: 'Actions',
-      key: 'actions',
-      render: (_: any, record: Volume) => {
-        const searchParams = new URLSearchParams();
-        searchParams.append('volume', record.volume);
-
-        return (
-          <>
-            <Link
-              key="listBuckets"
-              to={`/Buckets?${searchParams.toString()}`}
-              style={{
-                marginRight: '16px'
-              }}>
-              Show buckets
-            </Link>
-            <a
-              key='acl'
-              onClick={() => handleAclLinkClick(record)}>
-              Show ACL
-            </a>
-          </>
-        );
-      }
-    }
-  ];
-
   const defaultColumns = COLUMNS.map(column => ({
     label: column.title as string,
     value: column.key as string,
@@ -167,10 +74,10 @@ const Volumes: React.FC<{}> = () => {
   const [state, setState] = useState<VolumesState>({
     data: [],
     lastUpdated: 0,
-    columnOptions: defaultColumns,
-    currentRow: {}
+    columnOptions: defaultColumns
   });
   const [loading, setLoading] = useState<boolean>(false);
+  const [currentRow, setCurrentRow] = useState<Volume | Record<string, 
never>>({});
   const [selectedColumns, setSelectedColumns] = 
useState<Option[]>(defaultColumns);
   const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
   const [searchColumn, setSearchColumn] = useState<'volume' | 'owner' | 
'admin'>('volume');
@@ -255,35 +162,13 @@ const Volumes: React.FC<{}> = () => {
 
 
   function handleAclLinkClick(volume: Volume) {
-    setState({
-      ...state,
-      currentRow: volume
-    });
+    setCurrentRow(volume);
     setShowPanel(true);
   }
 
-  function filterSelectedColumns() {
-    const columnKeys = selectedColumns.map((column) => column.value);
-    return COLUMNS.filter(
-      (column) => columnKeys.indexOf(column.key as string) >= 0
-    )
-  }
-
-  function getFilteredData(data: Volume[]) {
-    return data.filter(
-      (volume: Volume) => volume[searchColumn].includes(debouncedSearch)
-    );
-  }
-
-
-  const paginationConfig: TablePaginationConfig = {
-    showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} 
volumes`,
-    showSizeChanger: true
-  };
-
   const {
     data, lastUpdated,
-    columnOptions, currentRow
+    columnOptions
   } = state;
 
   return (
@@ -329,17 +214,13 @@ const Volumes: React.FC<{}> = () => {
                 setSearchColumn(value as 'volume' | 'owner' | 'admin');
               }} />
           </div>
-          <div>
-            <Table
-              dataSource={getFilteredData(data)}
-              columns={filterSelectedColumns()}
-              loading={loading}
-              rowKey='volume'
-              pagination={paginationConfig}
-              scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
-              locale={{ filterTitle: '' }}
-            />
-          </div>
+          <VolumesTable
+            loading={loading}
+            data={data}
+            handleAclClick={handleAclLinkClick}
+            selectedColumns={selectedColumns}
+            searchColumn={searchColumn}
+            searchTerm={debouncedSearch} />
         </div>
         <AclPanel
           visible={showPanel}
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 8a37ef9c51..37ec3964bb 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
@@ -20,6 +20,7 @@ import { lazy } from 'react';
 const Overview = lazy(() => import('@/v2/pages/overview/overview'));
 const Volumes = lazy(() => import('@/v2/pages/volumes/volumes'))
 const Buckets = lazy(() => import('@/v2/pages/buckets/buckets'));
+const Pipelines = lazy(() => import('@/v2/pages/pipelines/pipelines'));
 
 export const routesV2 = [
   {
@@ -33,5 +34,9 @@ export const routesV2 = [
   {
     path: '/Buckets',
     component: Buckets
+  },
+  {
+    path: '/Pipelines',
+    component: Pipelines
   }
 ];
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
index 5cfc89d85e..eb499dc617 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
@@ -17,7 +17,6 @@
  */
 
 import { Acl } from "@/v2/types/acl.types";
-import { Option } from "@/v2/components/select/singleSelect";
 import { Option as MultiOption } from "@/v2/components/select/multiSelect";
 
 // Corresponds to OzoneManagerProtocolProtos.StorageTypeProto
@@ -68,4 +67,13 @@ export type BucketsState = {
   volumeBucketMap: Map<string, Set<Bucket>>;
   bucketsUnderVolume: Bucket[];
   volumeOptions: MultiOption[];
-}
\ No newline at end of file
+}
+
+export type BucketsTableProps = {
+  loading: boolean;
+  data: Bucket[];
+  handleAclClick: (arg0: Bucket) => void;
+  selectedColumns: MultiOption[];
+  searchColumn: 'name' | 'volumeName';
+  searchTerm: string;
+}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
new file mode 100644
index 0000000000..7c5a23bc0a
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 const PipelineStatusList = [
+  'OPEN',
+  'CLOSING',
+  'QUASI_CLOSED',
+  'CLOSED',
+  'UNHEALTHY',
+  'INVALID',
+  'DELETED',
+  'DORMANT'
+] as const;
+export type PipelineStatus = typeof PipelineStatusList[number];
+
+export type Pipeline = {
+  pipelineId: string;
+  status: PipelineStatus;
+  replicationType: string;
+  leaderNode: string;
+  datanodes: string[];
+  lastLeaderElection: number;
+  duration: number;
+  leaderElections: number;
+  replicationFactor: string;
+  containers: number;
+}
+
+export type PipelinesResponse = {
+  totalCount: number;
+  pipelines: Pipeline[];
+}
+
+export type PipelinesState = {
+  activeDataSource: Pipeline[];
+  columnOptions: Option[];
+  lastUpdated: number;
+}
+
+export type PipelinesTableProps = {
+  loading: boolean;
+  data: Pipeline[];
+  selectedColumns: Option[];
+  searchTerm: string;
+}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
index 67f007706a..b808d40358 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
@@ -40,5 +40,13 @@ export type VolumesState = {
   data: Volume[];
   lastUpdated: number;
   columnOptions: Option[];
-  currentRow: Volume | Record<string, never>;
+}
+
+export type VolumesTableProps = {
+  loading: boolean;
+  data: Volume[];
+  handleAclClick: (arg0: Volume) => void;
+  selectedColumns: Option[];
+  searchColumn: 'volume' | 'owner' | 'admin';
+  searchTerm: string;
 }
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
new file mode 100644
index 0000000000..fb553d0db3
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 moment from "moment";
+
+moment.updateLocale('en', {
+  relativeTime: {
+    past: '%s ago',
+    s: '%ds',
+    m: '1min',
+    mm: '%dmins',
+    h: '1hr',
+    hh: '%dhrs',
+    d: '1d',
+    dd: '%dd',
+    M: '1m',
+    MM: '%dm',
+    y: '1y',
+    yy: '%dy'
+  }
+});
+
+export function getTimeDiffFromTimestamp(timestamp: number): string {
+  const timestampDate = new Date(timestamp);
+  return moment(timestampDate).fromNow();
+}
+
+export function getDurationFromTimestamp(timestamp: number): string {
+  const duration: moment.Duration = moment.duration(timestamp, 'milliseconds');
+  // return nothing when the duration is falsy or not correctly parsed (P0D)
+  if(!duration || duration.toISOString() === "P0D") return '';
+
+  let elapsedTime = [];
+  const durationBreakdowns: Record<string, number> = {
+    'y': Math.floor(duration.years()),
+    'm': Math.floor(duration.months()),
+    'd': Math.floor(duration.days()),
+    'h': Math.floor(duration.hours()),
+    'min': Math.floor(duration.minutes()),
+    's': Math.floor(duration.seconds())
+  }
+
+  for (const [key, value] of Object.entries(durationBreakdowns)) {
+    value > 0 && elapsedTime.push(value + key);
+  }
+
+  return (elapsedTime.length === 0) ? 'Just now' : elapsedTime.join(' ');
+}


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

Reply via email to