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

spacemonkd pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new ed9e5ff1c19 HDDS-15272. Add error handling for SCM failure in cluster 
capacity (#10411)
ed9e5ff1c19 is described below

commit ed9e5ff1c19ae0b8cae2185edf333f72a1c32748
Author: Abhishek Pal <[email protected]>
AuthorDate: Wed Jun 3 13:24:24 2026 +0530

    HDDS-15272. Add error handling for SCM failure in cluster capacity (#10411)
---
 .../src/__tests__/capacity/Capacity.test.tsx       | 53 ++++++++++++++++++++++
 .../src/v2/pages/capacity/capacity.less            | 18 ++++++++
 .../src/v2/pages/capacity/capacity.tsx             | 17 ++++++-
 .../pages/capacity/components/CapacityDetail.tsx   | 49 ++++++++++++++------
 .../capacity/components/CapacityDetailError.tsx    | 39 ++++++++++++++++
 5 files changed, 159 insertions(+), 17 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
index 94109adb274..d6f878aaad0 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/capacity/Capacity.test.tsx
@@ -18,9 +18,11 @@
 
 import React from 'react';
 import { render, screen, waitFor } from '@testing-library/react';
+import { rest } from 'msw';
 
 import Capacity from '@/v2/pages/capacity/capacity';
 import { capacityServer } from '@tests/mocks/capacityMocks/capacityServer';
+import * as mockResponses from 
'@tests/mocks/capacityMocks/capacityResponseMocks';
 
 vi.mock('@/components/autoReloadPanel/autoReloadPanel', () => ({
   default: () => <div data-testid="auto-reload-panel" />,
@@ -92,4 +94,55 @@ describe('Capacity Page', () => {
     );
     expect(datanodeCard).toHaveTextContent(/FREE SPACE\s*3\s*KB/i);
   });
+
+  test('shows scm-only error state when SCM pending deletion returns sentinel 
failure values', async () => {
+    capacityServer.use(
+      rest.get('api/v1/pendingDeletion', (req, res, ctx) => {
+        const component = req.url.searchParams.get('component');
+        switch (component) {
+        case 'scm':
+          return res(
+            ctx.status(200),
+            ctx.json({
+              totalBlocksize: -1,
+              totalReplicatedBlockSize: -1,
+              totalBlocksCount: -1
+            })
+          );
+        case 'om':
+          return res(
+            ctx.status(200),
+            ctx.json(mockResponses.OmPendingDeletion)
+          );
+        case 'dn':
+          return res(
+            ctx.status(200),
+            ctx.json(mockResponses.DnPendingDeletion)
+          );
+        default:
+          return res(
+            ctx.status(400),
+            ctx.json({ message: 'Unsupported pending deletion component.' })
+          );
+        }
+      })
+    );
+
+    render(<Capacity />);
+
+    const pendingDeletionTitle = await screen.findByText('Pending Deletion');
+    const pendingDeletionCard = pendingDeletionTitle.closest('.ant-card');
+    expect(pendingDeletionCard).not.toBeNull();
+    if (!pendingDeletionCard) {
+      return;
+    }
+
+    await waitFor(() =>
+      expect(pendingDeletionCard).toHaveTextContent(/OZONE MANAGER\s*2\s*KB/i)
+    );
+    expect(pendingDeletionCard).toHaveTextContent(/DATANODES\s*3\s*KB/i);
+    expect(pendingDeletionCard).toHaveTextContent(/STORAGE CONTAINER 
MANAGER\s*N\/A/i);
+    expect(await 
screen.findByTestId('pending-deletion-scm-error')).toBeInTheDocument();
+    await waitFor(() => 
expect(screen.getAllByTestId('echart')).toHaveLength(4));
+  });
 });
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
index 5a1e9b0443f..a04f3482ded 100644
--- 
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
@@ -92,3 +92,21 @@
   color: #5a656d;
   margin-left: 15px;
 }
+
+.capacity-detail-error {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+  margin: 14px auto;
+  color: #5a656d;
+}
+
+.capacity-detail-error-icon {
+  color: #f47b2d;
+  font-size: 24px;
+}
+
+.capacity-detail-error-message {
+  font-size: 13px;
+}
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
index 307f5d1177a..b2e8e0522a0 100644
--- 
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
@@ -303,6 +303,16 @@ const Capacity: React.FC<object> = () => {
     </span>
   );
 
+  const hasSCMPendingDeletionError = (
+    scmPendingDeletes.data.totalBlocksize < 0
+    || scmPendingDeletes.data.totalReplicatedBlockSize < 0
+    || scmPendingDeletes.data.totalBlocksCount < 0
+  );
+
+  const scmReplicatedPendingDeletionSize = hasSCMPendingDeletionError
+    ? 0
+    : scmPendingDeletes.data.totalReplicatedBlockSize;
+
   return (
     <>
       <div className='page-header-v2'>
@@ -381,7 +391,7 @@ const Capacity: React.FC<object> = () => {
             ),
             value: (
               omPendingDeletes.data.totalSize
-              + scmPendingDeletes.data.totalReplicatedBlockSize
+              + scmReplicatedPendingDeletionSize
               + (dnPendingDeletes.data.totalPendingDeletionSize ?? 0)
             ),
             color: "#10073b"
@@ -406,7 +416,10 @@ const Capacity: React.FC<object> = () => {
               }]
             }, {
               title: 'STORAGE CONTAINER MANAGER',
-              size: scmPendingDeletes.data.totalReplicatedBlockSize,
+              size: hasSCMPendingDeletionError ? 0 : 
scmPendingDeletes.data.totalReplicatedBlockSize,
+              hasError: hasSCMPendingDeletionError,
+              errorMessage: 'SCM pending deletion details are currently 
unavailable.',
+              errorTestId: 'pending-deletion-scm-error',
               breakdown: [{
                 label: 'BLOCKS',
                 value: scmPendingDeletes.data.totalReplicatedBlockSize,
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
index 612f9f986fe..f17c52b6004 100644
--- 
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
@@ -19,6 +19,7 @@
 import { EChart } from '@/components/eChart/eChart';
 import { GraphLegendIcon } from '@/utils/themeIcons';
 import { cardHeadStyle, statisticValueStyle } from 
'@/v2/pages/capacity/constants/styles.constants';
+import CapacityDetailError from 
'@/v2/pages/capacity/components/CapacityDetailError';
 import { Segment } from '@/v2/types/capacity.types';
 import { DownloadOutlined } from '@ant-design/icons';
 import { Card, Divider, Row, Select, Spin, Statistic } from 'antd';
@@ -30,6 +31,9 @@ type DataDetailItem = {
   size: number;
   breakdown: Segment[];
   loading?: boolean;
+  hasError?: boolean;
+  errorMessage?: string;
+  errorTestId?: string;
 }
 
 type CapacityDetailProps = {
@@ -159,25 +163,40 @@ const CapacityDetail: React.FC<CapacityDetailProps> = (
                   <div key={`data-detail-${data.title}-${idx}`} 
className='data-detail-item'>
                     <Statistic
                       title={data.title}
-                      value={size[0]}
-                      suffix={size[1]}
+                      value={data.hasError ? 'N/A' : size[0]}
+                      suffix={data.hasError ? undefined : size[1]}
                       valueStyle={statisticValueStyle}
                       className='data-detail-statistic'
                       loading={data.loading}
                     />
-                    {!data.loading && <Row 
className='data-detail-breakdown-container'>
-                      {data.breakdown.map((item, idx) => (
-                        <div 
key={`data-defailt-breakdown-${item.label}-${idx}`} 
className='data-detail-breakdown-item'>
-                          <GraphLegendIcon color={item.color} height={12} />
-                          <span 
className="data-detail-breakdown-label">{item.label}</span>
-                          <span 
className="data-detail-breakdown-value">{filesize(item.value, {round: 
1})}</span>
-                        </div>
-                      ))}
-                      <EChart
-                        option={getEchartOptions(data.title, data)}
-                        style={{ height: '40px', width: '100%', margin: '10px 
0px' }} />
-                      {idx < dataDetails.length - 1 && <Divider />}
-                    </Row>}
+                    {!data.loading
+                      && (
+                        data.hasError
+                          ? (
+                            <>
+                              <CapacityDetailError
+                                message={data.errorMessage}
+                                testId={data.errorTestId}
+                              />
+                              {idx < dataDetails.length - 1 && <Divider />}
+                            </>
+                          )
+                          : (
+                            <Row className='data-detail-breakdown-container'>
+                              {data.breakdown.map((item, idx) => (
+                                <div 
key={`data-defailt-breakdown-${item.label}-${idx}`} 
className='data-detail-breakdown-item'>
+                                  <GraphLegendIcon color={item.color} 
height={12} />
+                                  <span 
className="data-detail-breakdown-label">{item.label}</span>
+                                  <span 
className="data-detail-breakdown-value">{filesize(item.value, {round: 
1})}</span>
+                                </div>
+                              ))}
+                              <EChart
+                                option={getEchartOptions(data.title, data)}
+                                style={{ height: '40px', width: '100%', 
margin: '10px 0px' }} />
+                              {idx < dataDetails.length - 1 && <Divider />}
+                            </Row>
+                          )
+                      )}
                   </div>
                 )
               })}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
new file mode 100644
index 00000000000..5f02fc5afe3
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/capacity/components/CapacityDetailError.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { DisconnectOutlined } from '@ant-design/icons';
+import React from 'react';
+
+type CapacityDetailErrorProps = {
+  message?: string;
+  testId?: string;
+}
+
+const CapacityDetailError: React.FC<CapacityDetailErrorProps> = ({
+  message = 'Pending deletion details are currently unavailable.',
+  testId
+}) => {
+  return (
+    <div className='capacity-detail-error' data-testid={testId}>
+      <DisconnectOutlined className='capacity-detail-error-icon' />
+      <span className='capacity-detail-error-message'>{message}</span>
+    </div>
+  );
+};
+
+export default CapacityDetailError;


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

Reply via email to