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 ce46297e85 HDDS-11162. Improve Disk Usage page UI (#7214)
ce46297e85 is described below
commit ce46297e85b490a29f933b171fcada2dbebba3b0
Author: Abhishek Pal <[email protected]>
AuthorDate: Tue Oct 1 11:26:04 2024 +0530
HDDS-11162. Improve Disk Usage page UI (#7214)
---
.../webapps/recon/ozone-recon-web/api/db.json | 2 +-
.../components/duBreadcrumbNav/duBreadcrumbNav.tsx | 173 +++++++++
.../src/v2/components/duMetadata/duMetadata.tsx | 389 +++++++++++++++++++++
.../src/v2/components/duPieChart/duPieChart.tsx | 211 +++++++++++
.../src/v2/components/eChart/eChart.tsx | 15 +-
.../src/v2/pages/diskUsage/diskUsage.less | 59 ++++
.../src/v2/pages/diskUsage/diskUsage.tsx | 144 ++++++++
.../recon/ozone-recon-web/src/v2/routes-v2.tsx | 5 +
.../v2/{routes-v2.tsx => types/diskUsage.types.ts} | 50 ++-
9 files changed, 1018 insertions(+), 30 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 8cfb23ad68..f1d5dc3670 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
@@ -1480,7 +1480,7 @@
"path": "/dummyVolume/dummyBucket",
"size": 200000,
"sizeWithReplica": -1,
- "subPathCount": 5,
+ "subPathCount": 8,
"subPaths": [
{
"path": "/dummyVolume/dummyBucket/dir1",
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duBreadcrumbNav/duBreadcrumbNav.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duBreadcrumbNav/duBreadcrumbNav.tsx
new file mode 100644
index 0000000000..d28212cca2
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duBreadcrumbNav/duBreadcrumbNav.tsx
@@ -0,0 +1,173 @@
+/*
+ * 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 React, { useState } from 'react';
+
+import { DUSubpath } from '@/v2/types/diskUsage.types';
+import { Breadcrumb, Menu, Input } from 'antd';
+import { MenuProps } from 'antd/es/menu';
+import { CaretDownOutlined, CaretRightOutlined, HomeFilled } from
'@ant-design/icons';
+
+
+type File = {
+ path: string;
+ subPaths: DUSubpath[];
+ updateHandler: (arg0: string) => void;
+};
+
+
+const DUBreadcrumbNav: React.FC<File> = ({
+ path = '/',
+ subPaths = [],
+ updateHandler
+}) => {
+ const [currPath, setCurrPath] = useState<string[]>([]);
+
+ function generateCurrentPathState() {
+ // We are not at root path
+ if (path !== '/') {
+ /**
+ * Remove leading / and split to avoid empty string
+ * Without the substring this will produce ['', 'pathLoc'] for /pathLoc
+ */
+ const splitPath = path.substring(1).split('/');
+ setCurrPath(
+ ['/', ...splitPath]
+ );
+ }
+ else {
+ setCurrPath(['/']);
+ }
+ }
+
+ const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
+ //If click is not on search panel
+ if (!(key as string).includes('-search')){
+ updateHandler(key as string);
+ }
+ }
+
+ function handleSearch(value: string) {
+ /**
+ * The following will generate path like //vol1/buck1/dir1/key...
+ * since the first element of the currPos is ['/']
+ * we are joining that with / as well causing //
+ * Hence we substring from 1st index to remove one / from path
+ */
+ updateHandler([...currPath, value].join('/').substring(1));
+ }
+
+ function handleBreadcrumbClick(idx: number, lastPath: string) {
+ /**
+ * The following will generate path like //vol1/buck1/dir1/key...
+ * since the first element of the currPos is ['/']
+ * we are joining that with / as well causing //
+ */
+ const constructedPath = [...currPath.slice(0, idx), lastPath].join('/');
+ if (idx === 0) {
+ //Root path clicked
+ updateHandler('/');
+ }
+ else {
+ // Pass the string without the leading /
+ updateHandler(constructedPath.substring(1));
+ }
+ }
+
+ function generateSubMenu(lastPath: string) {
+ const menuItems = subPaths.map((subpath) => {
+ // Do not add any menu item for keys i.e keys cannot be drilled down
+ // further
+ if (!subpath.isKey) {
+ const splitSubpath = subpath.path.split('/');
+ return (
+ <Menu.Item key={subpath.path}>
+ {splitSubpath[splitSubpath.length - 1]}
+ </Menu.Item>
+ );
+ }
+
+ });
+ //Push a new input to allow passing a path
+ menuItems.push(
+ <Menu.Item
+ key={`${lastPath}-search`}>
+ <Input.Search
+ placeholder='Enter Path'
+ onSearch={handleSearch}
+ onClick={(e) => {
+ //Prevent menu close on click
+ e.stopPropagation();
+ }}
+ style={{ width: 160}}/>
+ </Menu.Item>
+ )
+ return (
+ <Breadcrumb.Item key={lastPath}
+ overlay={
+ <Menu
+ onClick={handleMenuClick}
+ mode='inline'
+ expandIcon={<CaretDownOutlined/>}>
+ {menuItems}
+ </Menu>
+ }
+ dropdownProps={{
+ trigger: ['click']
+ }}>
+ {(lastPath === '/') ? <HomeFilled style={{fontSize: '16px'}}/> :
lastPath}
+ </Breadcrumb.Item>
+ )
+ }
+
+ React.useEffect(() => {
+ generateCurrentPathState()
+ }, [path]); //Anytime the path changes we need to generate the state
+
+ function generateBreadCrumbs(){
+ let breadCrumbs = [];
+ currPath.forEach((location, idx) => {
+ breadCrumbs.push(
+ <Breadcrumb.Item
+ key={location}>
+ {(location === '/')
+ ? <HomeFilled
+ onClick={() => {handleBreadcrumbClick(idx, location)}}
+ style={{color: '#1aa57a'}} />
+ : (<button
+ className='breadcrumb-nav-item'
+ onClick={() => {handleBreadcrumbClick(idx, location)}}>
+ {location}
+ </button>)}
+ </Breadcrumb.Item>
+ );
+ });
+ breadCrumbs[breadCrumbs.length - 1] =
generateSubMenu(currPath[currPath.length - 1]);
+ return breadCrumbs;
+ }
+
+ return (
+ <Breadcrumb
+ separator={<CaretRightOutlined style={{ fontSize: '12px'}}/>}
+ className='breadcrumb-nav'>
+ {generateBreadCrumbs()}
+ </Breadcrumb>
+ )
+}
+
+export default DUBreadcrumbNav;
\ 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
new file mode 100644
index 0000000000..f2c740f7db
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duMetadata/duMetadata.tsx
@@ -0,0 +1,389 @@
+/*
+ * 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 React, { useRef, useState } from 'react';
+import moment from 'moment';
+import { AxiosError } from 'axios';
+import { Table } from 'antd';
+
+import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
+import { byteToSize, showDataFetchError } from '@/utils/common';
+
+import { Acl } from '@/v2/types/acl.types';
+
+
+// ------------- Types -------------- //
+type CountStats = {
+ numBucket: number;
+ numDir: number;
+ numKey: number;
+ numVolume: number;
+};
+
+type LocationInfo = {
+ blockID: {
+ containerBlockID: {
+ containerID: number;
+ localID: number;
+ };
+ blockCommitSequenceId: number;
+ containerID: number;
+ localID: number;
+ };
+ length: number;
+ offset: number;
+ token: null;
+ createVersion: number;
+ pipeline: null;
+ partNumber: number;
+ containerID: number;
+ localID: number;
+ blockCommitSequenceId: number;
+};
+
+type ObjectInfo = {
+ bucketName: string;
+ bucketLayout: string;
+ encInfo: null;
+ fileName: string;
+ keyName: string;
+ name: string;
+ owner: string;
+ volume: string;
+ volumeName: string;
+ sourceVolume: string | null;
+ sourceBucket: string | null;
+ usedBytes: number | null;
+ usedNamespace: number;
+ storageType: string;
+ creationTime: number;
+ dataSize: number;
+ modificationTime: number;
+ quotaInBytes: number;
+ quotaInNamespace: number;
+}
+
+type ReplicationConfig = {
+ replicationFactor: string;
+ requiredNodes: number;
+ replicationType: string;
+}
+
+type ObjectInfoResponse = ObjectInfo & {
+ acls: Acl[];
+ versioningEnabled: boolean;
+ metadata: Record<string, any>;
+ file: boolean;
+ keyLocationVersions: {
+ version: number;
+ locationList: LocationInfo[];
+ multipartKey: boolean;
+ blocksLatestVersionOnly: LocationInfo[];
+ locationLists: LocationInfo[][];
+ locationListCount: number;
+ }[];
+ versioning: boolean;
+ encryptionInfo: null;
+ replicationConfig: ReplicationConfig;
+};
+
+type SummaryResponse = {
+ countStats: CountStats;
+ objectInfo: ObjectInfoResponse;
+ path: string;
+ status: string;
+ type: string;
+}
+
+type MetadataProps = {
+ path: string;
+};
+
+type MetadataState = {
+ keys: string[];
+ values: (string | number | boolean | null)[];
+};
+
+
+// ------------- Component -------------- //
+const DUMetadata: React.FC<MetadataProps> = ({
+ path = '/'
+}) => {
+ const [loading, setLoading] = useState<boolean>(false);
+ const [state, setState] = useState<MetadataState>({
+ keys: [],
+ values: []
+ });
+ const cancelSummarySignal = useRef<AbortController>();
+ const keyMetadataSummarySignal = useRef<AbortController>();
+ const cancelQuotaSignal = useRef<AbortController>();
+
+ const getObjectInfoMapping = React.useCallback((summaryResponse) => {
+
+ const keys: string[] = [];
+ const values: (string | number | boolean | null)[] = [];
+ /**
+ * We are creating a specific set of keys under Object Info response
+ * which do not require us to modify anything
+ */
+ const selectedInfoKeys = [
+ 'bucketName', 'bucketLayout', 'encInfo', 'fileName', 'keyName',
+ 'name', 'owner', 'sourceBucket', 'sourceVolume', 'storageType',
+ 'usedNamespace', 'volumeName', 'volume'
+ ] as const;
+ const objectInfo: ObjectInfo = summaryResponse.objectInfo ?? {};
+
+ selectedInfoKeys.forEach((key) => {
+ if (objectInfo[key as keyof ObjectInfo] !== undefined && objectInfo[key
as keyof ObjectInfo] !== -1) {
+ // We will use regex to convert the Object key from camel case to
space separated title
+ // 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]);
+ }
+ });
+
+ if (objectInfo?.creationTime !== undefined && objectInfo?.creationTime !==
-1) {
+ keys.push('Creation Time');
+ values.push(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));
+ }
+
+ if (objectInfo?.dataSize !== undefined && objectInfo?.dataSize !== -1) {
+ keys.push('Data Size');
+ values.push(byteToSize(objectInfo.dataSize, 3));
+ }
+
+ if (objectInfo?.modificationTime !== undefined &&
objectInfo?.modificationTime !== -1) {
+ keys.push('Modification Time');
+ values.push(moment(objectInfo.modificationTime).format('ll LTS'));
+ }
+
+ if (objectInfo?.quotaInBytes !== undefined && objectInfo?.quotaInBytes !==
-1) {
+ keys.push('Quota In Bytes');
+ values.push(byteToSize(objectInfo.quotaInBytes, 3));
+ }
+
+ if (objectInfo?.quotaInNamespace !== undefined &&
objectInfo?.quotaInNamespace !== -1) {
+ keys.push('Quota In Namespace');
+ values.push(byteToSize(objectInfo.quotaInNamespace, 3));
+ }
+
+ if (summaryResponse.objectInfo?.replicationConfig?.replicationFactor !==
undefined) {
+ keys.push('Replication Factor');
+
values.push(summaryResponse.objectInfo.replicationConfig.replicationFactor);
+ }
+
+ if (summaryResponse.objectInfo?.replicationConfig?.replicationType !==
undefined) {
+ keys.push('Replication Type');
+
values.push(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);
+ }
+
+ return { keys, values }
+ }, [path]);
+
+ function loadMetadataSummary(path: string) {
+ cancelRequests([
+ cancelSummarySignal.current!,
+ keyMetadataSummarySignal.current!
+ ]);
+ const keys: string[] = [];
+ const values: (string | number | boolean | null)[] = [];
+
+ const { request, controller } = AxiosGetHelper(
+ `/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);
+
+ if (summaryResponse.status === 'INITIALIZING') {
+ showDataFetchError(`The metadata is currently initializing. Please
wait a moment and try again later`);
+ return;
+ }
+
+ if (summaryResponse.status === 'PATH_NOT_FOUND') {
+ 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());
+ });
+ 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]);
+ }
+ })
+
+ 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;
+
+ if (quotaResponse.status === 'INITIALIZING') {
+ return;
+ }
+ if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
+ return;
+ }
+ if (quotaResponse.status === 'PATH_NOT_FOUND') {
+ showDataFetchError(`Invalid Path: ${path}`);
+ return;
+ }
+
+ 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 (quotaResponse.used !== undefined && quotaResponse.used !== -1) {
+ keys.push('Quota Used');
+ values.push(byteToSize(quotaResponse.used, 3));
+ }
+ setState((prevState) => ({
+ keys: [...prevState.keys, ...keys],
+ values: [...prevState.values, ...values]
+ }));
+ }).catch(error => {
+ showDataFetchError(error.toString());
+ });
+ }
+
+ React.useEffect(() => {
+ setLoading(true);
+ loadMetadataSummary(path);
+ loadQuotaSummary(path);
+ setLoading(false);
+
+ return (() => {
+ cancelRequests([
+ cancelSummarySignal.current!,
+ keyMetadataSummarySignal.current!,
+ cancelQuotaSignal.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}
+ bordered={true}
+ style={{
+ flex: '0 1 45%',
+ margin: '10px auto' }}
+ locale={{ filterTitle: '' }}>
+ <Table.Column title='Property' dataIndex='key' />
+ <Table.Column title='Value' dataIndex='value' />
+ </Table>
+ );
+}
+
+export default DUMetadata;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duPieChart/duPieChart.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duPieChart/duPieChart.tsx
new file mode 100644
index 0000000000..2601905a14
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/duPieChart/duPieChart.tsx
@@ -0,0 +1,211 @@
+/*
+ * 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 React from 'react';
+
+import EChart from '@/v2/components/eChart/eChart';
+import { byteToSize } from '@/utils/common';
+import { DUSubpath } from '@/v2/types/diskUsage.types';
+
+//-------Types--------//
+type PieChartProps = {
+ path: string;
+ limit: number;
+ size: number;
+ subPaths: DUSubpath[];
+ subPathCount: number;
+ sizeWithReplica: number;
+ loading: boolean;
+}
+
+//-------Constants---------//
+const OTHER_PATH_NAME = 'Other Objects';
+const MIN_BLOCK_SIZE = 0.05;
+
+
+//----------Component---------//
+const DUPieChart: React.FC<PieChartProps> = ({
+ path,
+ limit,
+ size,
+ subPaths,
+ subPathCount,
+ sizeWithReplica,
+ loading
+}) => {
+
+ const [subpathSize, setSubpathSize] = React.useState<number>(0);
+
+ function getSubpathSize(subpaths: DUSubpath[]): number {
+ const subpathSize = subpaths
+ .map((subpath) => subpath.size)
+ .reduce((acc, curr) => acc + curr, 0);
+ // If there is no subpaths, then the size will be total size of path
+ return (subPaths.length === 0) ? size : subpathSize;
+ }
+
+ function updatePieData() {
+ /**
+ * We need to calculate the size of "Other objects" in two cases:
+ *
+ * 1) If we have more subpaths listed, than the limit.
+ * 2) If the limit is set to the maximum limit (30) and we have any
number of subpaths.
+ * In this case we won't necessarily have "Other objects", but we
check if the
+ * other objects's size is more than zero (we will have other objects
if there are more than 30 subpaths,
+ * but we can't check on that, as the response will always have
+ * 30 subpaths, but from the total size and the subpaths size we can
calculate it).
+ */
+ let subpaths: DUSubpath[] = subPaths;
+
+ let pathLabels: string[] = [];
+ let percentage: string[] = [];
+ let sizeStr: string[];
+ let valuesWithMinBlockSize: number[] = [];
+
+ if (subPathCount > limit) {
+ // If the subpath count is greater than the provided limit
+ // Slice the subpath to the limit
+ subpaths = subpaths.slice(0, limit);
+ // Add the size of the subpath
+ const limitedSize = getSubpathSize(subpaths);
+ const remainingSize = size - limitedSize;
+ subpaths.push({
+ path: OTHER_PATH_NAME,
+ size: remainingSize,
+ sizeWithReplica: (sizeWithReplica === -1)
+ ? -1
+ : sizeWithReplica - remainingSize,
+ isKey: false
+ })
+ }
+
+ if (subPathCount === 0 || subpaths.length === 0) {
+ // No more subpaths available
+ pathLabels = [path.split('/').pop() ?? ''];
+ valuesWithMinBlockSize = [0.1];
+ percentage = ['100.00'];
+ sizeStr = [byteToSize(size, 1)];
+ } else {
+ pathLabels = subpaths.map(subpath => {
+ const subpathName = subpath.path.split('/').pop() ?? '';
+ // Diferentiate keys by removing trailing slash
+ return (subpath.isKey || subpathName === OTHER_PATH_NAME)
+ ? subpathName
+ : subpathName + '/';
+ });
+
+ let values: number[] = [0];
+ if (size > 0) {
+ values = subpaths.map(
+ subpath => (subpath.size / size)
+ );
+ }
+ const valueClone = structuredClone(values);
+ valuesWithMinBlockSize = valueClone?.map(
+ (val: number) => (val > 0)
+ ? val + MIN_BLOCK_SIZE
+ : val
+ );
+
+ percentage = values.map(value => (value * 100).toFixed(2));
+ sizeStr = subpaths.map((subpath) => byteToSize(subpath.size, 1));
+ }
+
+ return valuesWithMinBlockSize.map((key, idx) => {
+ return {
+ value: key,
+ name: pathLabels[idx],
+ size: sizeStr[idx],
+ percentage: percentage[idx]
+ }
+ });
+ }
+
+ React.useEffect(() => {
+ setSubpathSize(getSubpathSize(subPaths));
+ }, [subPaths, limit]);
+
+ const pieData = React.useMemo(() => updatePieData(), [path, subPaths,
limit]);
+
+ const eChartsOptions = {
+ title: {
+ text: `${byteToSize(subpathSize, 1)} / ${byteToSize(size, 1)}`,
+ left: 'center',
+ top: '95%'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: ({ dataIndex, name, color }) => {
+ const nameEl = `<strong style='color: ${color}'>${name}</strong><br>`;
+ const dataEl = `Total Data Size: ${pieData[dataIndex]['size']}<br>`
+ const percentageEl = `Percentage: ${pieData[dataIndex]['percentage']}
%`
+ return `${nameEl}${dataEl}${percentageEl}`
+ }
+ },
+ legend: {
+ top: '10%',
+ orient: 'vertical',
+ left: '0%',
+ width: '80%'
+ },
+ grid: {
+
+ },
+ series: [
+ {
+ type: 'pie',
+ radius: '70%',
+ data: pieData.map((value) => {
+ return {
+ value: value.value,
+ name: value.name
+ }
+ }),
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+ };
+
+ const handleLegendChange = ({selected}: {selected: Record<string, boolean>})
=> {
+ const filteredPath = subPaths.filter((value) => {
+ // In case of any leading '/' remove them and add a / at end
+ // to make it similar to legend
+ const splitPath = value.path?.split('/');
+ const pathName = splitPath[splitPath.length - 1] ?? '' + ((value.isKey)
? '' : '/');
+ return selected[pathName];
+ })
+ const newSize = getSubpathSize(filteredPath);
+ setSubpathSize(newSize);
+ }
+
+ return (
+ <EChart
+ loading={loading}
+ option={eChartsOptions}
+ style={{ flex: '0 1 90%', height: '50vh' }}
+ eventHandler={{name: 'legendselectchanged', handler:
handleLegendChange}}/>
+ );
+}
+
+export default DUPieChart;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
index 79fa076033..9d483efd6b 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
@@ -28,6 +28,10 @@ export interface EChartProps {
loading?: boolean;
theme?: 'light';
onClick?: () => any | void;
+ eventHandler?: {
+ name: string,
+ handler: (arg0: any) => void
+ };
}
const EChart = ({
@@ -36,7 +40,8 @@ const EChart = ({
settings,
loading,
theme,
- onClick
+ onClick,
+ eventHandler
}: EChartProps): JSX.Element => {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -47,6 +52,10 @@ const EChart = ({
if (onClick) {
chart.on('click', onClick);
}
+
+ if (eventHandler) {
+ chart.on(eventHandler.name, eventHandler.handler);
+ }
}
// Add chart resize listener
@@ -71,6 +80,10 @@ const EChart = ({
if (onClick) {
chart!.on('click', onClick);
}
+
+ if (eventHandler) {
+ chart!.on(eventHandler.name, eventHandler.handler);
+ }
}
}, [option, settings, theme]); // Whenever theme changes we need to add
option and setting due to it being deleted in cleanup function
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.less
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.less
new file mode 100644
index 0000000000..dca3861df1
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.less
@@ -0,0 +1,59 @@
+/*
+* 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.
+*/
+
+.du-alert-message {
+ background-color: #FFFFFF;
+ border: unset;
+ box-shadow: -1px -2px 28px -14px rgba(0,0,0,0.68);
+ -webkit-box-shadow: -1px -2px 28px -14px rgba(0,0,0,0.68);
+ -moz-box-shadow: -1px -2px 28px -14px rgba(0,0,0,0.68);
+ margin-bottom: 20px;
+}
+
+.content-div {
+ min-height: unset;
+
+ .breadcrumb-nav {
+ font-size: 0.9vw;
+ margin: 0px 0px 8px 8px;
+ .breadcrumb-nav-item {
+ background: transparent;
+ border: none !important;
+ cursor: pointer;
+ color: @primary-color;
+
+ }
+ }
+
+ .du-table-header-section {
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ }
+
+ .du-content {
+ width: 100%;
+ display: flex;
+ }
+
+ .tag-block {
+ display: flex;
+ column-gap: 8px;
+ padding: 0px 8px 16px 8px;
+ }
+}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.tsx
new file mode 100644
index 0000000000..1e92780619
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/diskUsage/diskUsage.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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 React, { useRef, useState } from 'react';
+import { AxiosError } from 'axios';
+import {
+ Alert
+} from 'antd';
+import {
+ InfoCircleFilled
+} from '@ant-design/icons';
+import { ValueType } from 'react-select';
+
+import DUMetadata from '@/v2/components/duMetadata/duMetadata';
+import DUPieChart from '@/v2/components/duPieChart/duPieChart';
+import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+import DUBreadcrumbNav from '@/v2/components/duBreadcrumbNav/duBreadcrumbNav';
+import { showDataFetchError } from '@/utils/common';
+import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
+
+import { DUResponse } from '@/v2/types/diskUsage.types';
+
+import './diskUsage.less';
+
+const LIMIT_OPTIONS: Option[] = [
+ { label: '5', value: '5' },
+ { label: '10', value: '10' },
+ { label: '15', value: '15' },
+ { label: '20', value: '20' },
+ { label: '30', value: '30' }
+]
+
+const DiskUsage: React.FC<{}> = () => {
+ const [loading, setLoading] = useState<boolean>(false);
+ const [limit, setLimit] = useState<Option>(LIMIT_OPTIONS[1]);
+ const [duResponse, setDUResponse] = useState<DUResponse>({
+ status: '',
+ path: '/',
+ subPathCount: 0,
+ size: 0,
+ sizeWithReplica: 0,
+ subPaths: [],
+ sizeDirectKey: 0
+ });
+
+ const cancelPieSignal = useRef<AbortController>();
+
+ function loadData(path: string) {
+ setLoading(true);
+ const { request, controller } = AxiosGetHelper(
+ `/api/v1/namespace/du?path=${path}&files=true&sortSubPaths=true`,
+ cancelPieSignal.current
+ );
+ cancelPieSignal.current = controller;
+
+ request.then(response => {
+ const duResponse: DUResponse = response.data;
+ const status = duResponse.status;
+ if (status === 'PATH_NOT_FOUND') {
+ setLoading(false);
+ showDataFetchError(`Invalid Path: ${path}`);
+ return;
+ }
+
+ setDUResponse(duResponse);
+ setLoading(false);
+ }).catch(error => {
+ setLoading(false);
+ showDataFetchError((error as AxiosError).toString());
+ });
+ }
+
+ function handleLimitChange(selected: ValueType<Option, false>) {
+ setLimit(selected as Option);
+ }
+
+ React.useEffect(() => {
+ //On mount load default data
+ loadData(duResponse.path)
+
+ return (() => {
+ cancelRequests([cancelPieSignal.current!]);
+ })
+ }, []);
+
+ return (
+ <>
+ <div className='page-header-v2'>
+ Disk Usage
+ </div>
+ <div style={{ padding: '24px' }}>
+ <Alert
+ className='du-alert-message'
+ message="Additional block size is added to small entities, for
better visibility.
+ Please refer to pie-chart details for exact size information."
+ type="info"
+ icon={<InfoCircleFilled />}
+ showIcon={true}
+ closable={false} />
+ <div className='content-div'>
+ <DUBreadcrumbNav
+ path={duResponse.path}
+ subPaths={duResponse.subPaths}
+ updateHandler={loadData} />
+ <div className='du-table-header-section'>
+ <SingleSelect
+ options={LIMIT_OPTIONS}
+ defaultValue={limit}
+ placeholder='Limit'
+ onChange={handleLimitChange} />
+ </div>
+ <div className='du-content'>
+ <DUPieChart
+ loading={loading}
+ limit={Number.parseInt(limit.value)}
+ path={duResponse.path}
+ subPathCount={duResponse.subPathCount}
+ subPaths={duResponse.subPaths}
+ sizeWithReplica={duResponse.sizeWithReplica}
+ size={duResponse.size} />
+ <DUMetadata path={duResponse.path} />
+ </div>
+ </div>
+ </div>
+ </>
+ );
+}
+
+export default DiskUsage;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
index 0aea9e8040..20907fd3ad 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
@@ -22,6 +22,7 @@ const Volumes = lazy(() =>
import('@/v2/pages/volumes/volumes'))
const Buckets = lazy(() => import('@/v2/pages/buckets/buckets'));
const Datanodes = lazy(() => import('@/v2/pages/datanodes/datanodes'));
const Pipelines = lazy(() => import('@/v2/pages/pipelines/pipelines'));
+const DiskUsage = lazy(() => import('@/v2/pages/diskUsage/diskUsage'));
export const routesV2 = [
{
@@ -43,5 +44,9 @@ export const routesV2 = [
{
path: '/Pipelines',
component: Pipelines
+ },
+ {
+ path: '/DiskUsage',
+ component: DiskUsage
}
];
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/diskUsage.types.ts
similarity index 54%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/diskUsage.types.ts
index 0aea9e8040..e649c143ae 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/diskUsage.types.ts
@@ -15,33 +15,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { lazy } from 'react';
-const Overview = lazy(() => import('@/v2/pages/overview/overview'));
-const Volumes = lazy(() => import('@/v2/pages/volumes/volumes'))
-const Buckets = lazy(() => import('@/v2/pages/buckets/buckets'));
-const Datanodes = lazy(() => import('@/v2/pages/datanodes/datanodes'));
-const Pipelines = lazy(() => import('@/v2/pages/pipelines/pipelines'));
+export type DUSubpath = {
+ path: string;
+ size: number;
+ sizeWithReplica: number;
+ isKey: boolean;
+}
-export const routesV2 = [
- {
- path: '/Overview',
- component: Overview
- },
- {
- path: '/Volumes',
- component: Volumes
- },
- {
- path: '/Buckets',
- component: Buckets
- },
- {
- path: '/Datanodes',
- component: Datanodes
- },
- {
- path: '/Pipelines',
- component: Pipelines
- }
-];
+export type DUResponse = {
+ status: string;
+ path: string;
+ subPathCount: number;
+ size: number;
+ sizeWithReplica: number;
+ subPaths: DUSubpath[];
+ sizeDirectKey: number;
+}
+
+export type PlotData = {
+ value: number;
+ name: string;
+ size: string;
+ percentage: string;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]