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]