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..775b390a5edf 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,15 @@ "quotaInNamespace": 50000, "owner": "testuser", "bucketLayout": "OBJECT_STORE", + "replicationConfigInfo": { + "type": "RATIS", + "replicationConfig": { + "replicationType": "RATIS", + "replicationFactor": "THREE", + "requiredNodes": 3, + "minimumNodes": 1 + } + }, "acls": [ { "type": "USER", @@ -4669,6 +4678,18 @@ "quotaInNamespace": 10000, "owner": "testuser2", "bucketLayout": "LEGACY", + "replicationConfigInfo": { + "type": "EC", + "replicationConfig": { + "replicationType": "EC", + "codec": "RS", + "data": 6, + "parity": 3, + "ecChunkSize": 1048576, + "requiredNodes": 9, + "minimumNodes": 6 + } + }, "acls": [ { "type": "GROUP", @@ -4737,6 +4758,15 @@ "quotaInNamespace": -1, "owner": "testuser3", "bucketLayout": "OBJECT_STORE", + "replicationConfigInfo": { + "type": "STAND_ALONE", + "replicationConfig": { + "replicationType": "STANDALONE", + "replicationFactor": "ONE", + "requiredNodes": 1, + "minimumNodes": 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..179ed66321e8 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/buckets/BucketsTable.test.tsx @@ -0,0 +1,182 @@ +/* + * 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.each([ + ['RS', 6, 3, 1048576, 'rs-6-3-1024k'], + ['RS', 3, 2, 1048576, 'rs-3-2-1024k'], + ['RS', 10, 4, 1048576, 'rs-10-4-1024k'], + ['XOR', 10, 4, 2097152, 'xor-10-4-2048k'] + ] as const)( + 'renders an EC bucket with codec %s, %i data and %i parity and a %i byte chunk as %s', + (codec, data, parity, ecChunkSize, expected) => { + render( + + ); + + expect(screen.getByText(expected)).toBeInTheDocument(); + }); + + test('renders the Standalone variant for a single replica bucket', () => { + render( + + ); + + expect(screen.getByText('Standalone-1')).toBeInTheDocument(); + }); + + test('renders the Standalone variant for the STAND_ALONE enum spelling', () => { + 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..203009ed6d1f 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,30 @@ function renderBucketLayout(bucketLayout: BucketLayout) { return {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', + STANDALONE: 'Standalone' +}; + +// Formats the bucket's default replication configuration for display +function formatReplicationType(replicationConfigInfo?: BucketReplicationConfig | null) { + const replicationConfig = replicationConfigInfo?.replicationConfig; + if (replicationConfig?.replicationType === 'EC') { + const { codec, data, parity, ecChunkSize } = replicationConfig; + return `${codec.toLowerCase()}-${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 +152,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/components/tables/insights/openKeysTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx index c7fc676bcd8c..999406f9a146 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx @@ -145,9 +145,9 @@ const OpenKeysTable: React.FC = ({ render: (replicationInfo: ReplicationInfo) => (
{ - (replicationInfo.replicationType === "RATIS") - ? replicationInfo.replicationFactor - : `${replicationInfo.codec}-${replicationInfo.data}-${replicationInfo.parity}` + (replicationInfo.replicationType === "EC") + ? `${replicationInfo.codec}-${replicationInfo.data}-${replicationInfo.parity}` + : replicationInfo.replicationFactor }
) 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..98d033d1b1c2 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 @@ -17,6 +17,7 @@ */ import { Acl } from "@/v2/types/acl.types"; +import { ReplicationInfo } from '@/v2/types/insights.types'; import { Option as MultiOption } from "@/v2/components/select/multiSelect"; // Corresponds to OzoneManagerProtocolProtos.StorageTypeProto @@ -37,6 +38,14 @@ export const BucketLayoutTypeList = [ export type BucketLayout = typeof BucketLayoutTypeList[number]; +// Corresponds to the serialized org.apache.hadoop.hdds.client.DefaultReplicationConfig +// returned by the Recon bucket endpoint (BucketObjectDBInfo#replicationConfigInfo). +// The nested config is the same shape the OM DB insights endpoints return. +export type BucketReplicationConfig = { + type: string; + replicationConfig?: ReplicationInfo | null; +} + export type Bucket = { volumeName: string; name: string; @@ -53,6 +62,7 @@ export type Bucket = { owner: string; acls?: Acl[]; bucketLayout: BucketLayout; + replicationConfigInfo?: BucketReplicationConfig | null; } export type BucketResponse = { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/insights.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/insights.types.ts index 55f2bc7a0324..7b2333b40270 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/insights.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/insights.types.ts @@ -119,7 +119,16 @@ export interface EcInfo { minimumNodes: number; } -export type ReplicationInfo = RatisInfo | EcInfo; +// StandaloneReplicationConfig serializes its replicationType as STANDALONE, +// without the underscore used by the ReplicationType enum name STAND_ALONE +export interface StandaloneInfo { + replicationType: 'STANDALONE' | 'STAND_ALONE'; + replicationFactor: string; + requiredNodes: number; + minimumNodes: number; +} + +export type ReplicationInfo = RatisInfo | EcInfo | StandaloneInfo; // Open Keys export type OpenKeys = {