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 8cfb23ad685b..f1d5dc367033 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 000000000000..d28212cca25f --- /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 = ({ + path = '/', + subPaths = [], + updateHandler +}) => { + const [currPath, setCurrPath] = useState([]); + + 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 ( + + {splitSubpath[splitSubpath.length - 1]} + + ); + } + + }); + //Push a new input to allow passing a path + menuItems.push( + + { + //Prevent menu close on click + e.stopPropagation(); + }} + style={{ width: 160}}/> + + ) + return ( + }> + {menuItems} + + } + dropdownProps={{ + trigger: ['click'] + }}> + {(lastPath === '/') ? : lastPath} + + ) + } + + React.useEffect(() => { + generateCurrentPathState() + }, [path]); //Anytime the path changes we need to generate the state + + function generateBreadCrumbs(){ + let breadCrumbs = []; + currPath.forEach((location, idx) => { + breadCrumbs.push( + + {(location === '/') + ? {handleBreadcrumbClick(idx, location)}} + style={{color: '#1aa57a'}} /> + : ()} + + ); + }); + breadCrumbs[breadCrumbs.length - 1] = generateSubMenu(currPath[currPath.length - 1]); + return breadCrumbs; + } + + return ( + } + className='breadcrumb-nav'> + {generateBreadCrumbs()} + + ) +} + +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 000000000000..f2c740f7dbcb --- /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; + 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 = ({ + path = '/' +}) => { + const [loading, setLoading] = useState(false); + const [state, setState] = useState({ + keys: [], + values: [] + }); + const cancelSummarySignal = useRef(); + const keyMetadataSummarySignal = useRef(); + const cancelQuotaSignal = useRef(); + + 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 = { + 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 ( + + + +
+ ); +} + +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 000000000000..2601905a142e --- /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 = ({ + path, + limit, + size, + subPaths, + subPathCount, + sizeWithReplica, + loading +}) => { + + const [subpathSize, setSubpathSize] = React.useState(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 = `${name}
`; + const dataEl = `Total Data Size: ${pieData[dataIndex]['size']}
` + 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}) => { + 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 ( + + ); +} + +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 79fa07603386..9d483efd6b00 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(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 000000000000..dca3861df138 --- /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 000000000000..1e92780619b1 --- /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(false); + const [limit, setLimit] = useState