From 3469dbc82d04b71a4ec74353c50fbbc547558db0 Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Fri, 6 Sep 2024 00:31:38 +0530 Subject: [PATCH 1/7] HDDS-11157. Improve Datanodes page UI --- .../src/utils/axiosRequestHelper.tsx | 2 +- .../decommissioningSummary.tsx | 114 ++++ .../v2/components/storageBar/storageBar.less | 45 ++ .../v2/components/storageBar/storageBar.tsx | 49 +- .../src/v2/pages/buckets/buckets.tsx | 16 +- .../src/v2/pages/datanodes/datanodes.less | 52 ++ .../src/v2/pages/datanodes/datanodes.tsx | 582 ++++++++++++++++++ .../src/v2/pages/volumes/volumes.tsx | 2 +- .../ozone-recon-web/src/v2/routes-v2.tsx | 5 + .../src/v2/types/bucket.types.ts | 1 - .../src/v2/types/datanode.types.ts | 156 +++++ .../src/v2/types/pipelines.types.ts | 61 ++ 12 files changed, 1050 insertions(+), 35 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.less create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.less create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/datanode.types.ts create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/axiosRequestHelper.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/axiosRequestHelper.tsx index 8fbe403dc375..53a76d83f19c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/axiosRequestHelper.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/axiosRequestHelper.tsx @@ -37,7 +37,7 @@ export const AxiosGetHelper = ( export const AxiosPutHelper = ( url: string, data: any = {}, - controller: AbortController, + controller: AbortController | undefined, message: string = '', //optional ): { request: Promise>; controller: AbortController } => { controller && controller.abort(message); diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx new file mode 100644 index 000000000000..7d01ce177293 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx @@ -0,0 +1,114 @@ +/* + * 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, { useEffect } from 'react'; +import { AxiosError } from 'axios'; +import { Descriptions, Popover, Result } from 'antd'; +import { Datanode, SummaryData } from '@/v2/types/datanode.types'; +import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper'; +import { showDataFetchError } from '@/utils/common'; + +type DecommisioningSummaryProps = { + uuid: string; +} + +type DecommisioningSummaryState = { + loading: boolean; + summaryData: SummaryData | Record; +}; + +const DecommissionSummary: React.FC = ({ + uuid = '' +}) => { + const [state, setState] = React.useState({ + summaryData: {}, + loading: false + }); + const cancelSignal = React.useRef(); + + async function fetchDecommissionSummary(selectedUuid: string) { + setState({ + ...state, + loading: true + }); + try { + const { request, controller } = AxiosGetHelper( + `/api/v1/datanodes/decommission/info/datanode?uuid=${selectedUuid}`, + cancelSignal.current + ); + cancelSignal.current = controller; + const datanodesInfoResponse = await request; + setState({ + ...state, + loading: false, + summaryData: datanodesInfoResponse?.data?.DatanodesDecommissionInfo[0] ?? {} + }); + } catch (error) { + setState({ + ...state, + loading: false, + summaryData: {} + }); + showDataFetchError((error as AxiosError).toString()); + } + } + + useEffect(() => { + fetchDecommissionSummary(uuid); + return (() => { + cancelRequests([cancelSignal.current!]); + }) + }, []); + + let content = ( + + ); + + const { summaryData } = state; + if (summaryData?.datanodeDetails && summaryData?.metrics && summaryData?.containers) { + const { + datanodeDetails: { uuid, networkLocation, ipAddress, hostName }, + containers: { UnderReplicated, UnClosed }, + metrics: { decommissionStartTime, numOfUnclosedPipelines, numOfUnclosedContainers, numOfUnderReplicatedContainers } + } = summaryData as SummaryData; + content = ( + + {uuid} + ({networkLocation}/{ipAddress}/{hostName}) + {decommissionStartTime} + {numOfUnclosedPipelines} + {numOfUnclosedContainers} + {numOfUnderReplicatedContainers} + {UnderReplicated} + {UnClosed} + + ); + } + // Need to check summarydata is not empty + return ( + +  {uuid} + + ); + +} + +export default DecommissionSummary; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.less new file mode 100644 index 000000000000..798287366c3f --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.less @@ -0,0 +1,45 @@ +/* +* 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. +*/ + +@progress-gray: #d0d0d0; +@progress-light-blue: rgb(230, 235, 248); +@progress-blue: #1890ff; +@progress-green: #52c41a; +@progress-red: #FFA39E; + +.storage-cell-container-v2 { + .capacity-bar-v2 { + font-size: 1em; + } +} + +.ozone-used-bg-v2 { + color: @progress-green !important; +} + +.non-ozone-used-bg-v2 { + color: @progress-blue !important; +} + +.remaining-bg-v2 { + color: @progress-light-blue !important; +} + +.committed-bg-v2 { + color: @progress-red !important; +} diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.tsx index 591b0088b04b..fd6dd8dfe9bc 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/storageBar/storageBar.tsx @@ -20,72 +20,73 @@ import React from 'react'; import { Progress } from 'antd'; import filesize from 'filesize'; import Icon from '@ant-design/icons'; -import { withRouter } from 'react-router-dom'; import Tooltip from 'antd/lib/tooltip'; import { FilledIcon } from '@/utils/themeIcons'; import { getCapacityPercent } from '@/utils/common'; import type { StorageReport } from '@/v2/types/overview.types'; +import './storageBar.less'; + const size = filesize.partial({ standard: 'iec', round: 1 }); type StorageReportProps = { - showMeta: boolean; + showMeta?: boolean; + strokeWidth?: number; } & StorageReport -const StorageBar = (props: StorageReportProps = { - capacity: 0, - used: 0, - remaining: 0, - committed: 0, - showMeta: true, +const StorageBar: React.FC = ({ + capacity = 0, + used = 0, + remaining = 0, + committed = 0, + showMeta = false, + strokeWidth = 3 }) => { - const { capacity, used, remaining, committed, showMeta } = props; const nonOzoneUsed = capacity - remaining - used; const totalUsed = capacity - remaining; const tooltip = ( <>
- + Ozone Used ({size(used)})
- + Non Ozone Used ({size(nonOzoneUsed)})
- + Remaining ({size(remaining)})
- + Container Pre-allocated ({size(committed)})
); - const metaElement = (showMeta) ? ( -
- {size(used + nonOzoneUsed)} / {size(capacity)} -
- ) : <>; - return ( -
- - {metaElement} + + {(showMeta) && +
+ {size(used + nonOzoneUsed)} / {size(capacity)} +
+ } + className='capacity-bar-v2' strokeWidth={strokeWidth} />
-
); } diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx index bd8950e54c87..c25706d6c3a1 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import moment from 'moment'; import { Table, Tag } from 'antd'; import { @@ -44,7 +44,7 @@ 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 { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper"; import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common'; import { useDebounce } from '@/v2/hooks/debounce.hook'; @@ -269,7 +269,7 @@ function getFilteredBuckets( const Buckets: React.FC<{}> = () => { - let cancelSignal: AbortController; + const cancelSignal = useRef(); const [state, setState] = useState({ totalCount: 0, @@ -383,11 +383,11 @@ const Buckets: React.FC<{}> = () => { setLoading(true); const { request, controller } = AxiosGetHelper( '/api/v1/buckets', - cancelSignal, + cancelSignal.current, '', { limit: selectedLimit.value } ); - cancelSignal = controller; + cancelSignal.current = controller; request.then(response => { const bucketsResponse: BucketResponse = response.data; const totalCount = bucketsResponse.totalCount; @@ -443,7 +443,7 @@ const Buckets: React.FC<{}> = () => { }); } - let autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData); + const autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData); useEffect(() => { autoReloadHelper.startPolling(); @@ -459,7 +459,7 @@ const Buckets: React.FC<{}> = () => { return (() => { autoReloadHelper.stopPolling(); - cancelSignal && cancelSignal.abort(); + cancelRequests([cancelSignal.current!]); }) }, []); @@ -544,7 +544,7 @@ const Buckets: React.FC<{}> = () => { loading={loading} rowKey='volume' pagination={paginationConfig} - scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }} + scroll={{ x: 'max-content', y: 400, scrollToFirstRowOnChange: true }} locale={{ filterTitle: '' }} /> diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.less new file mode 100644 index 000000000000..a1eee3852106 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.less @@ -0,0 +1,52 @@ +/* +* 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. +*/ + +.content-div { + min-height: unset; + + .table-header-section { + display: flex; + justify-content: space-between; + align-items: center; + + .table-filter-section { + font-size: 14px; + font-weight: normal; + display: flex; + column-gap: 8px; + padding: 16px 8px; + align-items: center; + } + } + + .tag-block { + display: flex; + column-gap: 8px; + padding: 0px 8px 16px 8px; + } +} + +.pipeline-container-v2 { + padding: 6px 0px; +} + +.decommission-summary-result { + .ant-result-title { + font-size: 15px; + } +} \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx new file mode 100644 index 000000000000..fb3bfd448a8a --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx @@ -0,0 +1,582 @@ +/* + * 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, { + useEffect, + useRef, + useState +} from 'react'; +import moment from 'moment'; +import { AxiosError } from 'axios'; +import { + Table, + Tooltip, + Popover, + Button, + Modal +} from 'antd'; +import { + ColumnsType, + TablePaginationConfig +} from 'antd/es/table'; +import { + CheckCircleFilled, + CloseCircleFilled, + DeleteOutlined, + HourglassFilled, + InfoCircleOutlined, + WarningFilled, +} from '@ant-design/icons'; +import { ValueType } from 'react-select'; + +import Search from '@/v2/components/search/search'; +import StorageBar from '@/v2/components/storageBar/storageBar'; +import MultiSelect, { Option } from '@/v2/components/select/multiSelect'; +import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel'; +import { showDataFetchError } from '@/utils/common'; +import { ReplicationIcon } from '@/utils/themeIcons'; +import { AutoReloadHelper } from '@/utils/autoReloadHelper'; +import { + AxiosGetHelper, + AxiosPutHelper, + cancelRequests +} from '@/utils/axiosRequestHelper'; + +import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { + Datanode, + DatanodeDecomissionInfo, + DatanodeOpState, + DatanodeOpStateList, + DatanodeResponse, + DatanodesResponse, + DatanodesState, + DatanodeState, + DatanodeStateList +} from '@/v2/types/datanode.types'; +import { Pipeline } from '@/v2/types/pipelines.types'; + +import './datanodes.less' +import DecommissionSummary from '@/v2/components/decommissioningSummary/decommissioningSummary'; +import { ColumnTitleProps } from 'antd/lib/table/interface'; +import { TableRowSelection } from 'antd/es/table/interface'; + +moment.updateLocale('en', { + relativeTime: { + past: '%s ago', + s: '%ds', + m: '1min', + mm: '%dmins', + h: '1hr', + hh: '%dhrs', + d: '1d', + dd: '%dd', + M: '1m', + MM: '%dm', + y: '1y', + yy: '%dy' + } +}); + +const headerIconStyles: React.CSSProperties = { + display: 'flex', + alignItems: 'center' +} + +const renderDatanodeState = (state: DatanodeState) => { + const stateIconMap = { + HEALTHY: , + STALE: , + DEAD: + }; + const icon = state in stateIconMap ? stateIconMap[state] : ''; + return {icon} {state}; +}; + +const renderDatanodeOpState = (opState: DatanodeOpState) => { + const opStateIconMap = { + IN_SERVICE: , + DECOMMISSIONING: , + DECOMMISSIONED: , + ENTERING_MAINTENANCE: , + IN_MAINTENANCE: + }; + const icon = opState in opStateIconMap ? opStateIconMap[opState] : ''; + return {icon} {opState}; +}; + +const getTimeDiffFromTimestamp = (timestamp: number): string => { + const timestampDate = new Date(timestamp); + return moment(timestampDate).fromNow(); +} + +const COLUMNS: ColumnsType = [ + { + title: 'Hostname', + dataIndex: 'hostname', + key: 'hostname', + sorter: (a: Datanode, b: Datanode) => a.hostname.localeCompare( + b.hostname, undefined, { numeric: true } + ), + defaultSortOrder: 'ascend' as const + }, + { + title: 'State', + dataIndex: 'state', + key: 'state', + filterMultiple: true, + filters: DatanodeStateList.map(state => ({ text: state, value: state })), + onFilter: (value, record: Datanode) => record.state === value, + render: (text: DatanodeState) => renderDatanodeState(text), + sorter: (a: Datanode, b: Datanode) => a.state.localeCompare(b.state) + }, + { + title: 'Operational State', + dataIndex: 'opState', + key: 'opState', + filterMultiple: true, + filters: DatanodeOpStateList.map(state => ({ text: state, value: state })), + onFilter: (value, record: Datanode) => record.opState === value, + render: (text: DatanodeOpState) => renderDatanodeOpState(text), + sorter: (a: Datanode, b: Datanode) => a.opState.localeCompare(b.opState) + }, + { + title: 'UUID', + dataIndex: 'uuid', + key: 'uuid', + sorter: (a: Datanode, b: Datanode) => a.uuid.localeCompare(b.uuid), + defaultSortOrder: 'ascend' as const, + render: (uuid: string, record: Datanode) => { + return ( + //1. Compare Decommission Api's UUID with all UUID in table and show Decommission Summary + (decommissionUuids && decommissionUuids.includes(record.uuid) && record.opState !== 'DECOMMISSIONED') ? + : {uuid} + ); + } + }, + { + title: 'Storage Capacity', + dataIndex: 'storageUsed', + key: 'storageUsed', + sorter: (a: Datanode, b: Datanode) => a.storageRemaining - b.storageRemaining, + render: (_: string, record: Datanode) => ( + + ) + }, + { + title: 'Last Heartbeat', + dataIndex: 'lastHeartbeat', + key: 'lastHeartbeat', + sorter: (a: Datanode, b: Datanode) => moment(a.lastHeartbeat).unix() - moment(b.lastHeartbeat).unix(), + render: (heartbeat: number) => { + return heartbeat > 0 ? getTimeDiffFromTimestamp(heartbeat) : 'NA'; + } + }, + { + title: 'Pipeline ID(s)', + dataIndex: 'pipelines', + key: 'pipelines', + render: (pipelines: Pipeline[], record: Datanode) => { + const renderPipelineIds = (pipelineIds: Pipeline[]) => { + return pipelineIds?.map((pipeline: any, index: any) => ( +
+ + {pipeline.pipelineID} +
+ )) + } + + return ( + + {pipelines.length} pipelines + + ); + } + }, + { + title: () => ( + + Leader Count + + + + + ), + dataIndex: 'leaderCount', + key: 'leaderCount', + sorter: (a: Datanode, b: Datanode) => a.leaderCount - b.leaderCount + }, + { + title: 'Containers', + dataIndex: 'containers', + key: 'containers', + sorter: (a: Datanode, b: Datanode) => a.containers - b.containers + }, + { + title: () => ( + + Open Container + + + + + ), + dataIndex: 'openContainers', + key: 'openContainers', + sorter: (a: Datanode, b: Datanode) => a.openContainers - b.openContainers + }, + { + title: 'Version', + dataIndex: 'version', + key: 'version', + sorter: (a: Datanode, b: Datanode) => a.version.localeCompare(b.version), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Setup Time', + dataIndex: 'setupTime', + key: 'setupTime', + sorter: (a: Datanode, b: Datanode) => a.setupTime - b.setupTime, + render: (uptime: number) => { + return uptime > 0 ? moment(uptime).format('ll LTS') : 'NA'; + } + }, + { + title: 'Revision', + dataIndex: 'revision', + key: 'revision', + sorter: (a: Datanode, b: Datanode) => a.revision.localeCompare(b.revision), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Build Date', + dataIndex: 'buildDate', + key: 'buildDate', + sorter: (a: Datanode, b: Datanode) => a.buildDate.localeCompare(b.buildDate), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Network Location', + dataIndex: 'networkLocation', + key: 'networkLocation', + sorter: (a: Datanode, b: Datanode) => a.networkLocation.localeCompare(b.networkLocation), + defaultSortOrder: 'ascend' as const + } +]; + +const defaultColumns = COLUMNS.map(column => ({ + label: (typeof column.title === 'string') + ? column.title + : (column.title as Function)().props.children[0], + value: column.key as string +})); + +const SearchableColumnOpts = [{ + label: 'Hostname', + value: 'hostname' +}, { + label: 'UUID', + value: 'uuid' +}, { + label: 'Version', + value: 'version' +}, { + label: 'Revision', + value: 'revision' +}]; + +let decommissionUuids: string | string[] = []; +const COLUMN_UPDATE_DECOMMISSIONING = 'DECOMMISSIONING'; + +const Datanodes: React.FC<{}> = () => { + + const cancelSignal = useRef(); + const cancelDecommissionSignal = useRef(); + + const [state, setState] = useState({ + lastUpdated: 0, + columnOptions: defaultColumns, + dataSource: [] + }); + const [loading, setLoading] = useState(false); + const [selectedColumns, setSelectedColumns] = useState(defaultColumns); + const [selectedRows, setSelectedRows] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [searchColumn, setSearchColumn] = useState<'hostname' | 'uuid' | 'version' | 'revision'>('hostname'); + const [modalOpen, setModalOpen] = useState(false); + + const debouncedSearch = useDebounce(searchTerm, 300); + + function handleColumnChange(selected: ValueType) { + setSelectedColumns(selected as Option[]); + } + + function filterSelectedColumns() { + const columnKeys = selectedColumns.map((column) => column.value); + return COLUMNS.filter( + (column) => columnKeys.indexOf(column.key as string) >= 0 + ); + } + + function getFilteredData(data: Datanode[]) { + return data.filter( + (datanode: Datanode) => datanode[searchColumn].includes(debouncedSearch) + ); + } + + async function loadDecommisionAPI() { + decommissionUuids = []; + const { request, controller } = await AxiosGetHelper( + '/api/v1/datanodes/decommission/info', + cancelDecommissionSignal.current + ); + cancelDecommissionSignal.current = controller; + return request + }; + + async function loadDataNodeAPI() { + const { request, controller } = await AxiosGetHelper( + '/api/v1/datanodes', + cancelSignal.current + ); + cancelSignal.current = controller; + return request; + }; + + async function removeDatanode(selectedRowKeys: string[]) { + setLoading(true); + const { request, controller } = await AxiosPutHelper( + '/api/v1/datanodes/remove', + selectedRowKeys, + cancelSignal.current + ); + cancelSignal.current = controller; + request.then(() => { + loadData(); + }).catch((error) => { + showDataFetchError(error.toString()); + }).finally(() => { + setLoading(false); + setSelectedRows([]); + }); + } + + const loadData = async () => { + setLoading(true); + // Need to call decommission API on each interval to get updated status + // before datanode API call to compare UUID's + // update 'Operation State' column in table manually before rendering + try { + let decomissionResponse = await loadDecommisionAPI(); + decommissionUuids = decomissionResponse.data?.DatanodesDecommissionInfo?.map( + (item: DatanodeDecomissionInfo) => item.datanodeDetails.uuid + ); + } catch (error) { + decommissionUuids = []; + showDataFetchError((error as AxiosError).toString()); + } + + try { + const datanodesAPIResponse = await loadDataNodeAPI(); + const datanodesResponse: DatanodesResponse = datanodesAPIResponse.data; + const datanodes: DatanodeResponse[] = datanodesResponse.datanodes; + const dataSource: Datanode[] = datanodes?.map( + (datanode) => ({ + hostname: datanode.hostname, + uuid: datanode.uuid, + state: datanode.state, + opState: (decommissionUuids?.includes(datanode.uuid) && datanode.opState !== 'DECOMMISSIONED') + ? COLUMN_UPDATE_DECOMMISSIONING + : datanode.opState, + lastHeartbeat: datanode.lastHeartbeat, + storageUsed: datanode.storageReport.used, + storageTotal: datanode.storageReport.capacity, + storageCommitted: datanode.storageReport.committed, + storageRemaining: datanode.storageReport.remaining, + pipelines: datanode.pipelines, + containers: datanode.containers, + openContainers: datanode.openContainers, + leaderCount: datanode.leaderCount, + version: datanode.version, + setupTime: datanode.setupTime, + revision: datanode.revision, + buildDate: datanode.buildDate, + networkLocation: datanode.networkLocation + }) + ); + setLoading(false); + setState({ + ...state, + dataSource: dataSource, + lastUpdated: Number(moment()) + }); + } catch (error) { + setLoading(false); + showDataFetchError((error as AxiosError).toString()) + } + } + + const autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData); + + useEffect(() => { + autoReloadHelper.startPolling(); + loadData(); + + return (() => { + autoReloadHelper.stopPolling(); + cancelRequests([ + cancelSignal.current!, + cancelDecommissionSignal.current! + ]); + }); + }, []); + + function isSelectable(record: Datanode) { + // Disable checkbox for any datanode which is not DEAD to prevent removal + return record.state !== 'DEAD' && true; + } + + function handleModalOk() { + setModalOpen(false); + removeDatanode(selectedRows as string[]) + }; + + function handleModalCancel() { + setModalOpen(false); + setSelectedRows([]); + }; + + const { dataSource, lastUpdated, columnOptions } = state; + + const rowSelection: TableRowSelection = { + selectedRowKeys: selectedRows, + onChange: (rows: React.Key[]) => { setSelectedRows(rows) }, + getCheckboxProps: (record: Datanode) => ({ + disabled: isSelectable(record) + }), + }; + + const paginationConfig: TablePaginationConfig = { + showTotal: (total: number, range) => ( + `${range[0]}-${range[1]} of ${total} Datanodes` + ), + showSizeChanger: true + }; + console.log(selectedRows); + + return ( + <> +
+ Datanodes + +
+
+
+
+
+ { }} + fixedColumn='hostname' + columnLength={columnOptions.length} /> + {selectedRows.length > 0 && + + } +
+ ) => setSearchTerm(e.target.value) + } + onChange={(value) => { + setSearchTerm(''); + setSearchColumn(value as 'hostname' | 'uuid' | 'version' | 'revision') + }} /> +
+
+ + + + + +
+ + Stop Tracking Datanode +
+ Are you sure, you want recon to stop tracking the selected {selectedRows.length} datanode(s)? +
+ + ); +} + +export default Datanodes; \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx index 605883caff94..92107346ce4c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx @@ -336,7 +336,7 @@ const Volumes: React.FC<{}> = () => { loading={loading} rowKey='volume' pagination={paginationConfig} - scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }} + scroll={{ x: 'max-content', y: 400, scrollToFirstRowOnChange: true }} locale={{ filterTitle: '' }} /> diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx index 8a37ef9c5104..53027d779019 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx @@ -20,6 +20,7 @@ import { lazy } from 'react'; const Overview = lazy(() => import('@/v2/pages/overview/overview')); const Volumes = lazy(() => import('@/v2/pages/volumes/volumes')) const Buckets = lazy(() => import('@/v2/pages/buckets/buckets')); +const Datanodes = lazy(() => import('@/v2/pages/datanodes/datanodes')); export const routesV2 = [ { @@ -33,5 +34,9 @@ export const routesV2 = [ { path: '/Buckets', component: Buckets + }, + { + path: '/Datanodes', + component: Datanodes } ]; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts index 5cfc89d85e61..08888efd33fe 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts @@ -17,7 +17,6 @@ */ import { Acl } from "@/v2/types/acl.types"; -import { Option } from "@/v2/components/select/singleSelect"; import { Option as MultiOption } from "@/v2/components/select/multiSelect"; // Corresponds to OzoneManagerProtocolProtos.StorageTypeProto diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/datanode.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/datanode.types.ts new file mode 100644 index 000000000000..921fb77dc32c --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/datanode.types.ts @@ -0,0 +1,156 @@ +/* + * 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 { Pipeline } from "@/v2/types/pipelines.types"; +import { StorageReport } from "@/v2/types/overview.types"; +import { Option as MultiOption } from "@/v2/components/select/multiSelect"; + +// Corresponds to HddsProtos.NodeState +export const DatanodeStateList = ['HEALTHY', 'STALE', 'DEAD'] as const; +type DatanodeStateType = typeof DatanodeStateList; +export type DatanodeState = DatanodeStateType[number]; + +// Corresponds to HddsProtos.NodeOperationalState +export const DatanodeOpStateList = [ + 'IN_SERVICE', + 'DECOMMISSIONING', + 'DECOMMISSIONED', + 'ENTERING_MAINTENANCE', + 'IN_MAINTENANCE' +] as const; +export type DatanodeOpState = typeof DatanodeOpStateList[number]; + +export type DatanodeResponse = { + hostname: string; + state: DatanodeState; + opState: DatanodeOpState; + lastHeartbeat: string; + storageReport: StorageReport; + pipelines: Pipeline[]; + containers: number; + openContainers: number; + leaderCount: number; + uuid: string; + version: string; + setupTime: number; + revision: string; + buildDate: string; + networkLocation: string; +} + +export type DatanodesResponse = { + totalCount: number; + datanodes: DatanodeResponse[]; +} + +export type Datanode = { + hostname: string; + state: DatanodeState; + opState: DatanodeOpState; + lastHeartbeat: string; + storageUsed: number; + storageTotal: number; + storageRemaining: number; + storageCommitted: number; + pipelines: Pipeline[]; + containers: number; + openContainers: number; + leaderCount: number; + uuid: string; + version: string; + setupTime: number; + revision: string; + buildDate: string; + networkLocation: string; +} + +export type DatanodeDetails = { + uuid: string; +} + +export type DatanodeDecomissionInfo = { + datanodeDetails: DatanodeDetails +} + +export type DatanodesState = { + dataSource: Datanode[]; + lastUpdated: number; + columnOptions: MultiOption[]; +} + +// Datanode Summary endpoint types +type summaryByteString = { + string: string; + bytes: { + validUtf8: boolean; + empty: boolean; + } +} + +type SummaryPort = { + name: string; + value: number; +} + +type SummaryDatanodeDetails = { + level: number; + parent: unknown | null; + cost: number; + uuid: string; + uuidString: string; + ipAddress: string; + hostName: string; + ports: SummaryPort; + certSerialId: null, + version: string | null; + setupTime: number; + revision: string | null; + buildDate: string; + persistedOpState: string; + persistedOpStateExpiryEpochSec: number; + initialVersion: number; + currentVersion: number; + decommissioned: boolean; + maintenance: boolean; + ipAddressAsByteString: summaryByteString; + hostNameAsByteString: summaryByteString; + networkName: string; + networkLocation: string; + networkFullPath: string; + numOfLeaves: number; + networkNameAsByteString: summaryByteString; + networkLocationAsByteString: summaryByteString +} + +type SummaryMetrics = { + decommissionStartTime: string; + numOfUnclosedPipelines: number; + numOfUnderReplicatedContainers: number; + numOfUnclosedContainers: number; +} + +type SummaryContainers = { + UnderReplicated: string[]; + UnClosed: string[]; +} + +export type SummaryData = { + datanodeDetails: SummaryDatanodeDetails; + metrics: SummaryMetrics; + containers: SummaryContainers; +} diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts new file mode 100644 index 000000000000..3cad9d8d79e0 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +const PipelineStatusList = [ + 'OPEN', + 'CLOSING', + 'QUASI_CLOSED', + 'CLOSED', + 'UNHEALTHY', + 'INVALID', + 'DELETED', + 'DORMANT' +] as const; +export type PipelineStatus = typeof PipelineStatusList[number]; + +export type Pipeline = { + pipelineID: string; + replicationType: string; + replicationFactor: string; + leaderNode: string; +} + +interface IPipelineResponse { + pipelineId: string; + status: PipelineStatus; + replicationType: string; + leaderNode: string; + datanodes: string[]; + lastLeaderElection: number; + duration: number; + leaderElections: number; + replicationFactor: string; + containers: number; +} + +interface IPipelinesResponse { + totalCount: number; + pipelines: IPipelineResponse[]; +} + +interface IPipelinesState { + activeLoading: boolean; + activeDataSource: IPipelineResponse[]; + activeTotalCount: number; + lastUpdated: number; +} \ No newline at end of file From 0ebfc6b216a42d5f549e535273389666a4617fad Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Fri, 6 Sep 2024 00:39:23 +0530 Subject: [PATCH 2/7] Restore y scroll --- .../recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx | 2 +- .../recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx | 2 +- .../recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx index c25706d6c3a1..30c744f5fb84 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx @@ -544,7 +544,7 @@ const Buckets: React.FC<{}> = () => { loading={loading} rowKey='volume' pagination={paginationConfig} - scroll={{ x: 'max-content', y: 400, scrollToFirstRowOnChange: true }} + scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }} locale={{ filterTitle: '' }} /> diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx index fb3bfd448a8a..8812504515a8 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx @@ -552,7 +552,7 @@ const Datanodes: React.FC<{}> = () => { loading={loading} rowKey='uuid' pagination={paginationConfig} - scroll={{ x: 'max-content', y: 400, scrollToFirstRowOnChange: true }} + scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }} locale={{ filterTitle: '' }} /> diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx index 92107346ce4c..605883caff94 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx @@ -336,7 +336,7 @@ const Volumes: React.FC<{}> = () => { loading={loading} rowKey='volume' pagination={paginationConfig} - scroll={{ x: 'max-content', y: 400, scrollToFirstRowOnChange: true }} + scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }} locale={{ filterTitle: '' }} /> From 8dd83d752efefb8809e58099dc73ba9536af0292 Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Fri, 6 Sep 2024 12:46:24 +0530 Subject: [PATCH 3/7] Fix pipelines type --- .../src/v2/types/pipelines.types.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts index 3cad9d8d79e0..d5889944f74c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts @@ -16,7 +16,7 @@ * limitations under the License. */ -const PipelineStatusList = [ +export const PipelineStatusList = [ 'OPEN', 'CLOSING', 'QUASI_CLOSED', @@ -35,7 +35,7 @@ export type Pipeline = { leaderNode: string; } -interface IPipelineResponse { +export type PipelineResponse = { pipelineId: string; status: PipelineStatus; replicationType: string; @@ -48,14 +48,14 @@ interface IPipelineResponse { containers: number; } -interface IPipelinesResponse { +export type PipelinesResponse = { totalCount: number; - pipelines: IPipelineResponse[]; + pipelines: PipelineResponse[]; } -interface IPipelinesState { +export type PipelinesState = { activeLoading: boolean; - activeDataSource: IPipelineResponse[]; + activeDataSource: PipelineResponse[]; activeTotalCount: number; lastUpdated: number; -} \ No newline at end of file +} From d9d12dfe4ef4378753d36c1e58f1c6238b85fdbd Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Tue, 10 Sep 2024 19:50:23 +0530 Subject: [PATCH 4/7] Refactored Summary popover component, removed console.log lines --- .../decommissioningSummary.tsx | 81 ++++++++++++------- .../src/v2/pages/datanodes/datanodes.tsx | 1 - 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx index 7d01ce177293..34e72b0889aa 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/decommissioningSummary/decommissioningSummary.tsx @@ -19,9 +19,10 @@ import React, { useEffect } from 'react'; import { AxiosError } from 'axios'; import { Descriptions, Popover, Result } from 'antd'; -import { Datanode, SummaryData } from '@/v2/types/datanode.types'; +import { SummaryData } from '@/v2/types/datanode.types'; import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper'; import { showDataFetchError } from '@/utils/common'; +import Spin from 'antd/es/spin'; type DecommisioningSummaryProps = { uuid: string; @@ -32,6 +33,37 @@ type DecommisioningSummaryState = { summaryData: SummaryData | Record; }; +function getDescriptions(summaryData: SummaryData): React.ReactElement { + const { + datanodeDetails: { + uuid, + networkLocation, + ipAddress, + hostName + }, + containers: { UnderReplicated, UnClosed }, + metrics: { + decommissionStartTime, + numOfUnclosedPipelines, + numOfUnclosedContainers, + numOfUnderReplicatedContainers + } + } = summaryData; + return ( + + {uuid} + ({networkLocation}/{ipAddress}/{hostName}) + {decommissionStartTime} + {numOfUnclosedPipelines} + {numOfUnclosedContainers} + {numOfUnderReplicatedContainers} + {UnderReplicated} + {UnClosed} + + ); +} + + const DecommissionSummary: React.FC = ({ uuid = '' }) => { @@ -40,6 +72,11 @@ const DecommissionSummary: React.FC = ({ loading: false }); const cancelSignal = React.useRef(); + let content = ( + + ); async function fetchDecommissionSummary(selectedUuid: string) { setState({ @@ -65,6 +102,12 @@ const DecommissionSummary: React.FC = ({ summaryData: {} }); showDataFetchError((error as AxiosError).toString()); + content = ( + + ) } } @@ -75,36 +118,18 @@ const DecommissionSummary: React.FC = ({ }) }, []); - let content = ( - - ); - const { summaryData } = state; - if (summaryData?.datanodeDetails && summaryData?.metrics && summaryData?.containers) { - const { - datanodeDetails: { uuid, networkLocation, ipAddress, hostName }, - containers: { UnderReplicated, UnClosed }, - metrics: { decommissionStartTime, numOfUnclosedPipelines, numOfUnclosedContainers, numOfUnderReplicatedContainers } - } = summaryData as SummaryData; - content = ( - - {uuid} - ({networkLocation}/{ipAddress}/{hostName}) - {decommissionStartTime} - {numOfUnclosedPipelines} - {numOfUnclosedContainers} - {numOfUnderReplicatedContainers} - {UnderReplicated} - {UnClosed} - - ); + if (summaryData?.datanodeDetails + && summaryData?.metrics + && summaryData?.containers + ) { + content = getDescriptions(summaryData as SummaryData); } - // Need to check summarydata is not empty + return ( - +  {uuid} ); diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx index 8812504515a8..f11decb3dc25 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx @@ -493,7 +493,6 @@ const Datanodes: React.FC<{}> = () => { ), showSizeChanger: true }; - console.log(selectedRows); return ( <> From 5b02f3df5767efc55dbe669e9983e1fbd996e3da Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Wed, 11 Sep 2024 12:55:45 +0530 Subject: [PATCH 5/7] Refactored Datanode table to a separate component --- .../v2/components/tables/datanodesTable.tsx | 315 ++++++++++++++++++ .../src/v2/pages/datanodes/datanodes.tsx | 295 +--------------- .../src/v2/types/datanode.types.ts | 11 + 3 files changed, 339 insertions(+), 282 deletions(-) create mode 100644 hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx new file mode 100644 index 000000000000..e138c7d8d099 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx @@ -0,0 +1,315 @@ +/* + * 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 { ReplicationIcon } from '@/utils/themeIcons'; +import DecommissionSummary from '@/v2/components/decommissioningSummary/decommissioningSummary'; +import StorageBar from '@/v2/components/storageBar/storageBar'; +import { Datanode, DatanodeOpState, DatanodeOpStateList, DatanodeState, DatanodeStateList, DatanodeTableProps } from '@/v2/types/datanode.types'; +import { Pipeline } from '@/v2/types/pipelines.types'; +import { CheckCircleFilled, CloseCircleFilled, HourglassFilled, InfoCircleOutlined, WarningFilled } from '@ant-design/icons'; +import { Popover, Tooltip } from 'antd' +import Table, { ColumnsType, TablePaginationConfig } from 'antd/es/table'; +import { TableRowSelection } from 'antd/es/table/interface'; +import moment from 'moment'; +import React from 'react'; + +moment.updateLocale('en', { + relativeTime: { + past: '%s ago', + s: '%ds', + m: '1min', + mm: '%dmins', + h: '1hr', + hh: '%dhrs', + d: '1d', + dd: '%dd', + M: '1m', + MM: '%dm', + y: '1y', + yy: '%dy' + } +}); + +let decommissioningUuids: string | string[] = []; + +const headerIconStyles: React.CSSProperties = { + display: 'flex', + alignItems: 'center' +} + +const renderDatanodeState = (state: DatanodeState) => { + const stateIconMap = { + HEALTHY: , + STALE: , + DEAD: + }; + const icon = state in stateIconMap ? stateIconMap[state] : ''; + return {icon} {state}; +}; + +const renderDatanodeOpState = (opState: DatanodeOpState) => { + const opStateIconMap = { + IN_SERVICE: , + DECOMMISSIONING: , + DECOMMISSIONED: , + ENTERING_MAINTENANCE: , + IN_MAINTENANCE: + }; + const icon = opState in opStateIconMap ? opStateIconMap[opState] : ''; + return {icon} {opState}; +}; + +const getTimeDiffFromTimestamp = (timestamp: number): string => { + const timestampDate = new Date(timestamp); + return moment(timestampDate).fromNow(); +} + +export const COLUMNS: ColumnsType = [ + { + title: 'Hostname', + dataIndex: 'hostname', + key: 'hostname', + sorter: (a: Datanode, b: Datanode) => a.hostname.localeCompare( + b.hostname, undefined, { numeric: true } + ), + defaultSortOrder: 'ascend' as const + }, + { + title: 'State', + dataIndex: 'state', + key: 'state', + filterMultiple: true, + filters: DatanodeStateList.map(state => ({ text: state, value: state })), + onFilter: (value, record: Datanode) => record.state === value, + render: (text: DatanodeState) => renderDatanodeState(text), + sorter: (a: Datanode, b: Datanode) => a.state.localeCompare(b.state) + }, + { + title: 'Operational State', + dataIndex: 'opState', + key: 'opState', + filterMultiple: true, + filters: DatanodeOpStateList.map(state => ({ text: state, value: state })), + onFilter: (value, record: Datanode) => record.opState === value, + render: (text: DatanodeOpState) => renderDatanodeOpState(text), + sorter: (a: Datanode, b: Datanode) => a.opState.localeCompare(b.opState) + }, + { + title: 'UUID', + dataIndex: 'uuid', + key: 'uuid', + sorter: (a: Datanode, b: Datanode) => a.uuid.localeCompare(b.uuid), + defaultSortOrder: 'ascend' as const, + render: (uuid: string, record: Datanode) => { + return ( + //1. Compare Decommission Api's UUID with all UUID in table and show Decommission Summary + (decommissioningUuids && decommissioningUuids.includes(record.uuid) && record.opState !== 'DECOMMISSIONED') ? + : {uuid} + ); + } + }, + { + title: 'Storage Capacity', + dataIndex: 'storageUsed', + key: 'storageUsed', + sorter: (a: Datanode, b: Datanode) => a.storageRemaining - b.storageRemaining, + render: (_: string, record: Datanode) => ( + + ) + }, + { + title: 'Last Heartbeat', + dataIndex: 'lastHeartbeat', + key: 'lastHeartbeat', + sorter: (a: Datanode, b: Datanode) => moment(a.lastHeartbeat).unix() - moment(b.lastHeartbeat).unix(), + render: (heartbeat: number) => { + return heartbeat > 0 ? getTimeDiffFromTimestamp(heartbeat) : 'NA'; + } + }, + { + title: 'Pipeline ID(s)', + dataIndex: 'pipelines', + key: 'pipelines', + render: (pipelines: Pipeline[], record: Datanode) => { + const renderPipelineIds = (pipelineIds: Pipeline[]) => { + return pipelineIds?.map((pipeline: any, index: any) => ( +
+ + {pipeline.pipelineID} +
+ )) + } + + return ( + + {pipelines.length} pipelines + + ); + } + }, + { + title: () => ( + + Leader Count + + + + + ), + dataIndex: 'leaderCount', + key: 'leaderCount', + sorter: (a: Datanode, b: Datanode) => a.leaderCount - b.leaderCount + }, + { + title: 'Containers', + dataIndex: 'containers', + key: 'containers', + sorter: (a: Datanode, b: Datanode) => a.containers - b.containers + }, + { + title: () => ( + + Open Container + + + + + ), + dataIndex: 'openContainers', + key: 'openContainers', + sorter: (a: Datanode, b: Datanode) => a.openContainers - b.openContainers + }, + { + title: 'Version', + dataIndex: 'version', + key: 'version', + sorter: (a: Datanode, b: Datanode) => a.version.localeCompare(b.version), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Setup Time', + dataIndex: 'setupTime', + key: 'setupTime', + sorter: (a: Datanode, b: Datanode) => a.setupTime - b.setupTime, + render: (uptime: number) => { + return uptime > 0 ? moment(uptime).format('ll LTS') : 'NA'; + } + }, + { + title: 'Revision', + dataIndex: 'revision', + key: 'revision', + sorter: (a: Datanode, b: Datanode) => a.revision.localeCompare(b.revision), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Build Date', + dataIndex: 'buildDate', + key: 'buildDate', + sorter: (a: Datanode, b: Datanode) => a.buildDate.localeCompare(b.buildDate), + defaultSortOrder: 'ascend' as const + }, + { + title: 'Network Location', + dataIndex: 'networkLocation', + key: 'networkLocation', + sorter: (a: Datanode, b: Datanode) => a.networkLocation.localeCompare(b.networkLocation), + defaultSortOrder: 'ascend' as const + } +]; + +const DatanodesTable: React.FC = ({ + data, + handleSelectionChange, + decommissionUuids, + selectedColumns, + loading = false, + selectedRows = [], + searchColumn = 'hostname', + searchTerm = '' +}) => { + + function filterSelectedColumns() { + const columnKeys = selectedColumns.map((column) => column.value); + return COLUMNS.filter( + (column) => columnKeys.indexOf(column.key as string) >= 0 + ); + } + + function getFilteredData(data: Datanode[]) { + return data.filter( + (datanode: Datanode) => datanode[searchColumn].includes(searchTerm) + ); + } + + function isSelectable(record: Datanode) { + // Disable checkbox for any datanode which is not DEAD to prevent removal + return record.state !== 'DEAD' && true; + } + + const paginationConfig: TablePaginationConfig = { + showTotal: (total: number, range) => ( + `${range[0]}-${range[1]} of ${total} Datanodes` + ), + showSizeChanger: true + }; + + const rowSelection: TableRowSelection = { + selectedRowKeys: selectedRows, + onChange: (rows: React.Key[]) => { handleSelectionChange(rows) }, + getCheckboxProps: (record: Datanode) => ({ + disabled: isSelectable(record) + }), + }; + + React.useEffect(() => { + decommissioningUuids = decommissionUuids; + }, [decommissionUuids]) + + return ( +
+
+ + ); +} + +export default DatanodesTable; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx index f11decb3dc25..ee32fa4f20ef 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx @@ -24,32 +24,20 @@ import React, { import moment from 'moment'; import { AxiosError } from 'axios'; import { - Table, - Tooltip, - Popover, Button, Modal } from 'antd'; import { - ColumnsType, - TablePaginationConfig -} from 'antd/es/table'; -import { - CheckCircleFilled, - CloseCircleFilled, DeleteOutlined, - HourglassFilled, - InfoCircleOutlined, WarningFilled, } from '@ant-design/icons'; import { ValueType } from 'react-select'; import Search from '@/v2/components/search/search'; -import StorageBar from '@/v2/components/storageBar/storageBar'; import MultiSelect, { Option } from '@/v2/components/select/multiSelect'; +import DatanodesTable, { COLUMNS } from '@/v2/components/tables/datanodesTable'; import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel'; import { showDataFetchError } from '@/utils/common'; -import { ReplicationIcon } from '@/utils/themeIcons'; import { AutoReloadHelper } from '@/utils/autoReloadHelper'; import { AxiosGetHelper, @@ -61,239 +49,13 @@ import { useDebounce } from '@/v2/hooks/debounce.hook'; import { Datanode, DatanodeDecomissionInfo, - DatanodeOpState, - DatanodeOpStateList, DatanodeResponse, DatanodesResponse, - DatanodesState, - DatanodeState, - DatanodeStateList + DatanodesState } from '@/v2/types/datanode.types'; -import { Pipeline } from '@/v2/types/pipelines.types'; import './datanodes.less' -import DecommissionSummary from '@/v2/components/decommissioningSummary/decommissioningSummary'; -import { ColumnTitleProps } from 'antd/lib/table/interface'; -import { TableRowSelection } from 'antd/es/table/interface'; - -moment.updateLocale('en', { - relativeTime: { - past: '%s ago', - s: '%ds', - m: '1min', - mm: '%dmins', - h: '1hr', - hh: '%dhrs', - d: '1d', - dd: '%dd', - M: '1m', - MM: '%dm', - y: '1y', - yy: '%dy' - } -}); - -const headerIconStyles: React.CSSProperties = { - display: 'flex', - alignItems: 'center' -} - -const renderDatanodeState = (state: DatanodeState) => { - const stateIconMap = { - HEALTHY: , - STALE: , - DEAD: - }; - const icon = state in stateIconMap ? stateIconMap[state] : ''; - return {icon} {state}; -}; - -const renderDatanodeOpState = (opState: DatanodeOpState) => { - const opStateIconMap = { - IN_SERVICE: , - DECOMMISSIONING: , - DECOMMISSIONED: , - ENTERING_MAINTENANCE: , - IN_MAINTENANCE: - }; - const icon = opState in opStateIconMap ? opStateIconMap[opState] : ''; - return {icon} {opState}; -}; - -const getTimeDiffFromTimestamp = (timestamp: number): string => { - const timestampDate = new Date(timestamp); - return moment(timestampDate).fromNow(); -} - -const COLUMNS: ColumnsType = [ - { - title: 'Hostname', - dataIndex: 'hostname', - key: 'hostname', - sorter: (a: Datanode, b: Datanode) => a.hostname.localeCompare( - b.hostname, undefined, { numeric: true } - ), - defaultSortOrder: 'ascend' as const - }, - { - title: 'State', - dataIndex: 'state', - key: 'state', - filterMultiple: true, - filters: DatanodeStateList.map(state => ({ text: state, value: state })), - onFilter: (value, record: Datanode) => record.state === value, - render: (text: DatanodeState) => renderDatanodeState(text), - sorter: (a: Datanode, b: Datanode) => a.state.localeCompare(b.state) - }, - { - title: 'Operational State', - dataIndex: 'opState', - key: 'opState', - filterMultiple: true, - filters: DatanodeOpStateList.map(state => ({ text: state, value: state })), - onFilter: (value, record: Datanode) => record.opState === value, - render: (text: DatanodeOpState) => renderDatanodeOpState(text), - sorter: (a: Datanode, b: Datanode) => a.opState.localeCompare(b.opState) - }, - { - title: 'UUID', - dataIndex: 'uuid', - key: 'uuid', - sorter: (a: Datanode, b: Datanode) => a.uuid.localeCompare(b.uuid), - defaultSortOrder: 'ascend' as const, - render: (uuid: string, record: Datanode) => { - return ( - //1. Compare Decommission Api's UUID with all UUID in table and show Decommission Summary - (decommissionUuids && decommissionUuids.includes(record.uuid) && record.opState !== 'DECOMMISSIONED') ? - : {uuid} - ); - } - }, - { - title: 'Storage Capacity', - dataIndex: 'storageUsed', - key: 'storageUsed', - sorter: (a: Datanode, b: Datanode) => a.storageRemaining - b.storageRemaining, - render: (_: string, record: Datanode) => ( - - ) - }, - { - title: 'Last Heartbeat', - dataIndex: 'lastHeartbeat', - key: 'lastHeartbeat', - sorter: (a: Datanode, b: Datanode) => moment(a.lastHeartbeat).unix() - moment(b.lastHeartbeat).unix(), - render: (heartbeat: number) => { - return heartbeat > 0 ? getTimeDiffFromTimestamp(heartbeat) : 'NA'; - } - }, - { - title: 'Pipeline ID(s)', - dataIndex: 'pipelines', - key: 'pipelines', - render: (pipelines: Pipeline[], record: Datanode) => { - const renderPipelineIds = (pipelineIds: Pipeline[]) => { - return pipelineIds?.map((pipeline: any, index: any) => ( -
- - {pipeline.pipelineID} -
- )) - } - return ( - - {pipelines.length} pipelines - - ); - } - }, - { - title: () => ( - - Leader Count - - - - - ), - dataIndex: 'leaderCount', - key: 'leaderCount', - sorter: (a: Datanode, b: Datanode) => a.leaderCount - b.leaderCount - }, - { - title: 'Containers', - dataIndex: 'containers', - key: 'containers', - sorter: (a: Datanode, b: Datanode) => a.containers - b.containers - }, - { - title: () => ( - - Open Container - - - - - ), - dataIndex: 'openContainers', - key: 'openContainers', - sorter: (a: Datanode, b: Datanode) => a.openContainers - b.openContainers - }, - { - title: 'Version', - dataIndex: 'version', - key: 'version', - sorter: (a: Datanode, b: Datanode) => a.version.localeCompare(b.version), - defaultSortOrder: 'ascend' as const - }, - { - title: 'Setup Time', - dataIndex: 'setupTime', - key: 'setupTime', - sorter: (a: Datanode, b: Datanode) => a.setupTime - b.setupTime, - render: (uptime: number) => { - return uptime > 0 ? moment(uptime).format('ll LTS') : 'NA'; - } - }, - { - title: 'Revision', - dataIndex: 'revision', - key: 'revision', - sorter: (a: Datanode, b: Datanode) => a.revision.localeCompare(b.revision), - defaultSortOrder: 'ascend' as const - }, - { - title: 'Build Date', - dataIndex: 'buildDate', - key: 'buildDate', - sorter: (a: Datanode, b: Datanode) => a.buildDate.localeCompare(b.buildDate), - defaultSortOrder: 'ascend' as const - }, - { - title: 'Network Location', - dataIndex: 'networkLocation', - key: 'networkLocation', - sorter: (a: Datanode, b: Datanode) => a.networkLocation.localeCompare(b.networkLocation), - defaultSortOrder: 'ascend' as const - } -]; const defaultColumns = COLUMNS.map(column => ({ label: (typeof column.title === 'string') @@ -342,19 +104,6 @@ const Datanodes: React.FC<{}> = () => { setSelectedColumns(selected as Option[]); } - function filterSelectedColumns() { - const columnKeys = selectedColumns.map((column) => column.value); - return COLUMNS.filter( - (column) => columnKeys.indexOf(column.key as string) >= 0 - ); - } - - function getFilteredData(data: Datanode[]) { - return data.filter( - (datanode: Datanode) => datanode[searchColumn].includes(debouncedSearch) - ); - } - async function loadDecommisionAPI() { decommissionUuids = []; const { request, controller } = await AxiosGetHelper( @@ -462,9 +211,8 @@ const Datanodes: React.FC<{}> = () => { }); }, []); - function isSelectable(record: Datanode) { - // Disable checkbox for any datanode which is not DEAD to prevent removal - return record.state !== 'DEAD' && true; + function handleSelectionChange(rows: React.Key[]) { + setSelectedRows(rows); } function handleModalOk() { @@ -479,21 +227,6 @@ const Datanodes: React.FC<{}> = () => { const { dataSource, lastUpdated, columnOptions } = state; - const rowSelection: TableRowSelection = { - selectedRowKeys: selectedRows, - onChange: (rows: React.Key[]) => { setSelectedRows(rows) }, - getCheckboxProps: (record: Datanode) => ({ - disabled: isSelectable(record) - }), - }; - - const paginationConfig: TablePaginationConfig = { - showTotal: (total: number, range) => ( - `${range[0]}-${range[1]} of ${total} Datanodes` - ), - showSizeChanger: true - }; - return ( <>
@@ -543,17 +276,15 @@ const Datanodes: React.FC<{}> = () => { setSearchColumn(value as 'hostname' | 'uuid' | 'version' | 'revision') }} />
-
-
- + void; +} From 0d25bca40011d79e298fc7b185836be1c9b38dc0 Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Mon, 16 Sep 2024 17:36:21 +0530 Subject: [PATCH 6/7] Fixed null data exception and removed Revision search field --- .../src/v2/components/tables/datanodesTable.tsx | 4 ++-- .../ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx index e138c7d8d099..137a0a6ad48a 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx @@ -268,9 +268,9 @@ const DatanodesTable: React.FC = ({ } function getFilteredData(data: Datanode[]) { - return data.filter( + return data?.filter( (datanode: Datanode) => datanode[searchColumn].includes(searchTerm) - ); + ) ?? []; } function isSelectable(record: Datanode) { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx index ee32fa4f20ef..13022dc05e02 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx @@ -73,9 +73,6 @@ const SearchableColumnOpts = [{ }, { label: 'Version', value: 'version' -}, { - label: 'Revision', - value: 'revision' }]; let decommissionUuids: string | string[] = []; From cbf28c14e7cad1781f5223a07a1ec6d96f1c3d8c Mon Sep 17 00:00:00 2001 From: Abhishek Pal Date: Wed, 18 Sep 2024 20:44:51 +0530 Subject: [PATCH 7/7] Refactored timestampDiff to import from moment utils --- .../v2/components/tables/datanodesTable.tsx | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx index 137a0a6ad48a..494d898509bf 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx @@ -16,34 +16,38 @@ * limitations under the License. */ -import { ReplicationIcon } from '@/utils/themeIcons'; -import DecommissionSummary from '@/v2/components/decommissioningSummary/decommissioningSummary'; -import StorageBar from '@/v2/components/storageBar/storageBar'; -import { Datanode, DatanodeOpState, DatanodeOpStateList, DatanodeState, DatanodeStateList, DatanodeTableProps } from '@/v2/types/datanode.types'; -import { Pipeline } from '@/v2/types/pipelines.types'; -import { CheckCircleFilled, CloseCircleFilled, HourglassFilled, InfoCircleOutlined, WarningFilled } from '@ant-design/icons'; +import React from 'react'; +import moment from 'moment'; import { Popover, Tooltip } from 'antd' -import Table, { ColumnsType, TablePaginationConfig } from 'antd/es/table'; +import { + CheckCircleFilled, + CloseCircleFilled, + HourglassFilled, + InfoCircleOutlined, + WarningFilled +} from '@ant-design/icons'; +import Table, { + ColumnsType, + TablePaginationConfig +} from 'antd/es/table'; import { TableRowSelection } from 'antd/es/table/interface'; -import moment from 'moment'; -import React from 'react'; -moment.updateLocale('en', { - relativeTime: { - past: '%s ago', - s: '%ds', - m: '1min', - mm: '%dmins', - h: '1hr', - hh: '%dhrs', - d: '1d', - dd: '%dd', - M: '1m', - MM: '%dm', - y: '1y', - yy: '%dy' - } -}); +import StorageBar from '@/v2/components/storageBar/storageBar'; +import DecommissionSummary from '@/v2/components/decommissioningSummary/decommissioningSummary'; + +import { ReplicationIcon } from '@/utils/themeIcons'; +import { getTimeDiffFromTimestamp } from '@/v2/utils/momentUtils'; + +import { + Datanode, + DatanodeOpState, + DatanodeOpStateList, + DatanodeState, + DatanodeStateList, + DatanodeTableProps +} from '@/v2/types/datanode.types'; +import { Pipeline } from '@/v2/types/pipelines.types'; + let decommissioningUuids: string | string[] = []; @@ -74,11 +78,6 @@ const renderDatanodeOpState = (opState: DatanodeOpState) => { return {icon} {opState}; }; -const getTimeDiffFromTimestamp = (timestamp: number): string => { - const timestampDate = new Date(timestamp); - return moment(timestampDate).fromNow(); -} - export const COLUMNS: ColumnsType = [ { title: 'Hostname',