From 70fb5d62ff0512a2616f209794f76908ad0ac215 Mon Sep 17 00:00:00 2001 From: HuangTao Date: Sun, 19 Jul 2020 02:14:08 +0800 Subject: [PATCH 1/3] HDDS-3984. Support filter and search the columns in recon UI --- .../components/columnSearch/columnSearch.tsx | 74 +++++++++++++++++++ .../src/types/datanode.types.tsx | 4 +- .../src/views/datanodes/datanodes.tsx | 29 +++++++- .../src/views/pipelines/pipelines.tsx | 30 +++++++- 4 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx new file mode 100644 index 000000000000..c0f3208417d6 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx @@ -0,0 +1,74 @@ +/* + * 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 {Input, Button, Icon} from 'antd'; + +class ColumnSearch extends React.Component { + getColumnSearchProps = (dataIndex: string) => ({ + filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => ( +
+ { + this.searchInput = node; + }} + placeholder={`Search ${dataIndex}`} + value={selectedKeys[0]} + style={{width: 188, marginBottom: 8, display: 'block'}} + onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} + onPressEnter={() => this.handleSearch(selectedKeys, confirm)} + /> + + +
+ ), + filterIcon: filtered => ( + + ), + onFilter: (value, record) => + record[dataIndex] + .toString() + .toLowerCase() + .includes(value.toLowerCase()), + onFilterDropdownVisibleChange: visible => { + if (visible) { + setTimeout(() => this.searchInput.select()); + } + } + }); + + handleSearch = (selectedKeys, confirm) => { + confirm(); + }; + + handleReset = clearFilters => { + clearFilters(); + }; +} + +export {ColumnSearch}; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/types/datanode.types.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/types/datanode.types.tsx index ba8336b678c3..e9cb16820814 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/types/datanode.types.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/types/datanode.types.tsx @@ -16,7 +16,9 @@ * limitations under the License. */ -export type DatanodeStatus = 'HEALTHY' | 'STALE' | 'DEAD' | 'DECOMMISSIONING' | 'DECOMMISSIONED'; +export const DatanodeStatusList = ['HEALTHY', 'STALE', 'DEAD', 'DECOMMISSIONING', 'DECOMMISSIONED'] as const; +type DatanodeStatusTuple = typeof DatanodeStatusList; +export type DatanodeStatus = DatanodeStatusTuple[number]; // 'HEALTHY' | 'STALE' | 'DEAD' | 'DECOMMISSIONING' | 'DECOMMISSIONED'; export interface IStorageReport { capacity: number; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx index fada1f43c35a..e68c2fb8b916 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx @@ -23,13 +23,14 @@ import {PaginationConfig} from 'antd/lib/pagination'; import moment from 'moment'; import {ReplicationIcon} from 'utils/themeIcons'; import StorageBar from 'components/storageBar/storageBar'; -import {DatanodeStatus, IStorageReport} from 'types/datanode.types'; +import {DatanodeStatus, DatanodeStatusList, IStorageReport} from 'types/datanode.types'; import './datanodes.less'; import {AutoReloadHelper} from 'utils/autoReloadHelper'; import AutoReloadPanel from 'components/autoReloadPanel/autoReloadPanel'; import {MultiSelect, IOption} from 'components/multiSelect/multiSelect'; import {ActionMeta, ValueType} from 'react-select'; import {showDataFetchError} from 'utils/common'; +import {ColumnSearch} from 'components/columnSearch/columnSearch'; interface IDatanodeResponse { hostname: string; @@ -98,6 +99,9 @@ const COLUMNS = [ dataIndex: 'state', key: 'state', isVisible: true, + filterMultiple: true, + filters: DatanodeStatusList.map(status => ({text: status, value: status})), + onFilter: (value: DatanodeStatus, record: IDatanode) => record.state === value, render: (text: DatanodeStatus) => renderDatanodeStatus(text), sorter: (a: IDatanode, b: IDatanode) => a.state.localeCompare(b.state) }, @@ -106,6 +110,7 @@ const COLUMNS = [ dataIndex: 'uuid', key: 'uuid', isVisible: true, + isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.uuid.localeCompare(b.uuid), defaultSortOrder: 'ascend' as const }, @@ -114,6 +119,7 @@ const COLUMNS = [ dataIndex: 'hostname', key: 'hostname', isVisible: true, + isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.hostname.localeCompare(b.hostname), defaultSortOrder: 'ascend' as const }, @@ -173,6 +179,7 @@ const COLUMNS = [ dataIndex: 'leaderCount', key: 'leaderCount', isVisible: true, + isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.leaderCount - b.leaderCount }, { @@ -180,6 +187,7 @@ const COLUMNS = [ dataIndex: 'containers', key: 'containers', isVisible: true, + isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.containers - b.containers }, { @@ -187,6 +195,7 @@ const COLUMNS = [ dataIndex: 'version', key: 'version', isVisible: false, + isSearchable: true, sorter: (a: IDatanode, b: IDatanode) => a.version.localeCompare(b.version), defaultSortOrder: 'ascend' as const }, @@ -330,9 +339,21 @@ export class Datanodes extends React.Component, IDatanode
- selectedColumns.some(e => e.value === column.key) - )} + columns={COLUMNS.reduce((filtered, column) => { + if (selectedColumns.some(e => e.value === column.key)) { + if (column.isSearchable) { + const newColumn = { + ...column, + ...new ColumnSearch({}).getColumnSearchProps(column.dataIndex) + }; + filtered.push(newColumn); + } else { + filtered.push(column); + } + } + + return filtered; + }, [])} loading={loading} pagination={paginationConfig} rowKey='hostname' diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx index b898818cd234..0cd09ae2a464 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx @@ -28,9 +28,12 @@ import {AutoReloadHelper} from 'utils/autoReloadHelper'; import AutoReloadPanel from 'components/autoReloadPanel/autoReloadPanel'; import {showDataFetchError} from 'utils/common'; import {IAxiosResponse} from 'types/axios.types'; +import {ColumnSearch} from 'components/columnSearch/columnSearch'; const {TabPane} = Tabs; -export type PipelineStatus = 'active' | 'inactive'; +const PipelineStatusList = ['OPEN', 'CLOSING', 'QUASI_CLOSED', 'CLOSED', 'UNHEALTHY', 'INVALID', 'DELETED'] as const; +type PipelineStatusTuple = typeof PipelineStatusList; +export type PipelineStatus = PipelineStatusTuple[number]; // 'OPEN' | 'CLOSING' | 'QUASI_CLOSED' | 'CLOSED' | 'UNHEALTHY' | 'INVALID' | 'DELETED'; interface IPipelineResponse { pipelineId: string; @@ -62,6 +65,7 @@ const COLUMNS = [ title: 'Pipeline ID', dataIndex: 'pipelineId', key: 'pipelineId', + isSearchable: true, sorter: (a: IPipelineResponse, b: IPipelineResponse) => a.pipelineId.localeCompare(b.pipelineId) }, { @@ -89,24 +93,30 @@ const COLUMNS = [ title: 'Status', dataIndex: 'status', key: 'status', + filterMultiple: true, + filters: PipelineStatusList.map(status => ({text: status, value: status})), + onFilter: (value: PipelineStatus, record: IPipelineResponse) => record.status === value, sorter: (a: IPipelineResponse, b: IPipelineResponse) => a.status.localeCompare(b.status) }, { title: 'Containers', dataIndex: 'containers', key: 'containers', + isSearchable: true, sorter: (a: IPipelineResponse, b: IPipelineResponse) => a.containers - b.containers }, { title: 'Datanodes', dataIndex: 'datanodes', key: 'datanodes', + isSearchable: true, render: (datanodes: string[]) =>
{datanodes.map(datanode =>
{datanode}
)}
}, { title: 'Leader', dataIndex: 'leaderNode', key: 'leaderNode', + isSearchable: true, sorter: (a: IPipelineResponse, b: IPipelineResponse) => a.leaderNode.localeCompare(b.leaderNode) }, { @@ -128,6 +138,7 @@ const COLUMNS = [ title: 'No. of Elections', dataIndex: 'leaderElections', key: 'leaderElections', + isSearchable: true, sorter: (a: IPipelineResponse, b: IPipelineResponse) => a.leaderElections - b.leaderElections } ]; @@ -205,7 +216,22 @@ export class Pipelines extends React.Component, IPipeline
-
+
((filtered, column) => { + if (column.isSearchable) { + const newColumn = { + ...column, + ...new ColumnSearch({}).getColumnSearchProps(column.dataIndex) + }; + filtered.push(newColumn); + } else { + filtered.push(column); + } + + return filtered; + }, [])} + loading={activeLoading} pagination={paginationConfig} rowKey='pipelineId'/> From bf2a82fc1a3862ba884cbd7b66515a1750842d9b Mon Sep 17 00:00:00 2001 From: HuangTao Date: Mon, 20 Jul 2020 00:26:29 +0800 Subject: [PATCH 2/3] HDDS-3984. fix code review --- .../src/utils/columnSearch.less | 36 ++++++++++++ .../columnSearch => utils}/columnSearch.tsx | 58 +++++++++++++------ .../src/views/datanodes/datanodes.tsx | 4 +- .../src/views/pipelines/pipelines.tsx | 4 +- 4 files changed, 79 insertions(+), 23 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.less rename hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/{components/columnSearch => utils}/columnSearch.tsx (53%) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.less new file mode 100644 index 000000000000..4c7013a1304d --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.less @@ -0,0 +1,36 @@ +/* + * 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. + */ + +.column-search-container { + padding: 8px; + + .input-block { + width: 188px; + margin-bottom: 8px; + display: block; + } + + .search-button { + width: 90px; + margin-right: 8px; + } + + .reset-button { + width: 90px; + } +} diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.tsx similarity index 53% rename from hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx rename to hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.tsx index c0f3208417d6..319bfd29e013 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/columnSearch/columnSearch.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/columnSearch.tsx @@ -18,55 +18,75 @@ import React from 'react'; import {Input, Button, Icon} from 'antd'; +import './columnSearch.less'; + +class ColumnSearch extends React.PureComponent { + searchInput: Input | null = null; -class ColumnSearch extends React.Component { getColumnSearchProps = (dataIndex: string) => ({ - filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => ( -
+ filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }: { + setSelectedKeys: (keys: string[]) => void; + selectedKeys: string[]; + confirm: () => void; + clearFilters: () => void; + }) => ( +
{ this.searchInput = node; }} + className='input-block' placeholder={`Search ${dataIndex}`} value={selectedKeys[0]} - style={{width: 188, marginBottom: 8, display: 'block'}} - onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} - onPressEnter={() => this.handleSearch(selectedKeys, confirm)} + onChange={e => + setSelectedKeys(e.target.value ? [e.target.value] : [])} + onPressEnter={() => this.handleSearch(confirm)} /> -
), - filterIcon: filtered => ( + filterIcon: (filtered: boolean) => ( ), - onFilter: (value, record) => - record[dataIndex] - .toString() - .toLowerCase() - .includes(value.toLowerCase()), - onFilterDropdownVisibleChange: visible => { + onFilter: (value: string, record: any) => + record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()), + onFilterDropdownVisibleChange: (visible: boolean) => { if (visible) { - setTimeout(() => this.searchInput.select()); + setTimeout(() => { + if (this.searchInput) { + this.searchInput.select(); + } + }); } } }); - handleSearch = (selectedKeys, confirm) => { + handleSearch = (confirm: () => void) => { confirm(); }; - handleReset = clearFilters => { + handleReset = (clearFilters: () => void) => { clearFilters(); }; } diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx index e68c2fb8b916..ceb7b51ac05c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/datanodes/datanodes.tsx @@ -30,7 +30,7 @@ import AutoReloadPanel from 'components/autoReloadPanel/autoReloadPanel'; import {MultiSelect, IOption} from 'components/multiSelect/multiSelect'; import {ActionMeta, ValueType} from 'react-select'; import {showDataFetchError} from 'utils/common'; -import {ColumnSearch} from 'components/columnSearch/columnSearch'; +import {ColumnSearch} from 'utils/columnSearch'; interface IDatanodeResponse { hostname: string; @@ -344,7 +344,7 @@ export class Datanodes extends React.Component, IDatanode if (column.isSearchable) { const newColumn = { ...column, - ...new ColumnSearch({}).getColumnSearchProps(column.dataIndex) + ...new ColumnSearch(column).getColumnSearchProps(column.dataIndex) }; filtered.push(newColumn); } else { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx index 0cd09ae2a464..342a8bd35f3f 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/pipelines/pipelines.tsx @@ -28,7 +28,7 @@ import {AutoReloadHelper} from 'utils/autoReloadHelper'; import AutoReloadPanel from 'components/autoReloadPanel/autoReloadPanel'; import {showDataFetchError} from 'utils/common'; import {IAxiosResponse} from 'types/axios.types'; -import {ColumnSearch} from 'components/columnSearch/columnSearch'; +import {ColumnSearch} from 'utils/columnSearch'; const {TabPane} = Tabs; const PipelineStatusList = ['OPEN', 'CLOSING', 'QUASI_CLOSED', 'CLOSED', 'UNHEALTHY', 'INVALID', 'DELETED'] as const; @@ -222,7 +222,7 @@ export class Pipelines extends React.Component, IPipeline if (column.isSearchable) { const newColumn = { ...column, - ...new ColumnSearch({}).getColumnSearchProps(column.dataIndex) + ...new ColumnSearch(column).getColumnSearchProps(column.dataIndex) }; filtered.push(newColumn); } else { From 89867e31111ddc3bfd872af9a9bfce18fe6c088e Mon Sep 17 00:00:00 2001 From: "Doroszlai, Attila" Date: Mon, 20 Jul 2020 06:24:51 +0200 Subject: [PATCH 3/3] trigger new CI check