Skip to content

Commit e00f7ae

Browse files
authored
HDDS-11159. Improve Containers page UI (#7267)
1 parent cfda951 commit e00f7ae

File tree

11 files changed

+713
-23
lines changed

11 files changed

+713
-23
lines changed

hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@
19231923
"actualReplicaCount": 2,
19241924
"replicaDeltaCount": 1,
19251925
"reason": null,
1926-
"keys": 1,
1926+
"keys": 4,
19271927
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
19281928
"replicas": [
19291929
{
@@ -1997,7 +1997,7 @@
19971997
"actualReplicaCount": 2,
19981998
"replicaDeltaCount": 2,
19991999
"reason": null,
2000-
"keys": 1,
2000+
"keys": 3,
20012001
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
20022002
"replicas": [
20032003
{
@@ -2071,7 +2071,7 @@
20712071
"actualReplicaCount": 2,
20722072
"replicaDeltaCount": 2,
20732073
"reason": null,
2074-
"keys": 1,
2074+
"keys": 2,
20752075
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
20762076
"replicas": [
20772077
{
@@ -2108,7 +2108,7 @@
21082108
"actualReplicaCount": 2,
21092109
"replicaDeltaCount": 2,
21102110
"reason": null,
2111-
"keys": 1,
2111+
"keys": 5,
21122112
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
21132113
"replicas": [
21142114
{
@@ -2145,7 +2145,7 @@
21452145
"actualReplicaCount": 2,
21462146
"replicaDeltaCount": 2,
21472147
"reason": null,
2148-
"keys": 1,
2148+
"keys": 3,
21492149
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a1",
21502150
"replicas": [
21512151
{
@@ -2182,7 +2182,7 @@
21822182
"actualReplicaCount": 2,
21832183
"replicaDeltaCount": 2,
21842184
"reason": null,
2185-
"keys": 1,
2185+
"keys": 6,
21862186
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a2",
21872187
"replicas": [
21882188
{
@@ -2219,7 +2219,7 @@
22192219
"actualReplicaCount": 2,
22202220
"replicaDeltaCount": 2,
22212221
"reason": null,
2222-
"keys": 1,
2222+
"keys": 2,
22232223
"pipelineID": "a10ffab6-8ed5-414a-aaf5-79890ff3e8a3",
22242224
"replicas": [
22252225
{

hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const breadcrumbNameMap: IBreadcrumbNameMap = {
2727
'/Datanodes': 'Datanodes',
2828
'/Pipelines': 'Pipelines',
2929
'/MissingContainers': 'Missing Containers',
30+
'/Containers': 'Containers',
3031
'/Insights': 'Insights',
3132
'/DiskUsage': 'Disk Usage',
3233
'/Heatmap': 'Heatmap',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ const BucketsTable: React.FC<BucketsTableProps> = ({
255255
dataSource={getFilteredData(data)}
256256
columns={filterSelectedColumns()}
257257
loading={loading}
258-
rowKey='volume'
258+
rowKey={(record: Bucket) => `${record.volumeName}/${record.name}`}
259259
pagination={paginationConfig}
260260
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
261261
locale={{ filterTitle: '' }}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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, { useRef } from 'react';
20+
import filesize from 'filesize';
21+
import { AxiosError } from 'axios';
22+
import { Popover, Table } from 'antd';
23+
import {
24+
ColumnsType,
25+
TablePaginationConfig
26+
} from 'antd/es/table';
27+
import { NodeIndexOutlined } from '@ant-design/icons';
28+
29+
import { getFormattedTime } from '@/v2/utils/momentUtils';
30+
import { showDataFetchError } from '@/utils/common';
31+
import { AxiosGetHelper } from '@/utils/axiosRequestHelper';
32+
import {
33+
Container, ContainerKeysResponse, ContainerReplica,
34+
ContainerTableProps,
35+
ExpandedRowState, KeyResponse
36+
} from '@/v2/types/container.types';
37+
38+
const size = filesize.partial({ standard: 'iec' });
39+
40+
export const COLUMNS: ColumnsType<Container> = [
41+
{
42+
title: 'Container ID',
43+
dataIndex: 'containerID',
44+
key: 'containerID',
45+
sorter: (a: Container, b: Container) => a.containerID - b.containerID
46+
},
47+
{
48+
title: 'No. of Keys',
49+
dataIndex: 'keys',
50+
key: 'keys',
51+
sorter: (a: Container, b: Container) => a.keys - b.keys
52+
},
53+
{
54+
title: 'Actual/Expected Replica(s)',
55+
dataIndex: 'expectedReplicaCount',
56+
key: 'expectedReplicaCount',
57+
render: (expectedReplicaCount: number, record: Container) => {
58+
const actualReplicaCount = record.actualReplicaCount;
59+
return (
60+
<span>
61+
{actualReplicaCount} / {expectedReplicaCount}
62+
</span>
63+
);
64+
}
65+
},
66+
{
67+
title: 'Datanodes',
68+
dataIndex: 'replicas',
69+
key: 'replicas',
70+
render: (replicas: ContainerReplica[]) => {
71+
const renderDatanodes = (replicas: ContainerReplica[]) => {
72+
return replicas?.map((replica: any, idx: number) => (
73+
<div key={idx} className='datanode-container-v2'>
74+
<NodeIndexOutlined /> {replica.datanodeHost}
75+
</div>
76+
))
77+
}
78+
79+
return (
80+
<Popover
81+
content={renderDatanodes(replicas)}
82+
title='Datanodes'
83+
placement='bottomRight'
84+
trigger='hover'>
85+
<strong>{replicas.length}</strong> datanodes
86+
</Popover>
87+
)
88+
}
89+
},
90+
{
91+
title: 'Pipeline ID',
92+
dataIndex: 'pipelineID',
93+
key: 'pipelineID'
94+
},
95+
{
96+
title: 'Unhealthy Since',
97+
dataIndex: 'unhealthySince',
98+
key: 'unhealthySince',
99+
render: (unhealthySince: number) => getFormattedTime(unhealthySince, 'lll'),
100+
sorter: (a: Container, b: Container) => a.unhealthySince - b.unhealthySince
101+
}
102+
];
103+
104+
const KEY_TABLE_COLUMNS: ColumnsType<KeyResponse> = [
105+
{
106+
title: 'Volume',
107+
dataIndex: 'Volume',
108+
key: 'Volume'
109+
},
110+
{
111+
title: 'Bucket',
112+
dataIndex: 'Bucket',
113+
key: 'Bucket'
114+
},
115+
{
116+
title: 'Key',
117+
dataIndex: 'Key',
118+
key: 'Key'
119+
},
120+
{
121+
title: 'Size',
122+
dataIndex: 'DataSize',
123+
key: 'DataSize',
124+
render: (dataSize: number) => <div>{size(dataSize)}</div>
125+
},
126+
{
127+
title: 'Date Created',
128+
dataIndex: 'CreationTime',
129+
key: 'CreationTime',
130+
render: (date: string) => getFormattedTime(date, 'lll')
131+
},
132+
{
133+
title: 'Date Modified',
134+
dataIndex: 'ModificationTime',
135+
key: 'ModificationTime',
136+
render: (date: string) => getFormattedTime(date, 'lll')
137+
},
138+
{
139+
title: 'Path',
140+
dataIndex: 'CompletePath',
141+
key: 'path'
142+
}
143+
];
144+
145+
const ContainerTable: React.FC<ContainerTableProps> = ({
146+
data,
147+
loading,
148+
selectedColumns,
149+
expandedRow,
150+
expandedRowSetter,
151+
searchColumn = 'containerID',
152+
searchTerm = ''
153+
}) => {
154+
155+
const cancelSignal = useRef<AbortController>();
156+
157+
function filterSelectedColumns() {
158+
const columnKeys = selectedColumns.map((column) => column.value);
159+
return COLUMNS.filter(
160+
(column) => columnKeys.indexOf(column.key as string) >= 0
161+
);
162+
}
163+
164+
function loadRowData(containerID: number) {
165+
const { request, controller } = AxiosGetHelper(
166+
`/api/v1/containers/${containerID}/keys`,
167+
cancelSignal.current
168+
);
169+
cancelSignal.current = controller;
170+
171+
request.then(response => {
172+
const containerKeysResponse: ContainerKeysResponse = response.data;
173+
expandedRowSetter({
174+
...expandedRow,
175+
[containerID]: {
176+
...expandedRow[containerID],
177+
loading: false,
178+
dataSource: containerKeysResponse.keys,
179+
totalCount: containerKeysResponse.totalCount
180+
}
181+
});
182+
}).catch(error => {
183+
expandedRowSetter({
184+
...expandedRow,
185+
[containerID]: {
186+
...expandedRow[containerID],
187+
loading: false
188+
}
189+
});
190+
showDataFetchError((error as AxiosError).toString());
191+
});
192+
}
193+
194+
function getFilteredData(data: Container[]) {
195+
196+
return data?.filter(
197+
(container: Container) => {
198+
return (searchColumn === 'containerID')
199+
? container[searchColumn].toString().includes(searchTerm)
200+
: container[searchColumn].includes(searchTerm)
201+
}
202+
) ?? [];
203+
}
204+
205+
function onRowExpandClick(expanded: boolean, record: Container) {
206+
if (expanded) {
207+
loadRowData(record.containerID);
208+
}
209+
else {
210+
cancelSignal.current && cancelSignal.current.abort();
211+
}
212+
}
213+
214+
function expandedRowRender(record: Container) {
215+
const containerId = record.containerID
216+
const containerKeys: ExpandedRowState = expandedRow[containerId];
217+
const dataSource = containerKeys?.dataSource ?? [];
218+
const paginationConfig: TablePaginationConfig = {
219+
showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} Keys`
220+
}
221+
222+
return (
223+
<Table
224+
loading={containerKeys?.loading ?? true}
225+
dataSource={dataSource}
226+
columns={KEY_TABLE_COLUMNS}
227+
pagination={paginationConfig}
228+
rowKey={(record: KeyResponse) => `${record.Volume}/${record.Bucket}/${record.Key}`}
229+
locale={{ filterTitle: '' }} />
230+
)
231+
};
232+
233+
const paginationConfig: TablePaginationConfig = {
234+
showTotal: (total: number, range) => (
235+
`${range[0]}-${range[1]} of ${total} Containers`
236+
),
237+
showSizeChanger: true
238+
};
239+
240+
return (
241+
<div>
242+
<Table
243+
rowKey='containerID'
244+
dataSource={getFilteredData(data)}
245+
columns={filterSelectedColumns()}
246+
loading={loading}
247+
pagination={paginationConfig}
248+
scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
249+
locale={{ filterTitle: '' }}
250+
expandable={{
251+
expandRowByClick: true,
252+
expandedRowRender: expandedRowRender,
253+
onExpand: onRowExpandClick
254+
}} />
255+
</div>
256+
);
257+
}
258+
259+
export default ContainerTable;

hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
2626
import Search from '@/v2/components/search/search';
2727
import MultiSelect from '@/v2/components/select/multiSelect';
2828
import SingleSelect, { Option } from '@/v2/components/select/singleSelect';
29+
import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
2930

3031
import { AutoReloadHelper } from '@/utils/autoReloadHelper';
3132
import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper";
@@ -39,7 +40,6 @@ import {
3940
} from '@/v2/types/bucket.types';
4041

4142
import './buckets.less';
42-
import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
4343

4444

4545
const LIMIT_OPTIONS: Option[] = [

0 commit comments

Comments
 (0)