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

priyeshkaratha 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 bb1da7fea63 HDDS-15764. [Recon] Show error card for unavailable 
datanode in Cluster Capacity (#10767)
bb1da7fea63 is described below

commit bb1da7fea63b1efd89518486455552f35ec5380a
Author: Priyesh Karatha <[email protected]>
AuthorDate: Thu Jul 23 13:40:48 2026 +0530

    HDDS-15764. [Recon] Show error card for unavailable datanode in Cluster 
Capacity (#10767)
---
 .../src/__tests__/capacity/Capacity.test.tsx       | 69 ++++++++++++++++++++--
 .../src/v2/pages/capacity/capacity.tsx             | 30 ++++++++--
 .../pages/capacity/components/CapacityDetail.tsx   |  4 +-
 3 files changed, 92 insertions(+), 11 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 7e2914e78be..0f3204674d8 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
@@ -101,7 +101,7 @@ describe('Capacity Page', () => {
     expect(datanodeCard).toHaveTextContent(/FREE SPACE\s*3\s*KB/i);
   });
 
-  test('clamps pendingBlockSize to 0 when selected datanode reports -1 
(offline/unreachable)', async () => {
+  test('defaults to the first available datanode when the first datanode 
reports -1 (offline/unreachable)', async () => {
     capacityServer.use(
       rest.get('api/v1/pendingDeletion', (req, res, ctx) => {
         const component = req.url.searchParams.get('component');
@@ -136,13 +136,70 @@ describe('Capacity Page', () => {
     if (!datanodeCard) {
       return;
     }
-    // dn-1 is selected by default; its pendingBlockSize is -1 (offline 
sentinel).
-    // PENDING DELETION should show 0 B, not a negative value.
+    // dn-1 is unavailable (pendingBlockSize -1), but dn-2 is healthy, so the 
page should
+    // default to dn-2 and show its capacity instead of landing on the error 
card.
+    // USED SPACE = used (2048) + pendingBlockSize (2048) = 4 KB, FREE SPACE = 
remaining
+    // (2048) + committed (1024) = 3 KB.
     await waitFor(() =>
-      expect(datanodeCard).toHaveTextContent(/PENDING DELETION\s*0\s*B/i)
+      expect(datanodeCard).toHaveTextContent(/USED SPACE\s*4\s*KB/i)
     );
-    // USED SPACE = used (4096) + clamped pendingBlockSize (0) = 4 KB
-    expect(datanodeCard).toHaveTextContent(/USED SPACE\s*4\s*KB/i);
+    expect(datanodeCard).toHaveTextContent(/FREE SPACE\s*3\s*KB/i);
+    
expect(screen.queryByTestId('dn-used-space-error')).not.toBeInTheDocument();
+    
expect(screen.queryByTestId('dn-free-space-error')).not.toBeInTheDocument();
+    // The dropdown label must reflect the actually-selected DN (dn-2), not 
the first
+    // (unavailable) option.
+    const selectionItem = 
datanodeCard.querySelector('.ant-select-selection-item');
+    expect(selectionItem).toHaveTextContent('dn-2');
+    expect(selectionItem).not.toHaveTextContent('dn-1');
+  });
+
+  test('shows error card instead of outdated data when every datanode reports 
-1 (offline/unreachable)', async () => {
+    capacityServer.use(
+      rest.get('api/v1/pendingDeletion', (req, res, ctx) => {
+        const component = req.url.searchParams.get('component');
+        if (component === 'dn') {
+          return res(
+            ctx.status(200),
+            ctx.json({
+              ...mockResponses.DnPendingDeletion,
+              pendingDeletionPerDataNode: [
+                { hostName: 'dn-1', datanodeUuid: 'uuid-1', pendingBlockSize: 
-1 },
+                { hostName: 'dn-2', datanodeUuid: 'uuid-2', pendingBlockSize: 
-1 }
+              ]
+            })
+          );
+        }
+        const map: Record<string, object> = {
+          scm: mockResponses.ScmPendingDeletion,
+          om: mockResponses.OmPendingDeletion
+        };
+        const body = component ? map[component] : undefined;
+        return body
+          ? res(ctx.status(200), ctx.json(body))
+          : res(ctx.status(400), ctx.json({ message: 'Unsupported pending 
deletion component.' }));
+      })
+    );
+
+    render(<Capacity />);
+
+    const downloadLink = await screen.findByText('Download Insights');
+    const datanodeCard = downloadLink.closest('.ant-card');
+    expect(datanodeCard).not.toBeNull();
+    if (!datanodeCard) {
+      return;
+    }
+    // No DN is available, so we fall back to dn-1; its pendingBlockSize is -1 
(offline
+    // sentinel), so the datanode is unavailable and its capacity data may be 
outdated.
+    // Show an error card for USED SPACE and FREE SPACE instead of the stale
+    // storage-report values.
+    expect(await 
screen.findByTestId('dn-used-space-error')).toBeInTheDocument();
+    expect(await 
screen.findByTestId('dn-free-space-error')).toBeInTheDocument();
+    await waitFor(() =>
+      expect(datanodeCard).toHaveTextContent(/USED SPACE\s*N\/A/i)
+    );
+    expect(datanodeCard).toHaveTextContent(/FREE SPACE\s*N\/A/i);
+    // With every DN unavailable, the dropdown falls back to the first DN 
(dn-1).
+    
expect(datanodeCard.querySelector('.ant-select-selection-item')).toHaveTextContent('dn-1');
   });
 
   test('shows scm-only error state when SCM pending deletion returns sentinel 
failure values', async () => {
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 152fe43ff30..f55e0e5f1d4 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
@@ -100,11 +100,20 @@ const Capacity: React.FC<object> = () => {
     return storageDistribution.data.dataNodeUsage.filter(dn => 
pendingHostNames.has(dn.hostName));
   }, [storageDistribution.data.dataNodeUsage, 
dnPendingDeletes.data.pendingDeletionPerDataNode]);
 
-  // Seed selected datanode once data loads so dependent calculations work
+  // Seed selected datanode once data loads so dependent calculations work.
+  // Prefer the first available DN so the user does not land on the error card
+  // while a healthy DN exists; fall back to the first DN (surfacing the error
+  // card) only when every DN is unavailable.
   React.useEffect(() => {
     const hostNames = filteredDNs.map(dn => dn.hostName);
     if (!hostNames.includes(selectedDatanode)) {
-      setSelectedDatanode(hostNames[0] ?? "");
+      const unavailableHosts = new Set(
+        (dnPendingDeletes.data.pendingDeletionPerDataNode ?? [])
+          .filter(dn => dn.pendingBlockSize === -1)
+          .map(dn => dn.hostName)
+      );
+      const firstAvailable = hostNames.find(hostName => 
!unavailableHosts.has(hostName));
+      setSelectedDatanode(firstAvailable ?? hostNames[0] ?? "");
     }
   }, [filteredDNs]);
 
@@ -132,7 +141,10 @@ const Capacity: React.FC<object> = () => {
 
   const autoReload = useAutoReload(loadDataIfIdle);
 
-  const selectedDNDetails: DataNodeUsage & { pendingBlockSize: number } = 
React.useMemo(() => {
+  const selectedDNDetails: DataNodeUsage & {
+    pendingBlockSize: number;
+    unavailable: boolean;
+  } = React.useMemo(() => {
     const selected = storageDistribution.data.dataNodeUsage.find(datanode => 
datanode.hostName === selectedDatanode)
       ?? storageDistribution.data.dataNodeUsage[0];
     const dnPendingEntry = 
dnPendingDeletes.data.pendingDeletionPerDataNode?.find(
@@ -150,7 +162,10 @@ const Capacity: React.FC<object> = () => {
         reserved: 0
       }),
       ...dnPendingEntry,
-      pendingBlockSize: Math.max(0, dnPendingEntry.pendingBlockSize)
+      pendingBlockSize: Math.max(0, dnPendingEntry.pendingBlockSize),
+      // -1 is the sentinel for a DN whose pending deletion query failed 
(offline/unreachable).
+      // Its capacity data from the storage report may be outdated, so surface 
an error instead.
+      unavailable: dnPendingEntry.pendingBlockSize === -1
     }
   }, [selectedDatanode, storageDistribution.data.dataNodeUsage, 
dnPendingDeletes.data.pendingDeletionPerDataNode]);
 
@@ -491,6 +506,7 @@ const Capacity: React.FC<object> = () => {
             downloadUrl={DN_CSV_DOWNLOAD_URL}
             onDownloadClick={() => downloadCsv(DN_CSV_DOWNLOAD_URL)}
             handleSelect={setSelectedDatanode}
+            selectedValue={selectedDatanode}
             dropdownItems={filteredDNs.map(datanode => ({
               label: (
                 <>
@@ -510,6 +526,9 @@ const Capacity: React.FC<object> = () => {
             dataDetails={[{
               title: 'USED SPACE',
               size: (selectedDNDetails.used ?? 0) + 
(selectedDNDetails.pendingBlockSize ?? 0),
+              hasError: selectedDNDetails.unavailable,
+              errorMessage: 'Datanode is unavailable; capacity data may be 
outdated.',
+              errorTestId: 'dn-used-space-error',
               breakdown: [{
                 label: 'PENDING DELETION',
                 value: selectedDNDetails.pendingBlockSize ?? 0,
@@ -522,6 +541,9 @@ const Capacity: React.FC<object> = () => {
             }, {
               title: 'FREE SPACE',
               size: (selectedDNDetails.remaining ?? 0) + 
(selectedDNDetails.committed ?? 0),
+              hasError: selectedDNDetails.unavailable,
+              errorMessage: 'Datanode is unavailable; capacity data may be 
outdated.',
+              errorTestId: 'dn-free-space-error',
               breakdown: [{
                 label: unusedSpaceBreakdown,
                 value: selectedDNDetails.remaining ?? 0,
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 f17c52b6004..3445c6e8bcb 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
@@ -50,6 +50,7 @@ type CapacityDetailProps = {
   disabledOpts?: string[];
   optsClass?: string;
   handleSelect?: React.Dispatch<React.SetStateAction<string>>
+  selectedValue?: string;
   loading: boolean;
   extra?: React.ReactNode;
 };
@@ -111,6 +112,7 @@ const CapacityDetail: React.FC<CapacityDetailProps> = (
     optsClass,
     dataDetails,
     handleSelect,
+    selectedValue,
     loading,
     extra
   }
@@ -149,7 +151,7 @@ const CapacityDetail: React.FC<CapacityDetailProps> = (
                 {selectorTitle}
                 <Select
                   showSearch
-                  defaultValue={options?.[0]?.value}
+                  value={selectedValue ?? options?.[0]?.value}
                   options={options}
                   onChange={handleSelect}
                   style={{ marginBottom: '16px' }}


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

Reply via email to