Skip to content

Commit 7a26aff

Browse files
authored
HDDS-11158. Improve Pipelines page UI (apache#7171)
1 parent c365aa0 commit 7a26aff

13 files changed

Lines changed: 1039 additions & 363 deletions

File tree

hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React from 'react';
2020
import { Input, Select } from 'antd';
2121

2222
import { Option } from '@/v2/components/select/singleSelect';
23+
import { DownOutlined } from '@ant-design/icons';
2324

2425
// ------------- Types -------------- //
2526
type SearchProps = {
@@ -51,6 +52,7 @@ const Search: React.FC<SearchProps> = ({
5152
const selectFilter = searchColumn
5253
? (<Select
5354
disabled={disabled}
55+
suffixIcon={(searchOptions.length > 1) ? <DownOutlined/> : null}
5456
defaultValue={searchColumn}
5557
options={searchOptions}
5658
onChange={onChange} />)
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import React from 'react';
20+
21+
import moment from 'moment';
22+
import Table, {
23+
ColumnProps,
24+
ColumnsType,
25+
TablePaginationConfig
26+
} from 'antd/es/table';
27+
import Tag from 'antd/es/tag';
28+
import {
29+
CheckCircleOutlined,
30+
CloseCircleOutlined,
31+
CloudServerOutlined,
32+
FileUnknownOutlined,
33+
HddOutlined,
34+
LaptopOutlined,
35+
SaveOutlined
36+
} from '@ant-design/icons';
37+
38+
import QuotaBar from '@/components/quotaBar/quotaBar';
39+
import { nullAwareLocaleCompare } from '@/utils/common';
40+
import {
41+
Bucket,
42+
BucketLayout,
43+
BucketLayoutTypeList,
44+
BucketsTableProps,
45+
BucketStorage,
46+
BucketStorageTypeList
47+
} from '@/v2/types/bucket.types';
48+
49+
function renderIsVersionEnabled(isVersionEnabled: boolean) {
50+
return isVersionEnabled
51+
? <CheckCircleOutlined
52+
style={{ color: '#1da57a' }}
53+
className='icon-success' />
54+
: <CloseCircleOutlined className='icon-neutral' />
55+
};
56+
57+
function renderStorageType(bucketStorage: BucketStorage) {
58+
const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
59+
RAM_DISK: <LaptopOutlined />,
60+
SSD: <SaveOutlined />,
61+
DISK: <HddOutlined />,
62+
ARCHIVE: <CloudServerOutlined />
63+
};
64+
const icon = bucketStorage in bucketStorageIconMap
65+
? bucketStorageIconMap[bucketStorage]
66+
: <FileUnknownOutlined />;
67+
return <span>{icon} {bucketStorage}</span>;
68+
};
69+
70+
function renderBucketLayout(bucketLayout: BucketLayout) {
71+
const bucketLayoutColorMap = {
72+
FILE_SYSTEM_OPTIMIZED: 'green',
73+
OBJECT_STORE: 'orange',
74+
LEGACY: 'blue'
75+
};
76+
const color = bucketLayout in bucketLayoutColorMap ?
77+
bucketLayoutColorMap[bucketLayout] : '';
78+
return <Tag color={color}>{bucketLayout}</Tag>;
79+
};
80+
81+
export const COLUMNS: ColumnsType<Bucket> = [
82+
{
83+
title: 'Bucket',
84+
dataIndex: 'name',
85+
key: 'name',
86+
sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
87+
defaultSortOrder: 'ascend' as const
88+
},
89+
{
90+
title: 'Volume',
91+
dataIndex: 'volumeName',
92+
key: 'volumeName',
93+
sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
94+
defaultSortOrder: 'ascend' as const
95+
},
96+
{
97+
title: 'Owner',
98+
dataIndex: 'owner',
99+
key: 'owner',
100+
sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
101+
},
102+
{
103+
title: 'Versioning',
104+
dataIndex: 'versioning',
105+
key: 'isVersionEnabled',
106+
render: (isVersionEnabled: boolean) => renderIsVersionEnabled(isVersionEnabled)
107+
},
108+
{
109+
title: 'Storage Type',
110+
dataIndex: 'storageType',
111+
key: 'storageType',
112+
filterMultiple: true,
113+
filters: BucketStorageTypeList.map(state => ({ text: state, value: state })),
114+
onFilter: (value, record: Bucket) => record.storageType === value,
115+
sorter: (a: Bucket, b: Bucket) => a.storageType.localeCompare(b.storageType),
116+
render: (storageType: BucketStorage) => renderStorageType(storageType)
117+
},
118+
{
119+
title: 'Bucket Layout',
120+
dataIndex: 'bucketLayout',
121+
key: 'bucketLayout',
122+
filterMultiple: true,
123+
filters: BucketLayoutTypeList.map(state => ({ text: state, value: state })),
124+
onFilter: (value, record: Bucket) => record.bucketLayout === value,
125+
sorter: (a: Bucket, b: Bucket) => a.bucketLayout.localeCompare(b.bucketLayout),
126+
render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
127+
},
128+
{
129+
title: 'Creation Time',
130+
dataIndex: 'creationTime',
131+
key: 'creationTime',
132+
sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
133+
render: (creationTime: number) => {
134+
return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
135+
}
136+
},
137+
{
138+
title: 'Modification Time',
139+
dataIndex: 'modificationTime',
140+
key: 'modificationTime',
141+
sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
142+
render: (modificationTime: number) => {
143+
return modificationTime > 0 ? moment(modificationTime).format('ll LTS') : 'NA';
144+
}
145+
},
146+
{
147+
title: 'Storage Capacity',
148+
key: 'quotaCapacityBytes',
149+
sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
150+
render: (text: string, record: Bucket) => (
151+
<QuotaBar
152+
quota={record.quotaInBytes}
153+
used={record.usedBytes}
154+
quotaType='size'
155+
/>
156+
)
157+
},
158+
{
159+
title: 'Namespace Capacity',
160+
key: 'namespaceCapacity',
161+
sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
162+
render: (text: string, record: Bucket) => (
163+
<QuotaBar
164+
quota={record.quotaInNamespace}
165+
used={record.usedNamespace}
166+
quotaType='namespace'
167+
/>
168+
)
169+
},
170+
{
171+
title: 'Source Volume',
172+
dataIndex: 'sourceVolume',
173+
key: 'sourceVolume',
174+
render: (sourceVolume: string) => {
175+
return sourceVolume ? sourceVolume : 'NA';
176+
}
177+
},
178+
{
179+
title: 'Source Bucket',
180+
dataIndex: 'sourceBucket',
181+
key: 'sourceBucket',
182+
render: (sourceBucket: string) => {
183+
return sourceBucket ? sourceBucket : 'NA';
184+
}
185+
}
186+
];
187+
188+
const BucketsTable: React.FC<BucketsTableProps> = ({
189+
loading = false,
190+
data,
191+
handleAclClick,
192+
selectedColumns,
193+
searchColumn = 'name',
194+
searchTerm = ''
195+
}) => {
196+
197+
React.useEffect(() => {
198+
const aclColumn: ColumnProps<Bucket> = {
199+
title: 'ACLs',
200+
dataIndex: 'acls',
201+
key: 'acls',
202+
render: (_: any, record: Bucket) => {
203+
return (
204+
<a
205+
key='acl'
206+
onClick={() => {
207+
handleAclClick(record);
208+
}}
209+
>
210+
Show ACL
211+
</a>
212+
);
213+
}
214+
};
215+
216+
if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
217+
// Push the ACL column for initial load
218+
COLUMNS.push(aclColumn);
219+
selectedColumns.push({
220+
label: aclColumn.title as string,
221+
value: aclColumn.key as string
222+
});
223+
} else {
224+
// Replace old ACL column with new ACL column with correct reference
225+
// e.g. After page is reloaded / redirect from other page
226+
COLUMNS[COLUMNS.length - 1] = aclColumn;
227+
selectedColumns[selectedColumns.length - 1] = {
228+
label: aclColumn.title as string,
229+
value: aclColumn.key as string
230+
}
231+
}
232+
}, []);
233+
234+
function filterSelectedColumns() {
235+
const columnKeys = selectedColumns.map((column) => column.value);
236+
return COLUMNS.filter(
237+
(column) => columnKeys.indexOf(column.key as string) >= 0
238+
)
239+
}
240+
241+
function getFilteredData(data: Bucket[]) {
242+
return data.filter(
243+
(bucket: Bucket) => bucket[searchColumn].includes(searchTerm)
244+
);
245+
}
246+
247+
const paginationConfig: TablePaginationConfig = {
248+
showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} buckets`,
249+
showSizeChanger: true
250+
};
251+
252+
return (
253+
<div>
254+
<Table
255+
dataSource={getFilteredData(data)}
256+
columns={filterSelectedColumns()}
257+
loading={loading}
258+
rowKey='volume'
259+
pagination={paginationConfig}
260+
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
261+
locale={{ filterTitle: '' }}
262+
/>
263+
</div>
264+
)
265+
}
266+
267+
export default BucketsTable;

0 commit comments

Comments
 (0)