From d7dd8e4bb68383ec6b5ffb53aa26dc3f5182496c Mon Sep 17 00:00:00 2001 From: stantheman0128 Date: Fri, 7 Aug 2026 05:56:21 +0800 Subject: [PATCH 1/2] HDDS-13625. Recon UI: Add "Replication Type" col on Bucket page Show the default replication of each bucket on the Recon Bucket list page. The bucket endpoint already returns this as BucketObjectDBInfo#replicationConfigInfo, so no backend change is needed. The column renders the replication strings Ozone uses elsewhere: Ratis-3 for Ratis buckets, RS-6-3-1024k for EC buckets, Standalone-1 for single replica buckets, and NA when a bucket carries no default replication config. Sorting follows the rendered string. Three of the five mock buckets in api/db.json gain a replicationConfigInfo so the column can be exercised locally with pnpm dev. The column is plain text rather than the themeIcon component named in the JIRA description, because the change that generalizes themeIcon (HDDS-13623) has not been merged. The icon can be added on top later. --- .../webapps/recon/ozone-recon-web/api/db.json | 27 ++++ .../__tests__/buckets/BucketsTable.test.tsx | 153 ++++++++++++++++++ .../src/v2/components/tables/bucketsTable.tsx | 31 ++++ .../src/v2/pages/buckets/buckets.tsx | 3 +- .../src/v2/types/bucket.types.ts | 30 ++++ 5 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx 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 e2d796e417c5..49ea6b4fdf0e 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 @@ -4643,6 +4643,14 @@ "quotaInNamespace": 50000, "owner": "testuser", "bucketLayout": "OBJECT_STORE", + "replicationConfigInfo": { + "type": "RATIS", + "replicationConfig": { + "replicationType": "RATIS", + "replicationFactor": "THREE", + "requiredNodes": 3 + } + }, "acls": [ { "type": "USER", @@ -4669,6 +4677,17 @@ "quotaInNamespace": 10000, "owner": "testuser2", "bucketLayout": "LEGACY", + "replicationConfigInfo": { + "type": "EC", + "replicationConfig": { + "replicationType": "EC", + "codec": "RS", + "data": 6, + "parity": 3, + "ecChunkSize": 1048576, + "requiredNodes": 9 + } + }, "acls": [ { "type": "GROUP", @@ -4737,6 +4756,14 @@ "quotaInNamespace": -1, "owner": "testuser3", "bucketLayout": "OBJECT_STORE", + "replicationConfigInfo": { + "type": "STAND_ALONE", + "replicationConfig": { + "replicationType": "STAND_ALONE", + "replicationFactor": "ONE", + "requiredNodes": 1 + } + }, "acls": [ { "type": "GROUP", diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx new file mode 100644 index 000000000000..5713845754e5 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx @@ -0,0 +1,153 @@ +/* + * 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 { vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +import BucketsTable from '@/v2/components/tables/bucketsTable'; +import { Bucket, BucketsTableProps } from '@/v2/types/bucket.types'; + +function getBucketWith( + name: string, + replicationConfigInfo: Bucket['replicationConfigInfo'] +): Bucket { + return { + volumeName: 'vol1', + name, + versioning: false, + storageType: 'DISK', + creationTime: 1728280581608, + modificationTime: 1728280581608, + usedBytes: 0, + usedNamespace: 0, + quotaInBytes: -1, + quotaInNamespace: -1, + owner: 'om', + acls: [], + bucketLayout: 'FILE_SYSTEM_OPTIMIZED', + replicationConfigInfo + }; +} + +const defaultProps: BucketsTableProps = { + loading: false, + data: [], + handleAclClick: vi.fn(), + searchColumn: 'name', + searchTerm: '', + selectedColumns: [ + { label: 'Bucket', + value: 'name' }, + { label: 'Volume', + value: 'volumeName' }, + { label: 'Replication Type', + value: 'replicationType' } + ] +}; + +describe('BucketsTable Replication Type column', () => { + test('renders the Ratis variant for a Ratis bucket', () => { + render( + + ); + + expect(screen.getByText('Ratis-3')).toBeInTheDocument(); + }); + + test('renders the EC variant for an Erasure Coded bucket', () => { + render( + + ); + + expect(screen.getByText('RS-6-3-1024k')).toBeInTheDocument(); + }); + + test('renders the Standalone variant for a single replica bucket', () => { + render( + + ); + + expect(screen.getByText('Standalone-1')).toBeInTheDocument(); + }); + + test('falls back to the replication type when the nested config is absent', () => { + render( + + ); + + expect(screen.getByText('RATIS')).toBeInTheDocument(); + }); + + test('falls back to NA when replicationConfigInfo is missing', () => { + render( + + ); + + expect(screen.getByText('NA')).toBeInTheDocument(); + }); + + test('falls back to NA when replicationConfigInfo is null', () => { + render( + + ); + + expect(screen.getByText('NA')).toBeInTheDocument(); + }); +}); diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx index a9013967e3eb..cd5cc5d8befc 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx @@ -41,6 +41,7 @@ import { Bucket, BucketLayout, BucketLayoutTypeList, + BucketReplicationConfig, BucketsTableProps, BucketStorage, BucketStorageTypeList @@ -78,6 +79,27 @@ function renderBucketLayout(bucketLayout: BucketLayout) { return {bucketLayout}; }; +const REPLICATION_TYPE_LABELS: Record = { + RATIS: 'Ratis', + STAND_ALONE: 'Standalone' +}; + +// Mirrors the replication strings Ozone uses elsewhere, e.g. Ratis-3 and RS-6-3-1024k +function formatReplicationType(replicationConfigInfo?: BucketReplicationConfig | null) { + const replicationConfig = replicationConfigInfo?.replicationConfig; + if (replicationConfig?.replicationType === 'EC') { + const { codec, data, parity, ecChunkSize } = replicationConfig; + return `${codec}-${data}-${parity}-${Math.floor(ecChunkSize / 1024)}k`; + } + if (replicationConfig) { + const label = REPLICATION_TYPE_LABELS[replicationConfig.replicationType] + ?? replicationConfig.replicationType; + return `${label}-${replicationConfig.requiredNodes}`; + } + // Fall back to the bare type, then to NA for buckets with no default replication config + return replicationConfigInfo?.type ?? 'NA'; +}; + export const COLUMNS: ColumnsType = [ { title: 'Bucket', @@ -127,6 +149,15 @@ export const COLUMNS: ColumnsType = [ sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout), render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout) }, + { + title: 'Replication Type', + dataIndex: 'replicationConfigInfo', + key: 'replicationType', + sorter: (a: Bucket, b: Bucket) => formatReplicationType(a.replicationConfigInfo) + .localeCompare(formatReplicationType(b.replicationConfigInfo)), + render: (replicationConfigInfo: BucketReplicationConfig | null | undefined) => + formatReplicationType(replicationConfigInfo) + }, { title: 'Creation Time', dataIndex: 'creationTime', diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx index ae3376df480d..4b8abb31eee1 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx @@ -177,7 +177,8 @@ const Buckets: React.FC<{}> = () => { quotaInBytes: bucket.quotaInBytes, quotaInNamespace: bucket.quotaInNamespace, owner: bucket.owner, - acls: bucket.acls + acls: bucket.acls, + replicationConfigInfo: bucket.replicationConfigInfo })); const volumeBucketMap: Map> = getVolumeBucketMap(buckets); diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts index eb499dc617e7..afec733dff5f 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts @@ -37,6 +37,35 @@ export const BucketLayoutTypeList = [ export type BucketLayout = typeof BucketLayoutTypeList[number]; +// Corresponds to the serialized org.apache.hadoop.hdds.client.RatisReplicationConfig +// and StandaloneReplicationConfig +type BucketRatisReplicationConfig = { + replicationType: 'RATIS' | 'STAND_ALONE'; + replicationFactor: string; + requiredNodes: number; +} + +// Corresponds to the serialized org.apache.hadoop.hdds.client.ECReplicationConfig +type BucketECReplicationConfig = { + replicationType: 'EC'; + codec: string; + data: number; + parity: number; + ecChunkSize: number; + requiredNodes: number; +} + +type BucketReplicationInfo = + | BucketRatisReplicationConfig + | BucketECReplicationConfig; + +// Corresponds to the serialized org.apache.hadoop.hdds.client.DefaultReplicationConfig +// returned by the Recon bucket endpoint (BucketObjectDBInfo#replicationConfigInfo) +export type BucketReplicationConfig = { + type: string; + replicationConfig?: BucketReplicationInfo | null; +} + export type Bucket = { volumeName: string; name: string; @@ -53,6 +82,7 @@ export type Bucket = { owner: string; acls?: Acl[]; bucketLayout: BucketLayout; + replicationConfigInfo?: BucketReplicationConfig | null; } export type BucketResponse = { From 5fdf8ab77840b243c820a0e0f345054144decdea Mon Sep 17 00:00:00 2001 From: stantheman0128 Date: Sat, 8 Aug 2026 03:17:03 +0800 Subject: [PATCH 2/2] HDDS-13625. Handle the STANDALONE spelling of replicationType StandaloneReplicationConfig serializes replicationType as STANDALONE, without the underscore, while the ReplicationType enum name used elsewhere is STAND_ALONE. A Standalone bucket therefore reached the column as STANDALONE and rendered as STANDALONE-1 instead of Standalone-1. Map both spellings, widen the type accordingly, and cover each spelling with its own test. The mock bucket in api/db.json now uses the spelling the backend actually emits. --- .../webapps/recon/ozone-recon-web/api/db.json | 2 +- .../__tests__/buckets/BucketsTable.test.tsx | 18 ++++++++++++++++++ .../src/v2/components/tables/bucketsTable.tsx | 5 ++++- .../src/v2/types/bucket.types.ts | 6 ++++-- 4 files changed, 27 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 49ea6b4fdf0e..6d374abc7ef5 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 @@ -4759,7 +4759,7 @@ "replicationConfigInfo": { "type": "STAND_ALONE", "replicationConfig": { - "replicationType": "STAND_ALONE", + "replicationType": "STANDALONE", "replicationFactor": "ONE", "requiredNodes": 1 } diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx index 5713845754e5..b1723d433aaf 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx @@ -105,6 +105,24 @@ describe('BucketsTable Replication Type column', () => { + ); + + expect(screen.getByText('Standalone-1')).toBeInTheDocument(); + }); + + test('renders the Standalone variant for the STAND_ALONE enum spelling', () => { + render( + {bucketLayout}; }; +// StandaloneReplicationConfig serializes replicationType as STANDALONE, while the +// ReplicationType enum name is STAND_ALONE, so both spellings are mapped here const REPLICATION_TYPE_LABELS: Record = { RATIS: 'Ratis', - STAND_ALONE: 'Standalone' + STAND_ALONE: 'Standalone', + STANDALONE: 'Standalone' }; // Mirrors the replication strings Ozone uses elsewhere, e.g. Ratis-3 and RS-6-3-1024k diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts index afec733dff5f..e6bd383483af 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts @@ -38,9 +38,11 @@ export type BucketLayout = typeof BucketLayoutTypeList[number]; // Corresponds to the serialized org.apache.hadoop.hdds.client.RatisReplicationConfig -// and StandaloneReplicationConfig +// and StandaloneReplicationConfig. The latter serializes its replicationType as +// STANDALONE, while the enum name used elsewhere is STAND_ALONE, so both spellings +// can reach the UI. type BucketRatisReplicationConfig = { - replicationType: 'RATIS' | 'STAND_ALONE'; + replicationType: 'RATIS' | 'STAND_ALONE' | 'STANDALONE'; replicationFactor: string; requiredNodes: number; }