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]