This is an automated email from the ASF dual-hosted git repository.

arafat2198 pushed a commit to branch HDDS-13177
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13177 by this push:
     new cc5cf028216 HDDS-13183. Create Cluster Capacity page UI (#9022).
cc5cf028216 is described below

commit cc5cf0282160738849d02f2106640cbed3d29c9d
Author: Abhishek Pal <[email protected]>
AuthorDate: Mon Nov 3 12:00:25 2025 +0530

    HDDS-13183. Create Cluster Capacity page UI (#9022).
---
 .../webapps/recon/ozone-recon-web/api/db.json      |  84 ++++++
 .../webapps/recon/ozone-recon-web/api/routes.json  |   4 +-
 .../recon/ozone-recon-web/src/utils/themeIcons.tsx |  27 ++
 .../overviewCardWrapper.tsx                        |   0
 .../{overviewCard => cards}/overviewSimpleCard.tsx |   0
 .../v2/components/cards/overviewStorageCard.tsx    | 248 ++++++++++++++++
 .../overviewSummaryCard.tsx                        |   0
 .../src/v2/components/navBar/navBar.tsx            |   6 +
 .../src/v2/pages/capacity/capacity.less            | 162 +++++++++++
 .../src/v2/pages/capacity/capacity.tsx             | 312 +++++++++++++++++++++
 .../capacity/components/CapacityBreakdown.tsx      |  73 +++++
 .../pages/capacity/components/CapacityDetail.tsx   | 143 ++++++++++
 .../pages/capacity/components/StackedProgress.tsx  |  59 ++++
 .../pages/capacity/components/WrappedInfoIcon.tsx  |  38 +++
 .../capacity/constants/descriptions.constants.tsx  |  25 ++
 .../pages/capacity/constants/styles.constants.tsx  |  43 +++
 .../src/v2/pages/overview/overview.tsx             |   6 +-
 .../recon/ozone-recon-web/src/v2/routes-v2.tsx     |   5 +
 .../ozone-recon-web/src/v2/types/capacity.types.ts |  72 +++++
 19 files changed, 1303 insertions(+), 4 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 30fed20baeb..2c4c2a998c3 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
@@ -6866,5 +6866,89 @@
     "selectedRowKeys": [
       "b5907812-a5f2-11ea-bb37-0242ac130011"
     ]
+  },
+  "utilization": {
+    "globalStorage": {
+        "totalUsedSpace": 1632526336,
+        "totalFreeSpace": 596013527040,
+        "totalCapacity": 746331226935
+    },
+    "globalNamespace": {
+        "totalUsedSpace": 0,
+        "totalKeys": 0
+    },
+    "usedSpaceBreakdown": {
+      "openKeysBytes": 10000,
+      "committedBytes": 250100,
+      "containerPreAllocated": 1073725932,
+      "deletionPendingBytes": {
+        "total": 3765706484,
+        "byStage": {
+            "DN": {
+                "pendingBytes": 3758096384
+            },
+            "SCM": {
+                "pendingBytes": 3145600
+            },
+            "OM": {
+                "pendingKeyBytes": 1254500,
+                "totalBytes": 4464500,
+                "pendingDirectoryBytes": 3210000
+            }
+        }
+      }
+    },
+    "dataNodeUsage": [
+      {
+          "datanodeUuid": "ec4d37e4-04d7-4d1b-b0bb-aafa05d86b3c",
+          "hostName": "ozone-datanode-2.ozone_default",
+          "capacity": 125645656770,
+          "used": 4382720,
+          "remaining": 104675287040,
+          "committed": 0,
+          "pendingDeletion": 0,
+          "minimumFreeSpace": 125645664
+      },
+      {
+          "datanodeUuid": "ed34b38a-88b0-4dde-8ef6-6d158339064e",
+          "hostName": "ozone-datanode-4.ozone_default",
+          "capacity": 125645656770,
+          "used": 4382720,
+          "remaining": 104675291136,
+          "committed": 0,
+          "pendingDeletion": 0,
+          "minimumFreeSpace": 125645664
+      },
+      {
+          "datanodeUuid": "49d5a41b-ffb4-426f-bd46-93d9263296ef",
+          "hostName": "ozone-datanode-5.ozone_default",
+          "capacity": 125645656770,
+          "used": 4382720,
+          "remaining": 104675291136,
+          "committed": 0,
+          "pendingDeletion": 0,
+          "minimumFreeSpace": 125645664
+      },
+      {
+          "datanodeUuid": "08b69287-6fd0-42e0-b944-f5d527607ac9",
+          "hostName": "ozone-datanode-1.ozone_default",
+          "capacity": 125645656770,
+          "used": 4382720,
+          "remaining": 104675282944,
+          "committed": 0,
+          "pendingDeletion": 0,
+          "minimumFreeSpace": 125645664
+      },
+      {
+          "datanodeUuid": "3be0d2dc-b068-46c9-a6df-ff086e94fec5",
+          "hostName": "ozone-datanode-3.ozone_default",
+          "capacity": 125645656770,
+          "used": 4382720,
+          "remaining": 104675282944,
+          "committed": 0,
+          "pendingDeletion": 0,
+          "minimumFreeSpace": 125645664
+      }
+    ]
   }
 }
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
index af586efb3fa..6466761d579 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
@@ -53,5 +53,7 @@
   "/keys/deletePending/dirs?limit=*": "/dirdeletePending",
   "/datanodes/decommission/info": "/decommissioninfo",
   "/datanodes/decommission/info/datanode?uuid=*": "/DatanodesDecommissionInfo",
-  "/datanodes/remove": "/datanodesRemove"
+  "/datanodes/remove": "/datanodesRemove",
+
+  "/storageDistribution": "/utilization"
 }
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/themeIcons.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/themeIcons.tsx
index 906a528cd28..d5cc414994a 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/themeIcons.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/themeIcons.tsx
@@ -98,3 +98,30 @@ export class ReplicationIcon extends 
React.PureComponent<IReplicationIconProps>
     return icon;
   }
 }
+
+interface IGraphLegendIconProps {
+  color: string;
+  height?: number;
+};
+export class GraphLegendIcon extends 
React.PureComponent<IGraphLegendIconProps> {
+  render() {
+    const { color, height = 14 } = this.props;
+
+    return (
+      <svg
+      width="18"
+      height={height}
+      viewBox={`0 0 18 ${height}`}
+      xmlns="http://www.w3.org/2000/svg";
+      style={{ display: 'inline-block', verticalAlign: 'middle' }} // 
Optional: helps with alignment
+    >
+      <circle
+        cx="6"
+        cy="6"
+        r="6" 
+        fill={color} // Use the color prop for the fill
+      />
+    </svg>
+    )
+  }
+};
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewCardWrapper.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewCardWrapper.tsx
similarity index 100%
rename from 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewCardWrapper.tsx
rename to 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewCardWrapper.tsx
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewSimpleCard.tsx
similarity index 100%
rename from 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx
rename to 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewSimpleCard.tsx
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewStorageCard.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewStorageCard.tsx
new file mode 100644
index 00000000000..e4b10e1218b
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewStorageCard.tsx
@@ -0,0 +1,248 @@
+/*
+ * 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, { HTMLAttributes, useMemo } from 'react';
+import filesize from 'filesize';
+import { Card, Row, Col, Table, Tag } from 'antd';
+
+import EChart from '@/v2/components/eChart/eChart';
+import OverviewCardWrapper from '@/v2/components/cards/overviewCardWrapper';
+
+import { StorageReport } from '@/v2/types/overview.types';
+
+// ------------- Types -------------- //
+type OverviewStorageCardProps = {
+  loading?: boolean;
+  storageReport: StorageReport;
+}
+
+const size = filesize.partial({ round: 1 });
+
+function getUsagePercentages(
+  { used, remaining, capacity, committed }: StorageReport): ({
+    ozoneUsedPercentage: number,
+    nonOzoneUsedPercentage: number,
+    committedPercentage: number,
+    usagePercentage: number
+  }) {
+  return {
+    ozoneUsedPercentage: Math.floor(used / capacity * 100),
+    nonOzoneUsedPercentage: Math.floor((capacity - remaining - used) / 
capacity * 100),
+    committedPercentage: Math.floor(committed / capacity * 100),
+    usagePercentage: Math.round((capacity - remaining) / capacity * 100)
+  }
+}
+
+// ------------- Styles -------------- //
+const cardHeadStyle: React.CSSProperties = { fontSize: '14px' };
+const cardBodyStyle: React.CSSProperties = { padding: '16px' };
+const cardStyle: React.CSSProperties = {
+  boxSizing: 'border-box',
+  height: '100%'
+}
+const cardErrorStyle: React.CSSProperties = {
+  borderColor: '#FF4D4E',
+  borderWidth: '1.4px'
+}
+const eChartStyle: React.CSSProperties = {
+  width: '280px',
+  height: '200px'
+}
+
+
+// ------------- Component -------------- //
+const OverviewStorageCard: React.FC<OverviewStorageCardProps> = ({
+  loading = false,
+  storageReport = {
+    capacity: 0,
+    used: 0,
+    remaining: 0,
+    committed: 0
+  }
+}) => {
+
+  const {
+    ozoneUsedPercentage,
+    nonOzoneUsedPercentage,
+    committedPercentage,
+    usagePercentage
+  } = useMemo(() =>
+    getUsagePercentages(storageReport),
+    [
+      storageReport.capacity,
+      storageReport.committed,
+      storageReport.remaining,
+      storageReport.used,
+    ]
+  )
+
+  let capacityData = [{
+    value: ozoneUsedPercentage,
+    itemStyle: {
+      color: '#52C41A'
+    }
+  }, {
+    value: nonOzoneUsedPercentage,
+    itemStyle: {
+      color: '#1890FF'
+    }
+  }, {
+    value: committedPercentage,
+    itemStyle: {
+      color: '#FF595E'
+    }
+  }]
+  // Remove all zero values
+  // because guage chart shows a dot if value is zero
+  capacityData = capacityData.filter((val) => val.value > 0)
+
+  const eChartOptions = {
+    title: {
+      left: 'center',
+      bottom: 'bottom',
+      text: `${size(storageReport.capacity - storageReport.remaining)} / 
${size(storageReport.capacity)}`,
+      textStyle: {
+        fontWeight: 'normal',
+        fontFamily: 'Roboto'
+      }
+    },
+    series: [
+      {
+        type: 'gauge',
+        startAngle: 90,
+        endAngle: -270,
+        radius: '70%',
+        center: ['50%', '45%'],
+        bottom: '50%',
+        pointer: {
+          show: false
+        },
+        progress: {
+          show: true,
+          overlap: true,
+          roundCap: true,
+          clip: true
+        },
+        splitLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          show: false,
+          distance: 50
+        },
+        detail: {
+          rich: {
+            value: {
+              fontSize: 24,
+              fontWeight: 400,
+              fontFamily: 'Roboto',
+              color: '#1B232A'
+            },
+            percent: {
+              fontSize: 20,
+              fontWeight: 400,
+              color: '#1B232A'
+            }
+          },
+          formatter: `{value|${usagePercentage}}{percent|%}`,
+          offsetCenter: [0, 0]
+        },
+        data: capacityData
+      }
+    ]
+  }
+
+  const cardChildren = (
+    <Card
+      size='small'
+      className={'overview-card'}
+      loading={loading}
+      hoverable={false}
+      title='Cluster Capacity'
+      headStyle={cardHeadStyle}
+      bodyStyle={cardBodyStyle}
+      style={(usagePercentage > 79) ? {...cardStyle, ...cardErrorStyle} : 
cardStyle} >
+      <Row justify='space-between'>
+        <Col
+          className='echart-col'
+          xs={24} sm={24} md={12} lg={12} xl={12}>
+          <EChart
+            option={eChartOptions}
+            style={eChartStyle} />
+        </Col>
+        <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+          <Table
+            size='small'
+            pagination={false}
+            columns={[
+              {
+                title: 'Usage',
+                dataIndex: 'usage',
+                key: 'usage'
+              },
+              {
+                title: 'Size',
+                dataIndex: 'size',
+                key: 'size',
+                align: 'right'
+              },
+            ]}
+            dataSource={[
+              {
+                key: 'ozone-used',
+                usage: <Tag key='ozone-used' color='green'>Ozone Used</Tag>,
+                size: size(storageReport.used)
+              },
+              {
+                key: 'non-ozone-used',
+                usage: <Tag key='non-ozone-used' color='blue'>Non Ozone 
Used</Tag>,
+                size: size(storageReport.capacity - storageReport.remaining - 
storageReport.used)
+              },
+              {
+                key: 'remaining',
+                usage: <Tag key='remaining' color='#E6EBF8'>
+                  <span style={{ color: '#4c7cf5' }}>Remaining</span>
+                </Tag>,
+                size: size(storageReport.remaining)
+              },
+              {
+                key: 'pre-allocated',
+                usage: <Tag key='pre-allocated' color='red'>Container 
Pre-allocated</Tag>,
+                size: size(storageReport.committed)
+              }
+            ]}
+            onRow={(record) => ({
+              'data-testid': `capacity-${record.key}`
+            }) as HTMLAttributes<HTMLElement>} />
+        </Col>
+      </Row>
+    </Card>
+  )
+
+  return (
+    <OverviewCardWrapper
+      linkToUrl={'/NamespaceUsage'}
+      title='Report'
+      children={cardChildren} />
+  )
+}
+
+export default OverviewStorageCard;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewSummaryCard.tsx
similarity index 100%
rename from 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx
rename to 
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/cards/overviewSummaryCard.tsx
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
index 518afef0198..6bdac24afb7 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
@@ -138,6 +138,12 @@ const NavBar: React.FC<NavBarProps> = ({
       <Link to='/NamespaceUsage' />
     </Menu.Item>
   ), (
+    <Menu.Item key='/Capacity'
+      icon={<PieChartOutlined />}>
+      <span>Cluster Capacity</span>
+      <Link to='/Capacity' />
+    </Menu.Item>
+  ),(
     isHeatmapEnabled &&
     <Menu.Item key='/Heatmap'
       icon={<LayoutOutlined />}>
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
new file mode 100644
index 00000000000..7e4f1ef7ba9
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.less
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+
+.data-container {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  height: 100%;
+  
+  .section-title {
+    flex-grow: 0;
+    font-family: Roboto;
+    font-size: 20px;
+    font-weight: 500;
+    font-stretch: normal;
+    font-style: normal;
+    line-height: 1.4;
+    letter-spacing: normal;
+    text-align: left;
+    color: #1b2329;
+  }
+
+  .node-select-container {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    margin: 16px 0px auto 0px;
+    height: 15em;
+  }
+
+  .cluster-card-data-container {
+    display: flex;
+    justify-content: space-between;
+    gap: 16px;
+
+    &.vertical-layout {
+      flex-direction: column;
+      gap: 0px;
+    }
+
+    .cluster-card-statistic {
+      flex: 2 1 10em;
+    }
+
+    .data-detail-item {
+      display: flex;
+      width: 100%;
+      flex-direction: column;
+      justify-content: flex-start;
+      align-items: stretch;
+      padding: 16px;
+      border-radius: 3px;
+
+      .data-detail-breakdown-container {
+        justify-content: flex-end;
+        flex-wrap: wrap;
+        .data-detail-breakdown-statistic {
+          flex: 0 0 50%;
+          max-width: 25%;
+          text-align: right;
+        }
+      }
+    }
+
+    .data-detail-breakdown-container {
+      .ant-statistic-title {
+        margin-bottom: 0;
+      }
+
+      .ant-statistic-content {
+        .ant-statistic-content-prefix {
+          margin-right: 1px;
+        }
+        .ant-statistic-content-value {
+          font-size: 16px;
+        }
+      }
+    }
+  }
+
+  .stacked-progress {
+    display: flex;
+    width: 100%;
+    height: 8px;
+    border-radius: 100px;
+    overflow: hidden;
+    margin: 24px auto 8px auto;
+  }
+  .stacked-progress-empty {
+    width: 100%;
+    height: 8px;
+    border-radius: 100px;
+    background-color: #f4f5f6;
+  }
+}
+
+.data-breakdown-section {
+  display: flex;
+  gap: 16px;
+  width: 100%;
+  justify-content: space-between;
+  align-items: stretch;
+
+  > .ant-card {
+    flex: 1 1 0;
+    min-width: 0;
+  }
+}
+
+.unused-space-breakdown {
+  display: grid;
+  grid-template-columns: 150px auto;
+  grid-column-gap: 20px;
+  grid-row-gap: 4px;
+
+  .ant-tag {
+    text-align: center;
+  }
+}
+
+.ant-statistic-title {
+  font-size: 12px;
+}
+
+// This is for the suffix part of the value ex: TB, GB etc
+.ant-statistic-content-suffix {
+  font-family: Roboto;
+  font-size: 14px;
+  font-weight: normal;
+  font-stretch: normal;
+  font-style: normal;
+  line-height: 1.43;
+  letter-spacing: normal;
+  text-align: left;
+  vertical-align: text-top;
+  color: rgba(0, 0, 0, 0.85);
+  margin: 3px 0 0 1px;
+}
+
+.ant-divider-horizontal {
+  margin: 16px 0;
+}
+
+.ant-card-body {
+  // This is to enforce 16px padding for card body which is 12px by default
+  padding: 16px !important;
+}
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
new file mode 100644
index 00000000000..d2acb63cd79
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/capacity.tsx
@@ -0,0 +1,312 @@
+/*
+ * 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 { Popover, Tag, Typography } from 'antd';
+import React from 'react';
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+
+import './capacity.less';
+import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
+import { showDataFetchError } from '@/utils/common';
+import { AxiosError } from 'axios';
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import moment from 'moment';
+import CapacityBreakdown from 
'@/v2/pages/capacity/components/CapacityBreakdown';
+import CapacityDetail from '@/v2/pages/capacity/components/CapacityDetail';
+import { datanodesPendingDeletionDesc, otherUsedSpaceDesc, ozoneUsedSpaceDesc, 
totalCapacityDesc } from '@/v2/pages/capacity/constants/descriptions.constants';
+import WrappedInfoIcon from '@/v2/pages/capacity/components/WrappedInfoIcon';
+import filesize from 'filesize';
+import { InfoCircleOutlined } from '@ant-design/icons';
+
+type CapacityState = {
+  loading: boolean;
+  lastUpdated: number;
+};
+
+const Capacity: React.FC<object> = () => {
+
+  const [state, setState] = React.useState<CapacityState>({
+    loading: false,
+    lastUpdated: 0
+  });
+  const [globalStorage, setGlobalStorage] = React.useState<GlobalStorage>({
+    totalUsedSpace: 0,
+    totalFreeSpace: 0,
+    totalCapacity: 0,
+  });
+
+  // Not being used for now
+  // const [globalNamespace, setGlobalNamespace] = 
React.useState<GlobalNamespace>({
+  //   totalUsedSpace: 0,
+  //   totalKeys: 0
+  // });
+
+  const [usageBreakdown, setUsageBreakdown] = 
React.useState<UsedSpaceBreakdown>({
+    openKeysBytes: 0,
+    committedBytes: 0,
+    containerPreAllocated: 0,
+    deletionPendingBytes: {
+      total: 0,
+      byStage: {
+        DN: {
+            pendingBytes: 0
+        },
+        SCM: {
+            pendingBytes: 0
+        },
+        OM: {
+            pendingKeyBytes: 0,
+            pendingBytes: 0,
+            pendingDirectoryBytes: 0
+        }
+      }
+    }
+  });
+  const [datanodeUsage, setDatanodeUsage] = 
React.useState<DataNodeUsage[]>([]);
+  const [selectedDatanode, setSelectedDatanode] = React.useState<string | 
null>(null);
+
+  const cancelSignal = React.useRef<AbortController>();
+
+  const loadData = () => {
+    setState(prev => ({
+      ...prev,
+      loading: true
+    }));
+
+    cancelRequests([cancelSignal.current!]);
+    const { request, controller } = AxiosGetHelper(
+      '/api/v1/storageDistribution',
+      cancelSignal.current
+    );
+    cancelSignal.current = controller;
+    
+    request.then(response => {
+      const utilizationResponse: UtilizationResponse = response.data;
+      setGlobalStorage(utilizationResponse.globalStorage);
+      // setGlobalNamespace(utilizationResponse.globalNamespace);
+      setUsageBreakdown(utilizationResponse.usedSpaceBreakdown);
+      setDatanodeUsage(utilizationResponse.dataNodeUsage);
+      setSelectedDatanode(utilizationResponse.dataNodeUsage[0].hostName);
+      setState({
+        loading: false,
+        lastUpdated: Number(moment())
+      });
+    }).catch(error => {
+      setState(prev => ({
+        ...prev,
+        loading: true
+      }));
+      showDataFetchError((error as AxiosError).toString());
+    });
+  };
+
+  const  autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+  React.useEffect(() => {
+    autoReloadHelper.startPolling();
+    loadData();
+
+    return (() => {
+      autoReloadHelper.stopPolling();
+      cancelRequests([cancelSignal.current!]);
+    });
+  }, []);
+
+  const selectedDNDetails: DataNodeUsage = React.useMemo(() => {
+    return datanodeUsage.find(datanode => datanode.hostName === 
selectedDatanode) ?? {
+      datanodeUuid: "unknown-uuid",
+      hostName: "unknown-host",
+      capacity: 0,
+      used: 0,
+      remaining: 0,
+      committed: 0,
+      pendingDeletion: 0
+    } as DataNodeUsage;
+  }, [selectedDatanode]);
+
+  const unusedSpaceBreakdown = (
+    <span>
+      UNUSED
+      <Popover
+        title="Unused Space Breakdown"
+        placement='topLeft'
+        content={
+          <div className='unused-space-breakdown'>
+            Minimum Free Space
+            <Tag color='red'>To Be Added</Tag>
+            Remaining
+            <Tag color='green'>{filesize(selectedDNDetails.remaining, { round: 
1})}</Tag>
+          </div>
+        }
+      >
+        <InfoCircleOutlined style={{ color: '#2f84d8', fontSize: 12, 
marginLeft: 4 }} />
+      </Popover>
+    </span>
+  )
+
+  return (
+    <>
+      <div className='page-header-v2'>
+        Cluster Capacity
+        <AutoReloadPanel
+          isLoading={state.loading}
+          lastRefreshed={state.lastUpdated}
+          togglePolling={autoReloadHelper.handleAutoReloadToggle}
+          onReload={loadData} />
+      </div>
+      <div className='data-container'>
+        <Typography.Title level={4} 
className='section-title'>Cluster</Typography.Title>
+        <CapacityBreakdown
+          title='Ozone Capacity'
+          loading={state.loading}
+          items={[{
+            title: (
+              <span>
+                TOTAL
+                <WrappedInfoIcon title={totalCapacityDesc} />
+              </span>
+            ),
+            value: globalStorage.totalCapacity,
+          }, {
+            title: 'OZONE USED SPACE',
+            value: globalStorage.totalUsedSpace,
+            color: '#f4a233'
+          }, {
+            title: (
+              <span>
+                OTHER USED SPACE
+                <WrappedInfoIcon title={otherUsedSpaceDesc} />
+              </span>
+            ),
+            value: globalStorage.totalCapacity - globalStorage.totalFreeSpace 
- globalStorage.totalUsedSpace,
+            color: '#11073a'
+          }, {
+            title: 'CONTAINER PRE-ALLOCATED',
+            value: usageBreakdown.containerPreAllocated,
+            color: '#f47b2d'
+          }, {
+            title: 'REMAINING SPACE',
+            value: globalStorage.totalFreeSpace,
+            color: '#4553ee'
+          }]}
+        />
+        <Typography.Title level={4} 
className='section-title'>Service</Typography.Title>
+        <CapacityBreakdown
+          title={(
+            <span>
+              Ozone Used Space
+              <WrappedInfoIcon title={ozoneUsedSpaceDesc} />
+            </span>
+          )}
+          loading={state.loading}
+          items={[{
+            title: 'TOTAL',
+            value: globalStorage.totalUsedSpace
+          }, {
+            title: 'OPEN KEYS',
+            value: usageBreakdown.openKeysBytes,
+            color: '#f47c2d'
+          }, {
+            title: 'COMMITTED KEYS',
+            value: usageBreakdown.committedBytes,
+            color: '#f4a233'
+          }, {
+            title: 'PENDING DELETION',
+            value: usageBreakdown.deletionPendingBytes.total,
+            color: '#10073b'
+          }]}
+        />
+        <div className='data-breakdown-section'>
+          <CapacityDetail
+            title='Pending Deletion'
+            loading={state.loading}
+            showDropdown={false}
+            dataDetails={[{
+              title: 'OZONE MANAGER',
+              size: usageBreakdown.deletionPendingBytes.byStage.OM.totalBytes 
?? 0,
+              breakdown: [{
+                label: 'KEYS',
+                value: 
usageBreakdown.deletionPendingBytes.byStage.OM.pendingKeyBytes,
+                color: '#f4a233'
+              }, {
+                label: 'DIRECTORIES',
+                value: 
usageBreakdown.deletionPendingBytes.byStage.OM.pendingDirectoryBytes,
+                color: '#10073b'
+              }]
+            }, {
+              title: 'STORAGE CONTAINER MANAGER',
+              size: 
usageBreakdown.deletionPendingBytes.byStage.SCM.pendingBytes,
+              breakdown: [{
+                label: 'BLOCKS',
+                value: 
usageBreakdown.deletionPendingBytes.byStage.SCM.pendingBytes,
+                color: '#f4a233'
+              }]
+            }, {
+              title: (
+                <span>
+                  DATANODES
+                  <WrappedInfoIcon title={datanodesPendingDeletionDesc} />
+                </span>
+              ),
+              size: 
usageBreakdown.deletionPendingBytes.byStage.DN.pendingBytes,
+              breakdown: [{
+                label: 'BLOCKS',
+                value: 
usageBreakdown.deletionPendingBytes.byStage.DN.pendingBytes,
+                color: '#f4a233'
+              }]
+            }]} />
+          <CapacityDetail
+            title='Datanode'
+            loading={state.loading}
+            showDropdown={true}
+            handleSelect={setSelectedDatanode}
+            dropdownItems={datanodeUsage.map(datanode => datanode.hostName)}
+            dataDetails={[{
+              title: 'USED SPACE',
+              size: (selectedDNDetails.used ?? 0) + 
(selectedDNDetails.pendingDeletion ?? 0),
+              breakdown: [{
+                label: 'PENDING DELETION',
+                value: selectedDNDetails.pendingDeletion ?? 0,
+                color: '#f4a233'
+              }, {
+                label: 'OZONE USED',
+                value: selectedDNDetails.used ?? 0,
+                color: '#10073b'
+              }]
+            }, {
+              title: 'FREE SPACE',
+              size: (selectedDNDetails.remaining ?? 0) + 
(selectedDNDetails.committed ?? 0),
+              breakdown: [{
+                label: unusedSpaceBreakdown,
+                value: selectedDNDetails.remaining ?? 0,
+                color: '#f4a233'
+              }, {
+                label: 'OZONE PRE-ALLOCATED',
+                value: selectedDNDetails.committed ?? 0,
+                color: '#10073b'
+              }]
+            }]} />
+        </div>
+      </div>
+    </>
+
+  )
+
+};
+
+export default Capacity;
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityBreakdown.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityBreakdown.tsx
new file mode 100644
index 00000000000..4ca85a32d5f
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityBreakdown.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { GraphLegendIcon } from '@/utils/themeIcons';
+import StackedProgress from '@/v2/pages/capacity/components/StackedProgress';
+import { cardHeadStyle, statisticValueStyle } from 
'@/v2/pages/capacity/constants/styles.constants';
+import { Card, Statistic } from 'antd';
+import filesize from 'filesize';
+import React from 'react';
+
+type GridItem = {
+  title: string | React.ReactNode;
+  value: number;
+  color?: string;
+  format?: 'bytes' | 'number' | 'percentage';
+};
+
+type ClusterCardProps = {
+  title: string | React.ReactNode;
+  items: GridItem[];
+  loading: boolean;
+};
+
+const getProgressSegments = (items: GridItem[]) => {
+  return items.filter(item => item.color).map((item) => ({
+    value: item.value,
+    color: item.color!,
+    label: item.title
+  } as Segment));
+}
+
+const CapacityBreakdown: React.FC<ClusterCardProps> = ({ title, items, loading 
}) => {
+
+  return (
+    <Card title={title} size='small' headStyle={cardHeadStyle} 
loading={loading}>
+      <div className='cluster-card-data-container'>
+        {items.map((item, idx) => {
+          // Split the size into the value and the unit
+          const size = filesize(item.value, { round: 1 }).split(' ');
+          return (
+            <Statistic
+              key={`cluster-statistic-${item.title}-${idx}`}
+              title={item.title}
+              prefix={item.color ? <GraphLegendIcon color={item.color} /> : 
undefined}
+              value={size[0]}
+              suffix={size[1]}
+              valueStyle={statisticValueStyle}
+              className='cluster-card-statistic'
+            />
+          )
+        })}
+      </div>
+      <StackedProgress segments={getProgressSegments(items)} />
+    </Card>
+  );
+};
+
+export default CapacityBreakdown;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
new file mode 100644
index 00000000000..b2fd9501824
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetail.tsx
@@ -0,0 +1,143 @@
+/*
+ * 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 { EChart } from '@/components/eChart/eChart';
+import { GraphLegendIcon } from '@/utils/themeIcons';
+import { cardHeadStyle, statisticValueStyle } from 
'@/v2/pages/capacity/constants/styles.constants';
+import { Card, Divider, Row, Select, Statistic } from 'antd';
+import filesize from 'filesize';
+import React from 'react';
+
+type DataDetailItem = {
+  title: string | React.ReactNode;
+  size: number;
+  breakdown: Segment[];
+}
+
+type CapacityDetailProps = {
+  title: string;
+  showDropdown: boolean;
+  dataDetails: DataDetailItem[];
+  dropdownItems?: string[];
+  handleSelect?: React.Dispatch<React.SetStateAction<string | null>>
+  loading: boolean;
+};
+
+const getEchartOptions = (title: string | React.ReactNode, data: 
DataDetailItem) => {
+  const option = {
+    grid: {
+      left: 2,
+      right: 4,
+      top: 16,
+      bottom: 0
+    },
+    xAxis: {
+      // Use linear scale to support zero values safely
+      type: 'value',
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { show: false }
+    },
+    yAxis: {
+      type: 'category',
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { show: false },
+    },
+  };
+
+  const breakdownLen = data.breakdown.length;
+  const series = data.breakdown.map((breakdown, idx) => ({
+    type: 'bar',
+    ...(breakdownLen > 1 && { stack: title }),
+    itemStyle: {
+      ...(idx === breakdownLen - 1 && { borderRadius: [0, 50, 50, 0] }),
+      ...(idx === 0 && { borderRadius: [50, 0, 0, 50] }),
+      ...(breakdownLen === 1 && { borderRadius: [50, 50, 50, 50] }),
+      color: breakdown.color,
+    },
+    data: [breakdown.value],
+    barWidth: '10px',
+    barGap: '2px'
+  }));
+
+  return {
+    ...option,
+    series
+  } as any
+}
+
+
+const CapacityDetail: React.FC<CapacityDetailProps> = (
+  { title, showDropdown, dropdownItems, dataDetails, handleSelect, loading }
+) => {
+
+  const options = dropdownItems?.map((item) => ({
+    label: item,
+    value: item,
+  })) ?? [];
+
+  return (
+    <Card title={title} size='small' headStyle={cardHeadStyle} 
loading={loading}>
+      { showDropdown && options.length > 0 &&
+        <div className='node-select-container'>
+          <strong>Node Selector:</strong>
+          <Select
+            defaultValue={options?.[0]?.value}
+            options={options}
+            onChange={handleSelect}
+            style={{ marginBottom: '16px' }}
+          />
+        </div>
+      }
+      <div className='cluster-card-data-container vertical-layout'>
+        {dataDetails.map((data, idx) => {
+          const size = filesize(data.size, { round: 1 }).split(' ');
+          return (
+            <div key={`data-detail-${data.title}-${idx}`} 
className='data-detail-item'>
+              <Statistic
+                title={data.title}
+                value={size[0]}
+                suffix={size[1]}
+                valueStyle={statisticValueStyle}
+                className='data-detail-statistic'
+              />
+              <Row className='data-detail-breakdown-container'>
+                {data.breakdown.map((item, idx) => (
+                  <Statistic
+                    key={`data-detail-breakdown-${item.label}-${idx}`}
+                    title={item.label}
+                    prefix={<GraphLegendIcon color={item.color} height={12}/>}
+                    value={filesize(item.value, { round: 1 })}
+                    className='data-detail-breakdown-statistic'
+                  />
+                ))}
+                <EChart
+                  option={getEchartOptions(data.title, data)}
+                  style={{ height: '40px', width: '100%', margin: '10px 0px' 
}} />
+                {idx < dataDetails.length - 1 && <Divider />}
+              </Row>
+            </div>
+          )
+        })}
+      </div>
+    </Card>
+  );
+}
+
+export default CapacityDetail;
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/StackedProgress.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/StackedProgress.tsx
new file mode 100644
index 00000000000..67b6f9dcad9
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/StackedProgress.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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, { useMemo } from 'react';
+
+type StackedProgressProps = {
+  segments: Segment[];
+};
+
+const StackedProgress: React.FC<StackedProgressProps> = ({
+  segments,
+}) => {
+  const total = useMemo(() => {
+    return segments.reduce((sum, item) => sum + item.value, 0);
+  }, [segments]);
+
+  // Handle the case where there is no data to show
+  if (!total || total === 0) {
+    return (
+      <div className='stacked-progress-empty' />
+    );
+  }
+
+  return (
+    <div className='stacked-progress'>
+      {segments.map((segment, idx) => {
+        const segmentWidth = (segment.value / total) * 100;
+        return (
+          <div
+            key={segment.label || idx}
+            style={{
+              width: `${segmentWidth}%`,
+              backgroundColor: segment.color,
+            }}
+          />
+        );
+      })}
+    </div>
+  );
+};
+
+export default StackedProgress;
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/WrappedInfoIcon.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/WrappedInfoIcon.tsx
new file mode 100644
index 00000000000..f98a5f5e2f1
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/WrappedInfoIcon.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { InfoCircleOutlined } from "@ant-design/icons";
+import { Tooltip } from "antd";
+
+type WrappedInfoIconProps = {
+  title: string;
+  placement?: "topLeft" | "topRight" | "bottomLeft" | "bottomRight" | "top" | 
"bottom" | "left" | "right";
+}
+
+const WrappedInfoIcon: React.FC<WrappedInfoIconProps> = ({ title, placement = 
"right" }) => {
+  return (
+    <Tooltip title={title} placement={placement}>
+      <InfoCircleOutlined style={{ color: '#2f84d8', fontSize: 12, marginLeft: 
4 }} />
+    </Tooltip>
+  )
+};
+
+export default WrappedInfoIcon;
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/descriptions.constants.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/descriptions.constants.tsx
new file mode 100644
index 00000000000..76e3670625d
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/descriptions.constants.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+export const totalCapacityDesc = 'The space configured for Ozone to use in the 
cluster. The actual disk space may be larger than what is allocated to Ozone.';
+
+export const otherUsedSpaceDesc = 'This is the space occupied by other Ozone 
related files but not actual data stored by Ozone. This may include things like 
logs, configuration files, Rocks DB files etc.';
+
+export const ozoneUsedSpaceDesc = 'These could also include potential missing 
space or extra occupied space due to situations like under-replication, 
over-replication, mismatched replicas, etc.';
+
+export const datanodesPendingDeletionDesc = 'This is the unreplicated size and 
a cumulative value of all the blocks across all the datanodes in the cluster.';
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/styles.constants.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/styles.constants.tsx
new file mode 100644
index 00000000000..0038f4c9ccb
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/constants/styles.constants.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+export const cardHeadStyle: React.CSSProperties = {
+  width: '100%',
+  flexGrow: 0,
+  fontFamily: 'Roboto',
+  fontSize: '16px',
+  fontWeight: 500,
+  fontStretch: 'normal',
+  fontStyle: 'normal',
+  lineHeight: 1.5,
+  letterSpacing: 'normal',
+  textAlign: 'left',
+  color: '#5a656d'
+};
+
+export const statisticValueStyle: React.CSSProperties = {
+  fontFamily: 'Roboto',
+  fontSize: '24px',
+  fontWeight: 'normal',
+  fontStretch: 'normal',
+  fontStyle: 'normal',
+  lineHeight: 1.33,
+  letterSpacing: 'normal',
+  textAlign: 'left',
+  color: '#1b2329'
+};
\ 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 6014577f90a..0d0de717685 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
@@ -28,9 +28,9 @@ import {
 import { Link } from 'react-router-dom';
 
 import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
-import OverviewSummaryCard from 
'@/v2/components/overviewCard/overviewSummaryCard';
-import OverviewStorageCard from 
'@/v2/components/overviewCard/overviewStorageCard';
-import OverviewSimpleCard from 
'@/v2/components/overviewCard/overviewSimpleCard';
+import OverviewSummaryCard from '@/v2/components/cards/overviewSummaryCard';
+import OverviewStorageCard from '@/v2/components/cards/overviewStorageCard';
+import OverviewSimpleCard from '@/v2/components/cards/overviewSimpleCard';
 
 import { AutoReloadHelper } from '@/utils/autoReloadHelper';
 import { checkResponseError, showDataFetchError } from '@/utils/common';
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 f465c64756a..cbf1f93e5cf 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
@@ -26,6 +26,7 @@ const NamespaceUsage = lazy(() => 
import('@/v2/pages/namespaceUsage/namespaceUsa
 const Containers = lazy(() => import('@/v2/pages/containers/containers'));
 const Insights = lazy(() => import('@/v2/pages/insights/insights'));
 const OMDBInsights = lazy(() => import('@/v2/pages/insights/omInsights'));
+const Capacity = lazy(() => import('@/v2/pages/capacity/capacity'));
 const Heatmap = lazy(() => import('@/v2/pages/heatmap/heatmap'));
 
 
@@ -66,6 +67,10 @@ export const routesV2 = [
     path: '/Om',
     component: OMDBInsights
   },
+  {
+    path: '/Capacity',
+    component: Capacity
+  },
   {
     path: '/Heatmap',
     component: Heatmap
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/capacity.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/capacity.types.ts
new file mode 100644
index 00000000000..8b9e12552c4
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/capacity.types.ts
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+type GlobalStorage = {
+  totalUsedSpace: number;
+  totalFreeSpace: number;
+  totalCapacity: number;
+};
+
+type GlobalNamespace = {
+  totalUsedSpace: number;
+  totalKeys: number;
+};
+
+type UsedSpaceBreakdown = {
+  openKeysBytes: number;
+  committedBytes: number;
+  containerPreAllocated: number;
+  deletionPendingBytes: {
+    total: number;
+    byStage: {
+      DN: {
+        pendingBytes: number;
+      };
+      SCM: {
+        pendingBytes: number;
+      };
+      OM: {
+        pendingKeyBytes: number;
+        pendingBytes: number;
+        pendingDirectoryBytes: number;
+      };
+    };
+  };
+};
+
+type DataNodeUsage = {
+  uuid: string;
+  capacity: number;
+  used: number;
+  remaining: number;
+  committed: number;
+  pendingDeletion: number;
+};
+
+type UtilizationResponse = {
+  globalStorage: GlobalStorage;
+  globalNamespace: GlobalNamespace;
+  usedSpaceBreakdown: UsedSpaceBreakdown;
+  dataNodeUsage: DataNodeUsage[];
+};
+
+type Segment = {
+  value: number;
+  color: string;
+  label: string | React.ReactNode;
+};


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

Reply via email to