You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@ozone.apache.org by "devabhishekpal (via GitHub)" <gi...@apache.org> on 2024/08/20 13:33:23 UTC
[PR] HDDS-11156. Improve Buckets page UI [ozone]
devabhishekpal opened a new pull request, #7100:
URL: https://github.com/apache/ozone/pull/7100
## What changes were proposed in this pull request?
This PR will add a new Buckets page which contains a better implementation for the UI
Please describe your PR in detail:
* This PR adds a new Volumes Page with improved layout and design.
* The new page would help to eventually improve the overall look and feel for Recon
* Currently the page is disabled to the user so as to not affect any existing functionality
* The PR also adds a minor refactoring to shift logic for unselectable/always enabled column to MultiSelect component
## What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-11156
## How was this patch tested?
Patch was tested manually.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "ArafatKhan2198 (via GitHub)" <gi...@apache.org>.
ArafatKhan2198 merged PR #7100:
URL: https://github.com/apache/ozone/pull/7100
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "devabhishekpal (via GitHub)" <gi...@apache.org>.
devabhishekpal commented on PR #7100:
URL: https://github.com/apache/ozone/pull/7100#issuecomment-2324503173
Thanks @devmadhuu for the comment.
Fixed the first issue.
For the second one, yes for versioning there will be two icons.
`X` for versioning disabled, and `✓` for enabled.
Check the vol2/buck2 row for the reference on the Versioning Enabled option in below screenshot.
Ref screenshot: <img src="https://github-production-user-asset-6210df.s3.amazonaws.com/43001336/359532254-cb171ce5-1dfd-4679-bf75-f35539a6bd0e.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20240902%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240902T112504Z&X-Amz-Expires=300&X-Amz-Signature=02928b5c781ba354a3a8f30019adb99f01453d4cc522140ab32dfd7724c1d479&X-Amz-SignedHeaders=host&actor_id=43001336&key_id=0&repo_id=212382406"/>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "devabhishekpal (via GitHub)" <gi...@apache.org>.
devabhishekpal commented on code in PR #7100:
URL: https://github.com/apache/ozone/pull/7100#discussion_r1727684945
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
+import moment from 'moment';
+import { Table, Tag } from 'antd';
+import {
+ ColumnProps,
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ CloudServerOutlined,
+ FileUnknownOutlined,
+ HddOutlined,
+ LaptopOutlined,
+ SaveOutlined
+} from '@ant-design/icons';
+import { ValueType } from 'react-select';
+import { useLocation } from 'react-router-dom';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
+import Search from '@/v2/components/search/search';
+import MultiSelect from '@/v2/components/select/multiSelect';
+import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
+import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Bucket,
+ BucketLayout,
+ BucketLayoutTypeList,
+ BucketResponse,
+ BucketsState,
+ BucketStorage,
+ BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+import './buckets.less';
+
+
+const LIMIT_OPTIONS: Option[] = [
+ {
+ label: '1000',
+ value: '1000'
+ },
+ {
+ label: '5000',
+ value: '5000'
+ },
+ {
+ label: '10000',
+ value: '10000'
+ },
+ {
+ label: '20000',
+ value: '20000'
+ }
+]
+
+const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
+ return isVersionEnabled
+ ? <CheckCircleOutlined
+ style={{ color: '#1da57a' }}
+ className='icon-success' />
+ : <CloseCircleOutlined className='icon-neutral' />
+};
+
+const renderStorageType = (bucketStorage: BucketStorage) => {
+ const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+ RAM_DISK: <LaptopOutlined />,
+ SSD: <SaveOutlined />,
+ DISK: <HddOutlined />,
+ ARCHIVE: <CloudServerOutlined />
+ };
+ const icon = bucketStorage in bucketStorageIconMap
+ ? bucketStorageIconMap[bucketStorage]
+ : <FileUnknownOutlined />;
+ return <span>{icon} {bucketStorage}</span>;
+};
+
+const renderBucketLayout = (bucketLayout: BucketLayout) => {
+ const bucketLayoutColorMap = {
+ FILE_SYSTEM_OPTIMIZED: 'green',
+ OBJECT_STORE: 'blue',
+ LEGACY: 'gray'
+ };
+ const color = bucketLayout in bucketLayoutColorMap ?
+ bucketLayoutColorMap[bucketLayout] : '';
+ return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+const SearchableColumnOpts = [{
+ label: 'Bucket',
+ value: 'name'
+}, {
+ label: 'Volume',
+ value: 'volumeName'
+}]
+
+const COLUMNS: ColumnsType<Bucket> = [
+ {
+ title: 'Bucket',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Volume',
+ dataIndex: 'volumeName',
+ key: 'volumeName',
+ sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+ },
+ {
+ title: 'Versioning',
+ dataIndex: 'versioning',
+ key: 'isVersionEnabled',
+ render: (isVersionEnabled: boolean) => renderIsVersionEnabled(isVersionEnabled)
+ },
+ {
+ title: 'Storage Type',
+ dataIndex: 'storageType',
+ key: 'storageType',
+ filterMultiple: true,
+ filters: BucketStorageTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.storageType === value,
+ sorter: (a: Bucket, b: Bucket) => a.storageType.localeCompare(b.storageType),
+ render: (storageType: BucketStorage) => renderStorageType(storageType)
+ },
+ {
+ title: 'Bucket Layout',
+ dataIndex: 'bucketLayout',
+ key: 'bucketLayout',
+ filterMultiple: true,
+ filters: BucketLayoutTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.bucketLayout === value,
+ sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout),
+ render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Storage Capacity',
+ key: 'quotaCapacityBytes',
+ sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInBytes}
+ used={record.usedBytes}
+ quotaType='size'
+ />
+ )
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+ {
+ title: 'Source Volume',
+ dataIndex: 'sourceVolume',
+ key: 'sourceVolume',
+ render: (sourceVolume: string) => {
+ return sourceVolume ? sourceVolume : 'NA';
+ }
+ },
+ {
+ title: 'Source Bucket',
+ dataIndex: 'sourceBucket',
+ key: 'sourceBucket',
+ render: (sourceBucket: string) => {
+ return sourceBucket ? sourceBucket : 'NA';
+ }
+ }
+];
+
+const defaultColumns = COLUMNS.map(column => ({
+ label: column.title as string,
+ value: column.key as string
+}));
+
+function getVolumeBucketMap(data: Bucket[]) {
+ const volumeBucketMap = data.reduce((
+ map: Map<string, Set<Bucket>>,
+ currentBucket
+ ) => {
+ const volume = currentBucket.volumeName;
+ if (map.has(volume)) {
+ const buckets = Array.from(map.get(volume)!);
+ map.set(volume, new Set([...buckets, currentBucket]));
+ } else {
+ map.set(volume, new Set<Bucket>().add(currentBucket));
+ }
+ return map;
+ }, new Map<string, Set<Bucket>>());
+ return volumeBucketMap;
+}
+
+function getFilteredBuckets(
+ selectedVolumes: Option[],
+ bucketsMap: Map<string, Set<Bucket>>
+) {
+ let selectedBuckets: Bucket[] = [];
+ selectedVolumes.forEach(selectedVolume => {
+ if (bucketsMap.has(selectedVolume.value)
+ && bucketsMap.get(selectedVolume.value)) {
+ selectedBuckets = [
+ ...selectedBuckets,
+ ...Array.from(bucketsMap.get(selectedVolume.value)!)
+ ];
+ }
+ });
+
+ return selectedBuckets;
+}
+
+const Buckets: React.FC<{}> = () => {
+
+ let cancelSignal: AbortController;
+
+ const [state, setState] = useState<BucketsState>({
+ totalCount: 0,
+ lastUpdated: 0,
+ columnOptions: defaultColumns,
+ volumeBucketMap: new Map<string, Set<Bucket>>(),
+ bucketsUnderVolume: [],
+ volumeOptions: [],
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] = useState<Option[]>(defaultColumns);
+ const [selectedVolumes, setSelectedVolumes] = useState<Option[]>([]);
+ const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
+ const [searchTerm, setSearchTerm] = useState<string>('');
+ const [showPanel, setShowPanel] = useState<boolean>(false);
+ const [searchColumn, setSearchColumn] = useState<'name' | 'volumeName'>('name');
+ const [currentRow, setCurrentRow] = useState<Bucket | Record<string, never>>({})
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+ const { search } = useLocation();
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} buckets`,
+ showSizeChanger: true
+ };
+
+ function getVolumeSearchParam() {
+ return new URLSearchParams(search).get('volume');
+ };
+
+ function getFilteredData(data: Bucket[]) {
+ return data.filter(
+ (bucket: Bucket) => bucket[searchColumn].includes(debouncedSearch)
+ );
+ }
+
+ function handleVolumeChange(selected: ValueType<Option, true>) {
+ const { volumeBucketMap } = state;
+ const volumeSelections = (selected as Option[]);
+ let selectedBuckets: Bucket[] = [];
+
+ if (volumeSelections?.length > 0) {
+ selectedBuckets = getFilteredBuckets(volumeSelections, volumeBucketMap)
+ }
+
+ setSelectedVolumes(volumeSelections);
+ setState({
+ ...state,
+ bucketsUnderVolume: selectedBuckets
+ });
+ };
+
+ function handleAclLinkClick(bucket: Bucket) {
+ setCurrentRow(bucket);
+ setShowPanel(true);
+ }
+
+ function filterSelectedColumns() {
+ const columnKeys = selectedColumns.map((column) => column.value);
+ return COLUMNS.filter(
+ (column) => columnKeys.indexOf(column.key as string) >= 0
+ )
+ }
+
+ function addAclColumn() {
+ // Inside the class component to access the React internal state
+ const aclLinkColumn: ColumnProps<Bucket> = {
+ title: 'ACLs',
+ dataIndex: 'acls',
+ key: 'acls',
+ render: (_: any, record: Bucket) => {
+ return (
+ <a
+ key='acl'
+ onClick={() => {
+ handleAclLinkClick(record);
+ }}
+ >
+ Show ACL
+ </a>
+ );
+ }
+ };
+
+ if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
+ // Push the ACL column for initial
+ COLUMNS.push(aclLinkColumn);
+ } else {
+ // Replace old ACL column with new ACL column with correct reference
+ // e.g. After page is reloaded / redirect from other page
+ COLUMNS[COLUMNS.length - 1] = aclLinkColumn;
+ }
+
+ if (defaultColumns.length > 0 && defaultColumns[defaultColumns.length - 1].label !== 'acls') {
+ defaultColumns.push({
+ label: aclLinkColumn.title as string,
+ value: aclLinkColumn.key as string
+ });
+ }
+ };
+
+ function handleColumnChange(selected: ValueType<Option, true>) {
+ setSelectedColumns(selected as Option[]);
+ }
+
+ function handleLimitChange(selected: ValueType<Option, false>) {
+ setSelectedLimit(selected as Option);
+ }
+
+ const loadData = () => {
+ setLoading(true);
+ const { request, controller } = AxiosGetHelper(
+ '/api/v1/buckets',
+ cancelSignal,
+ '',
+ { limit: selectedLimit.value }
+ );
+ cancelSignal = controller;
+ request.then(response => {
+ const bucketsResponse: BucketResponse = response.data;
+ const totalCount = bucketsResponse.totalCount;
+ const buckets: Bucket[] = bucketsResponse.buckets;
+
+ const dataSource: Bucket[] = buckets.map(bucket => {
+ return {
+ volumeName: bucket.volumeName,
+ name: bucket.name,
+ versioning: bucket.versioning,
+ storageType: bucket.storageType,
+ bucketLayout: bucket.bucketLayout,
+ creationTime: bucket.creationTime,
+ modificationTime: bucket.modificationTime,
+ sourceVolume: bucket.sourceVolume,
+ sourceBucket: bucket.sourceBucket,
+ usedBytes: bucket.usedBytes,
+ usedNamespace: bucket.usedNamespace,
+ quotaInBytes: bucket.quotaInBytes,
+ quotaInNamespace: bucket.quotaInNamespace,
+ owner: bucket.owner,
+ acls: bucket.acls
+ };
+ });
+
+ const volumeBucketMap: Map<string, Set<Bucket>> = getVolumeBucketMap(dataSource);
+
+ // Set options for volume selection dropdown
+ const volumeOptions: Option[] = Array.from(
+ volumeBucketMap.keys()
+ ).map(k => ({
+ label: k,
+ value: k
+ }));
+
+ setLoading(false);
+
+ setSelectedVolumes((prevState) => {
+ if (prevState.length === 0) return volumeOptions;
+ return prevState;
+ });
+
+ setState({
+ ...state,
+ totalCount: totalCount,
+ volumeBucketMap: volumeBucketMap,
+ volumeOptions: volumeOptions,
+ lastUpdated: Number(moment())
+ });
+ }).catch(error => {
+ setLoading(false);
+ showDataFetchError(error.toString());
+ });
+ }
+
+ let autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+ useEffect(() => {
+ autoReloadHelper.startPolling();
+ addAclColumn();
+ const initialVolume = getVolumeSearchParam();
+ if (initialVolume) {
+ setSelectedVolumes([{
+ label: initialVolume,
+ value: initialVolume
+ }]);
+ }
+ loadData();
+
+
Review Comment:
I think since the React docs already specifies this, it is unnecessary.
Ref: https://react.dev/reference/react/useEffect
```
setup: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function.
When your component is added to the DOM, React will run your setup function.
After every re-render with changed dependencies, React will first run the cleanup function (if you provided it)
with the old values, and then run your setup function with the new values.
After your component is removed from the DOM, React will run your cleanup function.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "devabhishekpal (via GitHub)" <gi...@apache.org>.
devabhishekpal commented on PR #7100:
URL: https://github.com/apache/ozone/pull/7100#issuecomment-2304396510
Thanks for the suggestions @smitajoshi12,
I added the null checks on the response and made the search bar disabled on no data being present.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "devabhishekpal (via GitHub)" <gi...@apache.org>.
devabhishekpal commented on code in PR #7100:
URL: https://github.com/apache/ozone/pull/7100#discussion_r1727708465
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
Review Comment:
Thanks for pointing this out.
In the current implementation we will need to abstract out the function.
Also since the calculation for the state is in an object, the dependent values i.e bucketName/name and volumeName will always be treated as a new object param.
If we execute the below code in Node or console, we will see that object are called by ref rather than value, so even
```
x = {
a: 1,
b: 2
};
y = {
a: 1,
b: 2
}
x == y;
```
It will return false, even though the value is the same.
Hence on every load, even if the volumeName and bucketName remains the same, it will be treated as new object and trigger re-calculation which again becomes the same as current implementation with extra useMemo import.
One way to avoid this is to create separate states for each object field. But in React v16 since state updates are not batched, having:
```
const [volumeName, setVolumeName] = useState('');
const [bucketName, setBucketName] = useState('');
...
...
const [owner, setOwner] = useState('');
const [acls, setAcls] = useState();
```
and then calling setVolumeName, setBucketName, setOwner etc one by one is inefficient in React 16.
That being said, once we upgrade to React 18, these updates are batched and then we can use useMemo to cache the calculation
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "ArafatKhan2198 (via GitHub)" <gi...@apache.org>.
ArafatKhan2198 commented on PR #7100:
URL: https://github.com/apache/ozone/pull/7100#issuecomment-2326358822
Thanks for the patch @devabhishekpal & thanks @devmadhuu for the review.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "devabhishekpal (via GitHub)" <gi...@apache.org>.
devabhishekpal commented on code in PR #7100:
URL: https://github.com/apache/ozone/pull/7100#discussion_r1727687822
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
+import moment from 'moment';
+import { Table, Tag } from 'antd';
+import {
+ ColumnProps,
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ CloudServerOutlined,
+ FileUnknownOutlined,
+ HddOutlined,
+ LaptopOutlined,
+ SaveOutlined
+} from '@ant-design/icons';
+import { ValueType } from 'react-select';
+import { useLocation } from 'react-router-dom';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
+import Search from '@/v2/components/search/search';
+import MultiSelect from '@/v2/components/select/multiSelect';
+import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
+import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Bucket,
+ BucketLayout,
+ BucketLayoutTypeList,
+ BucketResponse,
+ BucketsState,
+ BucketStorage,
+ BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+import './buckets.less';
+
+
+const LIMIT_OPTIONS: Option[] = [
+ {
+ label: '1000',
+ value: '1000'
+ },
+ {
+ label: '5000',
+ value: '5000'
+ },
+ {
+ label: '10000',
+ value: '10000'
+ },
+ {
+ label: '20000',
+ value: '20000'
+ }
+]
+
+const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
+ return isVersionEnabled
+ ? <CheckCircleOutlined
+ style={{ color: '#1da57a' }}
+ className='icon-success' />
+ : <CloseCircleOutlined className='icon-neutral' />
+};
+
+const renderStorageType = (bucketStorage: BucketStorage) => {
+ const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+ RAM_DISK: <LaptopOutlined />,
+ SSD: <SaveOutlined />,
+ DISK: <HddOutlined />,
+ ARCHIVE: <CloudServerOutlined />
+ };
+ const icon = bucketStorage in bucketStorageIconMap
+ ? bucketStorageIconMap[bucketStorage]
+ : <FileUnknownOutlined />;
+ return <span>{icon} {bucketStorage}</span>;
+};
+
+const renderBucketLayout = (bucketLayout: BucketLayout) => {
+ const bucketLayoutColorMap = {
+ FILE_SYSTEM_OPTIMIZED: 'green',
+ OBJECT_STORE: 'blue',
+ LEGACY: 'gray'
+ };
+ const color = bucketLayout in bucketLayoutColorMap ?
+ bucketLayoutColorMap[bucketLayout] : '';
+ return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+const SearchableColumnOpts = [{
+ label: 'Bucket',
+ value: 'name'
+}, {
+ label: 'Volume',
+ value: 'volumeName'
+}]
+
+const COLUMNS: ColumnsType<Bucket> = [
+ {
+ title: 'Bucket',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Volume',
+ dataIndex: 'volumeName',
+ key: 'volumeName',
+ sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+ },
+ {
+ title: 'Versioning',
+ dataIndex: 'versioning',
+ key: 'isVersionEnabled',
+ render: (isVersionEnabled: boolean) => renderIsVersionEnabled(isVersionEnabled)
+ },
+ {
+ title: 'Storage Type',
+ dataIndex: 'storageType',
+ key: 'storageType',
+ filterMultiple: true,
+ filters: BucketStorageTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.storageType === value,
+ sorter: (a: Bucket, b: Bucket) => a.storageType.localeCompare(b.storageType),
+ render: (storageType: BucketStorage) => renderStorageType(storageType)
+ },
+ {
+ title: 'Bucket Layout',
+ dataIndex: 'bucketLayout',
+ key: 'bucketLayout',
+ filterMultiple: true,
+ filters: BucketLayoutTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.bucketLayout === value,
+ sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout),
+ render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Storage Capacity',
+ key: 'quotaCapacityBytes',
+ sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInBytes}
+ used={record.usedBytes}
+ quotaType='size'
+ />
+ )
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+ {
+ title: 'Source Volume',
+ dataIndex: 'sourceVolume',
+ key: 'sourceVolume',
+ render: (sourceVolume: string) => {
+ return sourceVolume ? sourceVolume : 'NA';
+ }
+ },
+ {
+ title: 'Source Bucket',
+ dataIndex: 'sourceBucket',
+ key: 'sourceBucket',
+ render: (sourceBucket: string) => {
+ return sourceBucket ? sourceBucket : 'NA';
+ }
+ }
+];
+
+const defaultColumns = COLUMNS.map(column => ({
+ label: column.title as string,
+ value: column.key as string
+}));
+
+function getVolumeBucketMap(data: Bucket[]) {
+ const volumeBucketMap = data.reduce((
+ map: Map<string, Set<Bucket>>,
+ currentBucket
+ ) => {
+ const volume = currentBucket.volumeName;
+ if (map.has(volume)) {
+ const buckets = Array.from(map.get(volume)!);
+ map.set(volume, new Set([...buckets, currentBucket]));
+ } else {
+ map.set(volume, new Set<Bucket>().add(currentBucket));
+ }
+ return map;
+ }, new Map<string, Set<Bucket>>());
+ return volumeBucketMap;
+}
+
+function getFilteredBuckets(
+ selectedVolumes: Option[],
+ bucketsMap: Map<string, Set<Bucket>>
+) {
+ let selectedBuckets: Bucket[] = [];
+ selectedVolumes.forEach(selectedVolume => {
+ if (bucketsMap.has(selectedVolume.value)
+ && bucketsMap.get(selectedVolume.value)) {
+ selectedBuckets = [
+ ...selectedBuckets,
+ ...Array.from(bucketsMap.get(selectedVolume.value)!)
+ ];
+ }
+ });
+
+ return selectedBuckets;
+}
+
+const Buckets: React.FC<{}> = () => {
+
+ let cancelSignal: AbortController;
+
+ const [state, setState] = useState<BucketsState>({
+ totalCount: 0,
+ lastUpdated: 0,
+ columnOptions: defaultColumns,
+ volumeBucketMap: new Map<string, Set<Bucket>>(),
+ bucketsUnderVolume: [],
+ volumeOptions: [],
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] = useState<Option[]>(defaultColumns);
+ const [selectedVolumes, setSelectedVolumes] = useState<Option[]>([]);
+ const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
+ const [searchTerm, setSearchTerm] = useState<string>('');
+ const [showPanel, setShowPanel] = useState<boolean>(false);
+ const [searchColumn, setSearchColumn] = useState<'name' | 'volumeName'>('name');
+ const [currentRow, setCurrentRow] = useState<Bucket | Record<string, never>>({})
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+ const { search } = useLocation();
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} buckets`,
+ showSizeChanger: true
+ };
+
+ function getVolumeSearchParam() {
+ return new URLSearchParams(search).get('volume');
Review Comment:
Yes it will be null as it is the search param which comes only when we click on `Show Buckets` in the Volumes page.
If we directly access the Buckets page, this param will not be present and hence return null.
We are handling this already in the code:
https://github.com/apache/ozone/blob/c29db4c59a7a6d622c49b18ae3eee65b4e481a10/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx#L452
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "smitajoshi12 (via GitHub)" <gi...@apache.org>.
smitajoshi12 commented on PR #7100:
URL: https://github.com/apache/ozone/pull/7100#issuecomment-2303944810
@devabhishekpal
If there is no data can we disable Search functionality.

--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "smitajoshi12 (via GitHub)" <gi...@apache.org>.
smitajoshi12 commented on code in PR #7100:
URL: https://github.com/apache/ozone/pull/7100#discussion_r1726490117
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
Review Comment:
@devabhishekpal
useCallback is defined no where. Can we use useMemo for improve caching all components which are frequentlt loaded in memory.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "smitajoshi12 (via GitHub)" <gi...@apache.org>.
smitajoshi12 commented on code in PR #7100:
URL: https://github.com/apache/ozone/pull/7100#discussion_r1726505992
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
+import moment from 'moment';
+import { Table, Tag } from 'antd';
+import {
+ ColumnProps,
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ CloudServerOutlined,
+ FileUnknownOutlined,
+ HddOutlined,
+ LaptopOutlined,
+ SaveOutlined
+} from '@ant-design/icons';
+import { ValueType } from 'react-select';
+import { useLocation } from 'react-router-dom';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
+import Search from '@/v2/components/search/search';
+import MultiSelect from '@/v2/components/select/multiSelect';
+import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
+import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Bucket,
+ BucketLayout,
+ BucketLayoutTypeList,
+ BucketResponse,
+ BucketsState,
+ BucketStorage,
+ BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+import './buckets.less';
+
+
+const LIMIT_OPTIONS: Option[] = [
+ {
+ label: '1000',
+ value: '1000'
+ },
+ {
+ label: '5000',
+ value: '5000'
+ },
+ {
+ label: '10000',
+ value: '10000'
+ },
+ {
+ label: '20000',
+ value: '20000'
+ }
+]
+
+const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
+ return isVersionEnabled
+ ? <CheckCircleOutlined
+ style={{ color: '#1da57a' }}
+ className='icon-success' />
+ : <CloseCircleOutlined className='icon-neutral' />
+};
+
+const renderStorageType = (bucketStorage: BucketStorage) => {
+ const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+ RAM_DISK: <LaptopOutlined />,
+ SSD: <SaveOutlined />,
+ DISK: <HddOutlined />,
+ ARCHIVE: <CloudServerOutlined />
+ };
+ const icon = bucketStorage in bucketStorageIconMap
+ ? bucketStorageIconMap[bucketStorage]
+ : <FileUnknownOutlined />;
+ return <span>{icon} {bucketStorage}</span>;
+};
+
+const renderBucketLayout = (bucketLayout: BucketLayout) => {
+ const bucketLayoutColorMap = {
+ FILE_SYSTEM_OPTIMIZED: 'green',
+ OBJECT_STORE: 'blue',
+ LEGACY: 'gray'
+ };
+ const color = bucketLayout in bucketLayoutColorMap ?
+ bucketLayoutColorMap[bucketLayout] : '';
+ return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+const SearchableColumnOpts = [{
+ label: 'Bucket',
+ value: 'name'
+}, {
+ label: 'Volume',
+ value: 'volumeName'
+}]
+
+const COLUMNS: ColumnsType<Bucket> = [
+ {
+ title: 'Bucket',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Volume',
+ dataIndex: 'volumeName',
+ key: 'volumeName',
+ sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+ },
+ {
+ title: 'Versioning',
+ dataIndex: 'versioning',
+ key: 'isVersionEnabled',
+ render: (isVersionEnabled: boolean) => renderIsVersionEnabled(isVersionEnabled)
+ },
+ {
+ title: 'Storage Type',
+ dataIndex: 'storageType',
+ key: 'storageType',
+ filterMultiple: true,
+ filters: BucketStorageTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.storageType === value,
+ sorter: (a: Bucket, b: Bucket) => a.storageType.localeCompare(b.storageType),
+ render: (storageType: BucketStorage) => renderStorageType(storageType)
+ },
+ {
+ title: 'Bucket Layout',
+ dataIndex: 'bucketLayout',
+ key: 'bucketLayout',
+ filterMultiple: true,
+ filters: BucketLayoutTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.bucketLayout === value,
+ sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout),
+ render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Storage Capacity',
+ key: 'quotaCapacityBytes',
+ sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInBytes}
+ used={record.usedBytes}
+ quotaType='size'
+ />
+ )
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+ {
+ title: 'Source Volume',
+ dataIndex: 'sourceVolume',
+ key: 'sourceVolume',
+ render: (sourceVolume: string) => {
+ return sourceVolume ? sourceVolume : 'NA';
+ }
+ },
+ {
+ title: 'Source Bucket',
+ dataIndex: 'sourceBucket',
+ key: 'sourceBucket',
+ render: (sourceBucket: string) => {
+ return sourceBucket ? sourceBucket : 'NA';
+ }
+ }
+];
+
+const defaultColumns = COLUMNS.map(column => ({
+ label: column.title as string,
+ value: column.key as string
+}));
+
+function getVolumeBucketMap(data: Bucket[]) {
+ const volumeBucketMap = data.reduce((
+ map: Map<string, Set<Bucket>>,
+ currentBucket
+ ) => {
+ const volume = currentBucket.volumeName;
+ if (map.has(volume)) {
+ const buckets = Array.from(map.get(volume)!);
+ map.set(volume, new Set([...buckets, currentBucket]));
+ } else {
+ map.set(volume, new Set<Bucket>().add(currentBucket));
+ }
+ return map;
+ }, new Map<string, Set<Bucket>>());
+ return volumeBucketMap;
+}
+
+function getFilteredBuckets(
+ selectedVolumes: Option[],
+ bucketsMap: Map<string, Set<Bucket>>
+) {
+ let selectedBuckets: Bucket[] = [];
+ selectedVolumes.forEach(selectedVolume => {
+ if (bucketsMap.has(selectedVolume.value)
+ && bucketsMap.get(selectedVolume.value)) {
+ selectedBuckets = [
+ ...selectedBuckets,
+ ...Array.from(bucketsMap.get(selectedVolume.value)!)
+ ];
+ }
+ });
+
+ return selectedBuckets;
+}
+
+const Buckets: React.FC<{}> = () => {
+
+ let cancelSignal: AbortController;
+
+ const [state, setState] = useState<BucketsState>({
+ totalCount: 0,
+ lastUpdated: 0,
+ columnOptions: defaultColumns,
+ volumeBucketMap: new Map<string, Set<Bucket>>(),
+ bucketsUnderVolume: [],
+ volumeOptions: [],
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] = useState<Option[]>(defaultColumns);
+ const [selectedVolumes, setSelectedVolumes] = useState<Option[]>([]);
+ const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
+ const [searchTerm, setSearchTerm] = useState<string>('');
+ const [showPanel, setShowPanel] = useState<boolean>(false);
+ const [searchColumn, setSearchColumn] = useState<'name' | 'volumeName'>('name');
+ const [currentRow, setCurrentRow] = useState<Bucket | Record<string, never>>({})
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+ const { search } = useLocation();
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} buckets`,
+ showSizeChanger: true
+ };
+
+ function getVolumeSearchParam() {
+ return new URLSearchParams(search).get('volume');
Review Comment:
Always getting null here can u cross verify something is missing
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
Review Comment:
@devabhishekpal
useCallback is defined any where. Can we use useMemo for improve caching all components which are frequentlt loaded in memory.
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx:
##########
@@ -0,0 +1,563 @@
+/*
+ * 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, { useCallback, useEffect, useState } from 'react';
+import moment from 'moment';
+import { Table, Tag } from 'antd';
+import {
+ ColumnProps,
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ CloudServerOutlined,
+ FileUnknownOutlined,
+ HddOutlined,
+ LaptopOutlined,
+ SaveOutlined
+} from '@ant-design/icons';
+import { ValueType } from 'react-select';
+import { useLocation } from 'react-router-dom';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
+import Search from '@/v2/components/search/search';
+import MultiSelect from '@/v2/components/select/multiSelect';
+import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
+
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
+import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Bucket,
+ BucketLayout,
+ BucketLayoutTypeList,
+ BucketResponse,
+ BucketsState,
+ BucketStorage,
+ BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+import './buckets.less';
+
+
+const LIMIT_OPTIONS: Option[] = [
+ {
+ label: '1000',
+ value: '1000'
+ },
+ {
+ label: '5000',
+ value: '5000'
+ },
+ {
+ label: '10000',
+ value: '10000'
+ },
+ {
+ label: '20000',
+ value: '20000'
+ }
+]
+
+const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
+ return isVersionEnabled
+ ? <CheckCircleOutlined
+ style={{ color: '#1da57a' }}
+ className='icon-success' />
+ : <CloseCircleOutlined className='icon-neutral' />
+};
+
+const renderStorageType = (bucketStorage: BucketStorage) => {
+ const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+ RAM_DISK: <LaptopOutlined />,
+ SSD: <SaveOutlined />,
+ DISK: <HddOutlined />,
+ ARCHIVE: <CloudServerOutlined />
+ };
+ const icon = bucketStorage in bucketStorageIconMap
+ ? bucketStorageIconMap[bucketStorage]
+ : <FileUnknownOutlined />;
+ return <span>{icon} {bucketStorage}</span>;
+};
+
+const renderBucketLayout = (bucketLayout: BucketLayout) => {
+ const bucketLayoutColorMap = {
+ FILE_SYSTEM_OPTIMIZED: 'green',
+ OBJECT_STORE: 'blue',
+ LEGACY: 'gray'
+ };
+ const color = bucketLayout in bucketLayoutColorMap ?
+ bucketLayoutColorMap[bucketLayout] : '';
+ return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+const SearchableColumnOpts = [{
+ label: 'Bucket',
+ value: 'name'
+}, {
+ label: 'Volume',
+ value: 'volumeName'
+}]
+
+const COLUMNS: ColumnsType<Bucket> = [
+ {
+ title: 'Bucket',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Volume',
+ dataIndex: 'volumeName',
+ key: 'volumeName',
+ sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+ },
+ {
+ title: 'Versioning',
+ dataIndex: 'versioning',
+ key: 'isVersionEnabled',
+ render: (isVersionEnabled: boolean) => renderIsVersionEnabled(isVersionEnabled)
+ },
+ {
+ title: 'Storage Type',
+ dataIndex: 'storageType',
+ key: 'storageType',
+ filterMultiple: true,
+ filters: BucketStorageTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.storageType === value,
+ sorter: (a: Bucket, b: Bucket) => a.storageType.localeCompare(b.storageType),
+ render: (storageType: BucketStorage) => renderStorageType(storageType)
+ },
+ {
+ title: 'Bucket Layout',
+ dataIndex: 'bucketLayout',
+ key: 'bucketLayout',
+ filterMultiple: true,
+ filters: BucketLayoutTypeList.map(state => ({ text: state, value: state })),
+ onFilter: (value, record: Bucket) => record.bucketLayout === value,
+ sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout),
+ render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Storage Capacity',
+ key: 'quotaCapacityBytes',
+ sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInBytes}
+ used={record.usedBytes}
+ quotaType='size'
+ />
+ )
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+ {
+ title: 'Source Volume',
+ dataIndex: 'sourceVolume',
+ key: 'sourceVolume',
+ render: (sourceVolume: string) => {
+ return sourceVolume ? sourceVolume : 'NA';
+ }
+ },
+ {
+ title: 'Source Bucket',
+ dataIndex: 'sourceBucket',
+ key: 'sourceBucket',
+ render: (sourceBucket: string) => {
+ return sourceBucket ? sourceBucket : 'NA';
+ }
+ }
+];
+
+const defaultColumns = COLUMNS.map(column => ({
+ label: column.title as string,
+ value: column.key as string
+}));
+
+function getVolumeBucketMap(data: Bucket[]) {
+ const volumeBucketMap = data.reduce((
+ map: Map<string, Set<Bucket>>,
+ currentBucket
+ ) => {
+ const volume = currentBucket.volumeName;
+ if (map.has(volume)) {
+ const buckets = Array.from(map.get(volume)!);
+ map.set(volume, new Set([...buckets, currentBucket]));
+ } else {
+ map.set(volume, new Set<Bucket>().add(currentBucket));
+ }
+ return map;
+ }, new Map<string, Set<Bucket>>());
+ return volumeBucketMap;
+}
+
+function getFilteredBuckets(
+ selectedVolumes: Option[],
+ bucketsMap: Map<string, Set<Bucket>>
+) {
+ let selectedBuckets: Bucket[] = [];
+ selectedVolumes.forEach(selectedVolume => {
+ if (bucketsMap.has(selectedVolume.value)
+ && bucketsMap.get(selectedVolume.value)) {
+ selectedBuckets = [
+ ...selectedBuckets,
+ ...Array.from(bucketsMap.get(selectedVolume.value)!)
+ ];
+ }
+ });
+
+ return selectedBuckets;
+}
+
+const Buckets: React.FC<{}> = () => {
+
+ let cancelSignal: AbortController;
+
+ const [state, setState] = useState<BucketsState>({
+ totalCount: 0,
+ lastUpdated: 0,
+ columnOptions: defaultColumns,
+ volumeBucketMap: new Map<string, Set<Bucket>>(),
+ bucketsUnderVolume: [],
+ volumeOptions: [],
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] = useState<Option[]>(defaultColumns);
+ const [selectedVolumes, setSelectedVolumes] = useState<Option[]>([]);
+ const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
+ const [searchTerm, setSearchTerm] = useState<string>('');
+ const [showPanel, setShowPanel] = useState<boolean>(false);
+ const [searchColumn, setSearchColumn] = useState<'name' | 'volumeName'>('name');
+ const [currentRow, setCurrentRow] = useState<Bucket | Record<string, never>>({})
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+ const { search } = useLocation();
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} buckets`,
+ showSizeChanger: true
+ };
+
+ function getVolumeSearchParam() {
+ return new URLSearchParams(search).get('volume');
+ };
+
+ function getFilteredData(data: Bucket[]) {
+ return data.filter(
+ (bucket: Bucket) => bucket[searchColumn].includes(debouncedSearch)
+ );
+ }
+
+ function handleVolumeChange(selected: ValueType<Option, true>) {
+ const { volumeBucketMap } = state;
+ const volumeSelections = (selected as Option[]);
+ let selectedBuckets: Bucket[] = [];
+
+ if (volumeSelections?.length > 0) {
+ selectedBuckets = getFilteredBuckets(volumeSelections, volumeBucketMap)
+ }
+
+ setSelectedVolumes(volumeSelections);
+ setState({
+ ...state,
+ bucketsUnderVolume: selectedBuckets
+ });
+ };
+
+ function handleAclLinkClick(bucket: Bucket) {
+ setCurrentRow(bucket);
+ setShowPanel(true);
+ }
+
+ function filterSelectedColumns() {
+ const columnKeys = selectedColumns.map((column) => column.value);
+ return COLUMNS.filter(
+ (column) => columnKeys.indexOf(column.key as string) >= 0
+ )
+ }
+
+ function addAclColumn() {
+ // Inside the class component to access the React internal state
+ const aclLinkColumn: ColumnProps<Bucket> = {
+ title: 'ACLs',
+ dataIndex: 'acls',
+ key: 'acls',
+ render: (_: any, record: Bucket) => {
+ return (
+ <a
+ key='acl'
+ onClick={() => {
+ handleAclLinkClick(record);
+ }}
+ >
+ Show ACL
+ </a>
+ );
+ }
+ };
+
+ if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
+ // Push the ACL column for initial
+ COLUMNS.push(aclLinkColumn);
+ } else {
+ // Replace old ACL column with new ACL column with correct reference
+ // e.g. After page is reloaded / redirect from other page
+ COLUMNS[COLUMNS.length - 1] = aclLinkColumn;
+ }
+
+ if (defaultColumns.length > 0 && defaultColumns[defaultColumns.length - 1].label !== 'acls') {
+ defaultColumns.push({
+ label: aclLinkColumn.title as string,
+ value: aclLinkColumn.key as string
+ });
+ }
+ };
+
+ function handleColumnChange(selected: ValueType<Option, true>) {
+ setSelectedColumns(selected as Option[]);
+ }
+
+ function handleLimitChange(selected: ValueType<Option, false>) {
+ setSelectedLimit(selected as Option);
+ }
+
+ const loadData = () => {
+ setLoading(true);
+ const { request, controller } = AxiosGetHelper(
+ '/api/v1/buckets',
+ cancelSignal,
+ '',
+ { limit: selectedLimit.value }
+ );
+ cancelSignal = controller;
+ request.then(response => {
+ const bucketsResponse: BucketResponse = response.data;
+ const totalCount = bucketsResponse.totalCount;
+ const buckets: Bucket[] = bucketsResponse.buckets;
+
+ const dataSource: Bucket[] = buckets.map(bucket => {
+ return {
+ volumeName: bucket.volumeName,
+ name: bucket.name,
+ versioning: bucket.versioning,
+ storageType: bucket.storageType,
+ bucketLayout: bucket.bucketLayout,
+ creationTime: bucket.creationTime,
+ modificationTime: bucket.modificationTime,
+ sourceVolume: bucket.sourceVolume,
+ sourceBucket: bucket.sourceBucket,
+ usedBytes: bucket.usedBytes,
+ usedNamespace: bucket.usedNamespace,
+ quotaInBytes: bucket.quotaInBytes,
+ quotaInNamespace: bucket.quotaInNamespace,
+ owner: bucket.owner,
+ acls: bucket.acls
+ };
+ });
+
+ const volumeBucketMap: Map<string, Set<Bucket>> = getVolumeBucketMap(dataSource);
+
+ // Set options for volume selection dropdown
+ const volumeOptions: Option[] = Array.from(
+ volumeBucketMap.keys()
+ ).map(k => ({
+ label: k,
+ value: k
+ }));
+
+ setLoading(false);
+
+ setSelectedVolumes((prevState) => {
+ if (prevState.length === 0) return volumeOptions;
+ return prevState;
+ });
+
+ setState({
+ ...state,
+ totalCount: totalCount,
+ volumeBucketMap: volumeBucketMap,
+ volumeOptions: volumeOptions,
+ lastUpdated: Number(moment())
+ });
+ }).catch(error => {
+ setLoading(false);
+ showDataFetchError(error.toString());
+ });
+ }
+
+ let autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+ useEffect(() => {
+ autoReloadHelper.startPolling();
+ addAclColumn();
+ const initialVolume = getVolumeSearchParam();
+ if (initialVolume) {
+ setSelectedVolumes([{
+ label: initialVolume,
+ value: initialVolume
+ }]);
+ }
+ loadData();
+
+
Review Comment:
Can you add comment here it will act as componentwill unmount and used for Clean up purpose
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org
Re: [PR] HDDS-11156. Improve Buckets page UI [ozone]
Posted by "smitajoshi12 (via GitHub)" <gi...@apache.org>.
smitajoshi12 commented on PR #7100:
URL: https://github.com/apache/ozone/pull/7100#issuecomment-2303948530
@devabhishekpal
If we get empty results from API Response or null then map method is failing.

--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org