This is an automated email from the ASF dual-hosted git repository.
arafat2198 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 c006c572c5 HDDS-12016. Fixed duplicate entries when changing path in
DU page (#7657)
c006c572c5 is described below
commit c006c572c546a3c60b7e87947d2fc9ea4dffd7e4
Author: Abhishek Pal <[email protected]>
AuthorDate: Sun Jan 19 13:34:17 2025 +0530
HDDS-12016. Fixed duplicate entries when changing path in DU page (#7657)
---
.../recon/ozone-recon-web/src/utils/common.tsx | 29 ++
.../src/v2/components/duMetadata/duMetadata.tsx | 316 ++++++++++-----------
.../src/v2/pages/overview/overview.tsx | 25 +-
3 files changed, 181 insertions(+), 189 deletions(-)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
index f641b8797d..9b1f9e09ea 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
@@ -18,6 +18,7 @@
import moment from 'moment';
import { notification } from 'antd';
+import { CanceledError } from 'axios';
export const getCapacityPercent = (used: number, total: number) =>
Math.round((used / total) * 100);
@@ -80,3 +81,31 @@ export const nullAwareLocaleCompare = (a: string, b: string)
=> {
return a.localeCompare(b);
};
+
+export function removeDuplicatesAndMerge<T>(origArr: T[], updateArr: T[],
mergeKey: string): T[] {
+ return Array.from([...origArr, ...updateArr].reduce(
+ (accumulator, curr) => accumulator.set(curr[mergeKey as keyof T], curr),
+ new Map
+ ).values());
+}
+
+export const checkResponseError = (responses: Awaited<Promise<any>>[]) => {
+ const responseError = responses.filter(
+ (resp) => resp.status === 'rejected'
+ );
+
+ if (responseError.length !== 0) {
+ responseError.forEach((err) => {
+ if (err.reason.toString().includes("CanceledError")) {
+ throw new CanceledError('canceled', "ERR_CANCELED");
+ }
+ else {
+ const reqMethod = err.reason.config.method;
+ const reqURL = err.reason.config.url
+ showDataFetchError(
+ `Failed to ${reqMethod} URL ${reqURL}\n${err.reason.toString()}`
+ );
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duMetadata/duMetadata.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duMetadata/duMetadata.tsx
index e46282f185..5cae2fbc87 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duMetadata/duMetadata.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duMetadata/duMetadata.tsx
@@ -18,11 +18,11 @@
import React, { useRef, useState } from 'react';
import moment from 'moment';
-import { AxiosError } from 'axios';
+import axios, { AxiosError } from 'axios';
import { Table } from 'antd';
-import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
-import { byteToSize, showDataFetchError } from '@/utils/common';
+import { AxiosGetHelper, cancelRequests, PromiseAllSettledGetHelper } from
'@/utils/axiosRequestHelper';
+import { byteToSize, checkResponseError, removeDuplicatesAndMerge,
showDataFetchError } from '@/utils/common';
import { Acl } from '@/v2/types/acl.types';
@@ -115,9 +115,9 @@ type MetadataProps = {
};
type MetadataState = {
- keys: string[];
- values: (string | number | boolean | null)[];
-};
+ key: string,
+ value: string | number | boolean | null
+}[];
// ------------- Component -------------- //
@@ -125,18 +125,12 @@ const DUMetadata: React.FC<MetadataProps> = ({
path = '/'
}) => {
const [loading, setLoading] = useState<boolean>(false);
- const [state, setState] = useState<MetadataState>({
- keys: [],
- values: []
- });
- const cancelSummarySignal = useRef<AbortController>();
+ const [state, setState] = useState<MetadataState>([]);
const keyMetadataSummarySignal = useRef<AbortController>();
- const cancelQuotaSignal = useRef<AbortController>();
+ const cancelMetadataSignal = useRef<AbortController>();
const getObjectInfoMapping = React.useCallback((summaryResponse) => {
-
- const keys: string[] = [];
- const values: (string | number | boolean | null)[] = [];
+ const data: MetadataState = [];
/**
* We are creating a specific set of keys under Object Info response
* which do not require us to modify anything
@@ -154,226 +148,216 @@ const DUMetadata: React.FC<MetadataProps> = ({
// The following regex will match abcDef and produce Abc Def
let keyName = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
keyName = keyName.charAt(0).toUpperCase() + keyName.slice(1);
- keys.push(keyName);
- values.push(objectInfo[key as keyof ObjectInfo]);
+ data.push({
+ key: keyName as string,
+ value: objectInfo[key as keyof ObjectInfo]
+ });
}
});
if (objectInfo?.creationTime !== undefined && objectInfo?.creationTime !==
-1) {
- keys.push('Creation Time');
- values.push(moment(objectInfo.creationTime).format('ll LTS'));
+ data.push({
+ key: 'Creation Time',
+ value: moment(objectInfo.creationTime).format('ll LTS')
+ });
}
if (objectInfo?.usedBytes !== undefined && objectInfo?.usedBytes !== -1 &&
objectInfo!.usedBytes !== null) {
- keys.push('Used Bytes');
- values.push(byteToSize(objectInfo.usedBytes, 3));
+ data.push({
+ key: 'Used Bytes',
+ value: byteToSize(objectInfo.usedBytes, 3)
+ });
}
if (objectInfo?.dataSize !== undefined && objectInfo?.dataSize !== -1) {
- keys.push('Data Size');
- values.push(byteToSize(objectInfo.dataSize, 3));
+ data.push({
+ key: 'Data Size',
+ value: byteToSize(objectInfo.dataSize, 3)
+ });
}
if (objectInfo?.modificationTime !== undefined &&
objectInfo?.modificationTime !== -1) {
- keys.push('Modification Time');
- values.push(moment(objectInfo.modificationTime).format('ll LTS'));
+ data.push({
+ key: 'Modification Time',
+ value: moment(objectInfo.modificationTime).format('ll LTS')
+ });
}
if (objectInfo?.quotaInNamespace !== undefined &&
objectInfo?.quotaInNamespace !== -1) {
- keys.push('Quota In Namespace');
- values.push(byteToSize(objectInfo.quotaInNamespace, 3));
+ data.push({
+ key: 'Quota In Namespace',
+ value: byteToSize(objectInfo.quotaInNamespace, 3)
+ });
}
if (summaryResponse.objectInfo?.replicationConfig?.replicationFactor !==
undefined) {
- keys.push('Replication Factor');
-
values.push(summaryResponse.objectInfo.replicationConfig.replicationFactor);
+ data.push({
+ key: 'Replication Factor',
+ value: summaryResponse.objectInfo.replicationConfig.replicationFactor
+ });
}
if (summaryResponse.objectInfo?.replicationConfig?.replicationType !==
undefined) {
- keys.push('Replication Type');
-
values.push(summaryResponse.objectInfo.replicationConfig.replicationType);
+ data.push({
+ key: 'Replication Type',
+ value: summaryResponse.objectInfo.replicationConfig.replicationType
+ });
}
if (summaryResponse.objectInfo?.replicationConfig?.requiredNodes !==
undefined
&& summaryResponse.objectInfo?.replicationConfig?.requiredNodes !== -1) {
- keys.push('Replication Required Nodes');
- values.push(summaryResponse.objectInfo.replicationConfig.requiredNodes);
+ data.push({
+ key: 'Replication Required Nodes',
+ value: summaryResponse.objectInfo.replicationConfig.requiredNodes
+ });
}
- return { keys, values }
+ return data;
}, [path]);
- function loadMetadataSummary(path: string) {
- cancelRequests([
- cancelSummarySignal.current!,
- keyMetadataSummarySignal.current!
- ]);
- const keys: string[] = [];
- const values: (string | number | boolean | null)[] = [];
-
- const { request, controller } = AxiosGetHelper(
+ function loadData(path: string) {
+ const { requests, controller } = PromiseAllSettledGetHelper([
`/api/v1/namespace/summary?path=${path}`,
- cancelSummarySignal.current
- );
- cancelSummarySignal.current = controller;
-
- request.then(response => {
- const summaryResponse: SummaryResponse = response.data;
- keys.push('Entity Type');
- values.push(summaryResponse.type);
-
+ `/api/v1/namespace/quota?path=${path}`
+ ], cancelMetadataSignal.current);
+ cancelMetadataSignal.current = controller;
+
+ requests.then(axios.spread((
+ nsSummaryResponse: Awaited<Promise<any>>,
+ quotaApiResponse: Awaited<Promise<any>>,
+ ) => {
+ checkResponseError([nsSummaryResponse, quotaApiResponse]);
+ const summaryResponse: SummaryResponse = nsSummaryResponse.value?.data
?? {};
+ const quotaResponse = quotaApiResponse.value?.data ?? {};
+ let data: MetadataState = [];
+ let summaryResponsePresent = true;
+ let quotaResponsePresent = true;
+
+ // Error checks
if (summaryResponse.status === 'INITIALIZING') {
+ summaryResponsePresent = false;
showDataFetchError(`The metadata is currently initializing. Please
wait a moment and try again later`);
- return;
}
- if (summaryResponse.status === 'PATH_NOT_FOUND') {
+ if (summaryResponse.status === 'PATH_NOT_FOUND' || quotaResponse.status
=== 'PATH_NOT_FOUND') {
+ summaryResponsePresent = false;
+ quotaResponsePresent = false;
showDataFetchError(`Invalid Path: ${path}`);
- return;
}
- // If the entity is a Key then fetch the Key metadata only
- if (summaryResponse.type === 'KEY') {
- const { request: metadataRequest, controller: metadataNewController }
= AxiosGetHelper(
- `/api/v1/namespace/du?path=${path}&replica=true`,
- keyMetadataSummarySignal.current
- );
- keyMetadataSummarySignal.current = metadataNewController;
- metadataRequest.then(response => {
- keys.push('File Size');
- values.push(byteToSize(response.data.size, 3));
- keys.push('File Size With Replication');
- values.push(byteToSize(response.data.sizeWithReplica, 3));
- keys.push("Creation Time");
-
values.push(moment(summaryResponse.objectInfo.creationTime).format('ll LTS'));
- keys.push("Modification Time");
-
values.push(moment(summaryResponse.objectInfo.modificationTime).format('ll
LTS'));
-
- setState({
- keys: keys,
- values: values
- });
- }).catch(error => {
- showDataFetchError(error.toString());
+ if (summaryResponsePresent) {
+ // Summary Response data section
+ data.push({
+ key: 'Entity Type',
+ value: summaryResponse.type
});
- return;
- }
- /**
- * Will iterate over the keys of the countStats to avoid multiple if
blocks
- * and check from the map for the respective key name / title to insert
- */
- const countStats: CountStats = summaryResponse.countStats ?? {};
- const keyToNameMap: Record<string, string> = {
- numVolume: 'Volumes',
- numBucket: 'Buckets',
- numDir: 'Total Directories',
- numKey: 'Total Keys'
- }
- Object.keys(countStats).forEach((key: string) => {
- if (countStats[key as keyof CountStats] !== undefined
- && countStats[key as keyof CountStats] !== -1) {
- keys.push(keyToNameMap[key]);
- values.push(countStats[key as keyof CountStats]);
+ // If the entity is a Key then fetch the Key metadata only
+ if (summaryResponse.type === 'KEY') {
+ const { request: metadataRequest, controller: metadataNewController
} = AxiosGetHelper(
+ `/api/v1/namespace/du?path=${path}&replica=true`,
+ keyMetadataSummarySignal.current
+ );
+ keyMetadataSummarySignal.current = metadataNewController;
+ metadataRequest.then(response => {
+ data.push(...[{
+ key: 'File Size',
+ value: byteToSize(response.data.size, 3)
+ }, {
+ key: 'File Size With Replication',
+ value: byteToSize(response.data.sizeWithReplica, 3)
+ }, {
+ key: 'Creation Time',
+ value:
moment(summaryResponse.objectInfo.creationTime).format('ll LTS')
+ }, {
+ key: 'Modification Time',
+ value:
moment(summaryResponse.objectInfo.modificationTime).format('ll LTS')
+ }])
+ setState(data);
+ }).catch(error => {
+ showDataFetchError(error.toString());
+ });
+ return;
}
- })
-
- const {
- keys: objectInfoKeys,
- values: objectInfoValues
- } = getObjectInfoMapping(summaryResponse);
-
- keys.push(...objectInfoKeys);
- values.push(...objectInfoValues);
- setState({
- keys: keys,
- values: values
- });
- }).catch(error => {
- showDataFetchError((error as AxiosError).toString());
- });
- }
-
- function loadQuotaSummary(path: string) {
- cancelRequests([
- cancelQuotaSignal.current!
- ]);
-
- const { request, controller } = AxiosGetHelper(
- `/api/v1/namespace/quota?path=${path}`,
- cancelQuotaSignal.current
- );
- cancelQuotaSignal.current = controller;
-
- request.then(response => {
- const quotaResponse = response.data;
+ data = removeDuplicatesAndMerge(data,
getObjectInfoMapping(summaryResponse), 'key');
+
+ /**
+ * Will iterate over the keys of the countStats to avoid multiple if
blocks
+ * and check from the map for the respective key name / title to insert
+ */
+ const countStats: CountStats = summaryResponse.countStats ?? {};
+ const keyToNameMap: Record<string, string> = {
+ numVolume: 'Volumes',
+ numBucket: 'Buckets',
+ numDir: 'Total Directories',
+ numKey: 'Total Keys'
+ }
+ Object.keys(countStats).forEach((key: string) => {
+ if (countStats[key as keyof CountStats] !== undefined
+ && countStats[key as keyof CountStats] !== -1) {
+ data.push({
+ key: keyToNameMap[key],
+ value: countStats[key as keyof CountStats]
+ });
+ }
+ })
+ }
- if (quotaResponse.status === 'INITIALIZING') {
- return;
+ if (quotaResponse.state === 'INITIALIZING') {
+ quotaResponsePresent = false;
+ showDataFetchError(`The quota is currently initializing. Please wait a
moment and try again later`);
}
+
if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
- return;
- }
- if (quotaResponse.status === 'PATH_NOT_FOUND') {
- showDataFetchError(`Invalid Path: ${path}`);
- return;
+ quotaResponsePresent = false;
}
- const keys: string[] = [];
- const values: (string | number | boolean | null)[] = [];
- // Append quota information
- // In case the object's quota isn't set
- if (quotaResponse.allowed !== undefined && quotaResponse.allowed !== -1)
{
- keys.push('Quota Allowed');
- values.push(byteToSize(quotaResponse.allowed, 3));
- }
+ if (quotaResponsePresent) {
+ // Quota Response section
+ // In case the object's quota isn't set, we should not populate the
values
+ if (quotaResponse.allowed !== undefined && quotaResponse.allowed !==
-1) {
+ data.push({
+ key: 'Quota Allowed',
+ value: byteToSize(quotaResponse.allowed, 3)
+ });
+ }
- if (quotaResponse.used !== undefined && quotaResponse.used !== -1) {
- keys.push('Quota Used');
- values.push(byteToSize(quotaResponse.used, 3));
+ if (quotaResponse.used !== undefined && quotaResponse.used !== -1) {
+ data.push({
+ key: 'Quota Used',
+ value: byteToSize(quotaResponse.used, 3)
+ })
+ }
}
- setState((prevState) => ({
- keys: [...prevState.keys, ...keys],
- values: [...prevState.values, ...values]
- }));
- }).catch(error => {
- showDataFetchError(error.toString());
+ setState(data);
+ })).catch(error => {
+ showDataFetchError((error as AxiosError).toString());
});
}
React.useEffect(() => {
setLoading(true);
- loadMetadataSummary(path);
- loadQuotaSummary(path);
+ loadData(path);
setLoading(false);
return (() => {
cancelRequests([
- cancelSummarySignal.current!,
- keyMetadataSummarySignal.current!,
- cancelQuotaSignal.current!
+ cancelMetadataSignal.current!,
]);
})
}, [path]);
- const content = [];
- for (const [i, v] of state.keys.entries()) {
- content.push({
- key: v,
- value: state.values[i]
- });
- }
-
return (
<Table
size='small'
loading={loading}
- dataSource={content}
+ dataSource={state}
bordered={true}
style={{
flex: '0 1 45%',
- margin: '10px auto' }}
+ margin: '10px auto'
+ }}
locale={{ filterTitle: '' }}>
<Table.Column title='Property' dataIndex='key' />
<Table.Column title='Value' dataIndex='value' />
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
index e14f134a0e..6014577f90 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx
@@ -19,7 +19,7 @@
import React, { useEffect, useRef, useState } from 'react';
import moment from 'moment';
import filesize from 'filesize';
-import axios, { CanceledError } from 'axios';
+import axios from 'axios';
import { Row, Col, Button } from 'antd';
import {
CheckCircleFilled,
@@ -33,7 +33,7 @@ import OverviewStorageCard from
'@/v2/components/overviewCard/overviewStorageCar
import OverviewSimpleCard from
'@/v2/components/overviewCard/overviewSimpleCard';
import { AutoReloadHelper } from '@/utils/autoReloadHelper';
-import { showDataFetchError } from '@/utils/common';
+import { checkResponseError, showDataFetchError } from '@/utils/common';
import { AxiosGetHelper, cancelRequests, PromiseAllSettledGetHelper } from
'@/utils/axiosRequestHelper';
import { ClusterStateResponse, OverviewState, StorageReport } from
'@/v2/types/overview.types';
@@ -73,27 +73,6 @@ const getHealthIcon = (value: string): React.ReactElement =>
{
)
}
-const checkResponseError = (responses: Awaited<Promise<any>>[]) => {
- const responseError = responses.filter(
- (resp) => resp.status === 'rejected'
- );
-
- if (responseError.length !== 0) {
- responseError.forEach((err) => {
- if (err.reason.toString().includes("CanceledError")) {
- throw new CanceledError('canceled', "ERR_CANCELED");
- }
- else {
- const reqMethod = err.reason.config.method;
- const reqURL = err.reason.config.url
- showDataFetchError(
- `Failed to ${reqMethod} URL ${reqURL}\n${err.reason.toString()}`
- );
- }
- })
- }
-}
-
const getSummaryTableValue = (
value: number | string | undefined,
colType: 'value' | undefined = undefined
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]