diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.less index 44f53fa9d47d..6819ff701e6f 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.less +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.less @@ -169,4 +169,10 @@ body { .data-container { padding: 24px; height: 80vh; +} + +#error-icon { + font-size: 24px; + width: 100%; + color: #5A656D; } \ No newline at end of file diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errorBoundary/errorBoundary.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errors/errorBoundary.tsx similarity index 100% rename from hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errorBoundary/errorBoundary.tsx rename to hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errors/errorBoundary.tsx diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errors/errorCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errors/errorCard.tsx new file mode 100644 index 000000000000..b904673a93d5 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/errors/errorCard.tsx @@ -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. + */ + + +import React from 'react'; +import { DisconnectOutlined } from "@ant-design/icons" +import { Card } from 'antd'; + +type ErrorCardProps = { + title: string; + compact?: boolean; +}; + +// ------------- Styles -------------- // +const cardHeadStyle: React.CSSProperties = { fontSize: '14px' }; +const compactCardBodyStyle: React.CSSProperties = { + padding: '24px', + justifyContent: 'space-between' +} +const cardBodyStyle: React.CSSProperties = { + padding: '80px' +} + +const ErrorCard: React.FC = ({ title, compact }) => { + return ( + + + + ) +}; + +export default ErrorCard; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx index 2269ad9663a4..5971934381e9 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSimpleCard.tsx @@ -31,6 +31,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons'; import { numberWithCommas } from '@/utils/common'; +import ErrorCard from '@/v2/components/errors/errorCard'; // ------------- Types -------------- // @@ -40,11 +41,12 @@ type IconOptions = { type OverviewCardProps = { icon: string; - data: number | React.ReactElement; + data: number; title: string; hoverable?: boolean; loading?: boolean; linkToUrl?: string; + error?: string | null; } // ------------- Styles -------------- // @@ -109,9 +111,14 @@ const OverviewSimpleCard: React.FC = ({ title = '', hoverable = false, loading = false, - linkToUrl = '' + linkToUrl = '', + error }) => { + if (error) { + return + } + const titleElement = (linkToUrl) ? (
@@ -122,7 +129,7 @@ const OverviewSimpleCard: React.FC = ({ View More
) - : title + : title; return ( = ({ ); -} +}; -export default OverviewSimpleCard; \ No newline at end of file +export default OverviewSimpleCard; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewStorageCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewStorageCard.tsx index 2272f2ca01d9..ce9287b3db8d 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewStorageCard.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewStorageCard.tsx @@ -24,11 +24,14 @@ import EChart from '@/v2/components/eChart/eChart'; import OverviewCardWrapper from '@/v2/components/overviewCard/overviewCardWrapper'; import { StorageReport } from '@/v2/types/overview.types'; +import ErrorMessage from '@/v2/components/errors/errorCard'; +import ErrorCard from '@/v2/components/errors/errorCard'; // ------------- Types -------------- // type OverviewStorageCardProps = { loading?: boolean; storageReport: StorageReport; + error?: string | null; } const size = filesize.partial({ round: 1 }); @@ -73,9 +76,14 @@ const OverviewStorageCard: React.FC = ({ used: 0, remaining: 0, committed: 0 - } + }, + error }) => { + if (error) { + return + } + const { ozoneUsedPercentage, nonOzoneUsedPercentage, diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx index 8736b3e0d290..9214c456b6c7 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/overviewCard/overviewSummaryCard.tsx @@ -21,6 +21,8 @@ import { Card, Row, Table } from 'antd'; import { ColumnType } from 'antd/es/table'; import { Link } from 'react-router-dom'; +import ErrorMessage from '@/v2/components/errors/errorCard'; +import ErrorCard from '@/v2/components/errors/errorCard'; // ------------- Types -------------- // type TableData = { @@ -40,6 +42,7 @@ type OverviewTableCardProps = { linkToUrl?: string; showHeader?: boolean; state?: Record; + error?: string | null; } // ------------- Styles -------------- // @@ -65,8 +68,14 @@ const OverviewSummaryCard: React.FC = ({ tableData = [], linkToUrl = '', showHeader = false, - state + state, + error }) => { + + if (error) { + return ; + } + const titleElement = (linkToUrl) ? (
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/containerMismatchTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/containerMismatchTable.tsx index 818eca37f8ef..565acde6db70 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/containerMismatchTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/containerMismatchTable.tsx @@ -39,7 +39,7 @@ import Search from '@/v2/components/search/search'; import SingleSelect, { Option } from '@/v2/components/select/singleSelect'; import { showDataFetchError } from '@/utils/common'; import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; import { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingDirsTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingDirsTable.tsx index f0c6fc8161e4..190754b93886 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingDirsTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingDirsTable.tsx @@ -29,7 +29,7 @@ import SingleSelect, { Option } from '@/v2/components/select/singleSelect'; import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; import { byteToSize, showDataFetchError } from '@/utils/common'; import { getFormattedTime } from '@/v2/utils/momentUtils'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; import { DeletedDirInfo } from '@/v2/types/insights.types'; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingKeysTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingKeysTable.tsx index 65ada4956411..81ed9020c2be 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingKeysTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletePendingKeysTable.tsx @@ -29,7 +29,7 @@ import SingleSelect, { Option } from '@/v2/components/select/singleSelect'; import ExpandedPendingKeysTable from '@/v2/components/tables/insights/expandedPendingKeysTable'; import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; import { byteToSize, showDataFetchError } from '@/utils/common'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; import { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletedContainerKeysTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletedContainerKeysTable.tsx index 9aaf62a63d6f..9f665857b886 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletedContainerKeysTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/deletedContainerKeysTable.tsx @@ -28,7 +28,7 @@ import Search from '@/v2/components/search/search'; import SingleSelect, { Option } from '@/v2/components/select/singleSelect'; import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; import { showDataFetchError } from '@/utils/common'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; import { diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx index 02c73c77528d..38c57f4cef23 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/insights/openKeysTable.tsx @@ -36,7 +36,7 @@ import SingleSelect, { Option } from '@/v2/components/select/singleSelect'; import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; import { byteToSize, showDataFetchError } from '@/utils/common'; import { getFormattedTime } from '@/v2/utils/momentUtils'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; import { OpenKeys, OpenKeysResponse } from '@/v2/types/insights.types'; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/overview.constants.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/overview.constants.tsx new file mode 100644 index 000000000000..0429580c8e0c --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/overview.constants.tsx @@ -0,0 +1,50 @@ +/* +* 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 { ClusterStateResponse, KeysSummary, TaskStatus } from "@/v2/types/overview.types"; + +export const DEFAULT_CLUSTER_STATE: ClusterStateResponse = { + missingContainers: 0, + totalDatanodes: 0, + healthyDatanodes: 0, + pipelines: 0, + storageReport: { capacity: 0, used: 0, remaining: 0, committed: 0 }, + containers: 0, + volumes: 0, + buckets: 0, + keys: 0, + openContainers: 0, + deletedContainers: 0, + keysPendingDeletion: 0, + scmServiceId: 'N/A', + omServiceId: 'N/A' +}; + +export const DEFAULT_TASK_STATUS: TaskStatus[] = []; + +export const DEFAULT_OPEN_KEYS_SUMMARY: KeysSummary & {totalOpenKeys: number} = { + totalUnreplicatedDataSize: 0, + totalReplicatedDataSize: 0, + totalOpenKeys: 0 +}; + +export const DEFAULT_DELETE_PENDING_KEYS_SUMMARY: KeysSummary & {totalDeletedKeys: number} = { + totalUnreplicatedDataSize: 0, + totalReplicatedDataSize: 0, + totalDeletedKeys: 0 +}; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAPIData.hook.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAPIData.hook.ts new file mode 100644 index 000000000000..dfcdec0cefae --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAPIData.hook.ts @@ -0,0 +1,187 @@ +/* + * 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 { useState, useEffect, useRef } from 'react'; +import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; + +export interface ApiState { + data: T; + loading: boolean; + error: string | null; + lastUpdated: number | null; +} + +export interface UseApiDataOptions { + retryAttempts?: number; + retryDelay?: number; + initialFetch?: boolean; + onError?: (error: string) => void; +}; + +export function useApiData( + url: string, + defaultValue: T, + options: UseApiDataOptions = {} +): ApiState & { + refetch: () => void; + clearError: () => void; +} { + const { + retryAttempts = 3, + retryDelay = 1000, + initialFetch = true, + onError + } = options; + + const [state, setState] = useState>({ + data: defaultValue, + loading: initialFetch, + error: null, + lastUpdated: null + }); + + const controllerRef = useRef(); + const retryCountRef = useRef(0); + const retryTimeoutRef = useRef(); + + // Store stable references + const urlRef = useRef(url); + const retryAttemptsRef = useRef(retryAttempts); + const retryDelayRef = useRef(retryDelay); + const onErrorRef = useRef(onError); + + // Update refs when props change + useEffect(() => { + urlRef.current = url; + }, [url]); + + useEffect(() => { + retryAttemptsRef.current = retryAttempts; + }, [retryAttempts]); + + useEffect(() => { + retryDelayRef.current = retryDelay; + }, [retryDelay]); + + useEffect(() => { + onErrorRef.current = onError; + }, [onError]); + + + const fetchData = async (isRetry = false) => { + if (!isRetry) { + setState(prev => ({ ...prev, loading: true, error: null })); + retryCountRef.current = 0; + } + + try { + const { request, controller } = AxiosGetHelper( + urlRef.current, + controllerRef.current, + 'Request cancelled due to component unmount or new request' + ); + controllerRef.current = controller; + + const response = await request; + + setState({ + data: response.data, + loading: false, + error: null, + lastUpdated: Date.now() + }); + + retryCountRef.current = 0; + } catch (error: any) { + if (error.name === 'CanceledError') { + return; + } + + const errorMessage = error.response?.data?.message || + error.response?.statusText || + error.message || + `Request failed with status: ${error.response?.status || 'unknown'}`; + + // Clear any existing retry timeout + if (retryTimeoutRef.current) { + clearTimeout(retryTimeoutRef.current); + } + + // Retry logic for network errors and 5xx errors + if (retryCountRef.current < retryAttemptsRef.current && + (!error.response?.status || error.response?.status >= 500)) { + retryCountRef.current++; + retryTimeoutRef.current = setTimeout(() => { + fetchData(true); + }, retryDelayRef.current * retryCountRef.current); + return; + } + + if (onErrorRef.current) { + onErrorRef.current(errorMessage); + } + + setState({ + data: defaultValue, + loading: false, + error: errorMessage, + lastUpdated: Date.now() + }); + } + }; + + const refetch = () => { + fetchData(); + }; + + const clearError = () => { + setState(prev => ({ ...prev, error: null })); + }; + + // Initial fetch only + useEffect(() => { + if (initialFetch) { + fetchData(); + } + + // Cleanup retry timeout on unmount + return () => { + if (retryTimeoutRef.current) { + clearTimeout(retryTimeoutRef.current); + } + }; + }, []); // Empty dependency array + + // Cleanup on unmount + useEffect(() => { + return () => { + if (controllerRef.current) { + controllerRef.current.abort('Component unmounted'); + } + if (retryTimeoutRef.current) { + clearTimeout(retryTimeoutRef.current); + } + }; + }, []); + + return { + ...state, + refetch, + clearError + }; +} diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAutoReload.hook.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAutoReload.hook.tsx new file mode 100644 index 000000000000..baa8190bfc91 --- /dev/null +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useAutoReload.hook.tsx @@ -0,0 +1,89 @@ +/* + * 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 { useEffect, useRef, useState } from 'react'; +import { AUTO_RELOAD_INTERVAL_DEFAULT } from '@/constants/autoReload.constants'; + +export function useAutoReload( + refreshFunction: () => void, + interval: number = AUTO_RELOAD_INTERVAL_DEFAULT +) { + const intervalRef = useRef(0); + const [isPolling, setIsPolling] = useState(false); + const refreshFunctionRef = useRef(refreshFunction); + const lastPollCallRef = useRef(0); // This is used to store the last time poll was called + + // Update the ref when the function changes + refreshFunctionRef.current = refreshFunction; + + const stopPolling = () => { + if (intervalRef.current > 0) { + clearTimeout(intervalRef.current); + intervalRef.current = 0; + setIsPolling(false); + } + }; + + const startPolling = () => { + stopPolling(); + const poll = () => { + /** + * Prevent any extra polling calls within 100ms of the last call, + * This is done in case at any place multiple API calls are made, for example + * the useEffect on mount in this component will call the startPolling() function. + * If this startPolling() function is called elsewhere in a different component then + * race condition can occur where this gets called in succession multiple times. + */ + if (Date.now() - lastPollCallRef.current > 100) { + refreshFunctionRef.current(); + lastPollCallRef.current = Date.now(); + } + intervalRef.current = window.setTimeout(poll, interval); + }; + poll(); + setIsPolling(true); + }; + + const handleAutoReloadToggle = (checked: boolean) => { + sessionStorage.setItem('autoReloadEnabled', JSON.stringify(checked)); + if (checked) { + startPolling(); + } else { + stopPolling(); + } + }; + + // Initialize polling on mount if auto-reload is enabled + useEffect(() => { + const autoReloadEnabled = sessionStorage.getItem('autoReloadEnabled') !== 'false'; + if (autoReloadEnabled) { + startPolling(); + } + + return () => { + stopPolling(); + }; + }, []); // Empty dependency array + + return { + startPolling, + stopPolling, + isPolling, + handleAutoReloadToggle + }; +} diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/debounce.hook.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useDebounce.tsx similarity index 100% rename from hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/debounce.hook.tsx rename to hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/useDebounce.tsx 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 1c039f42709b..7d2c77de3c38 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 @@ -32,7 +32,7 @@ import { AutoReloadHelper } from '@/utils/autoReloadHelper'; import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper"; import { showDataFetchError } from '@/utils/common'; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { Bucket, diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx index 5cdbdd52625a..3a784bb9932c 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/containers/containers.tsx @@ -29,7 +29,7 @@ import AutoReloadPanel from "@/components/autoReloadPanel/autoReloadPanel"; import { showDataFetchError } from "@/utils/common"; import { AutoReloadHelper } from "@/utils/autoReloadHelper"; import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper"; -import { useDebounce } from "@/v2/hooks/debounce.hook"; +import { useDebounce } from "@/v2/hooks/useDebounce"; import { Container, 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 33dd661d97ba..7c044b2ae1e6 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 @@ -45,7 +45,7 @@ import { cancelRequests } from '@/utils/axiosRequestHelper'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { Datanode, DatanodeDecomissionInfo, diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx index 6014577f90a2..70c774cd441d 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/overview/overview.tsx @@ -16,31 +16,28 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react'; -import moment from 'moment'; -import filesize from 'filesize'; -import axios from 'axios'; +import React, { useState, useRef, useEffect } from 'react'; import { Row, Col, Button } from 'antd'; -import { - CheckCircleFilled, - WarningFilled -} from '@ant-design/icons'; +import { CheckCircleFilled, WarningFilled } from '@ant-design/icons'; import { Link } from 'react-router-dom'; +import moment from 'moment'; +import filesize from 'filesize'; import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel'; +import OverviewSimpleCard from '@/v2/components/overviewCard/overviewSimpleCard'; import OverviewSummaryCard from '@/v2/components/overviewCard/overviewSummaryCard'; import OverviewStorageCard from '@/v2/components/overviewCard/overviewStorageCard'; -import OverviewSimpleCard from '@/v2/components/overviewCard/overviewSimpleCard'; - -import { AutoReloadHelper } from '@/utils/autoReloadHelper'; -import { checkResponseError, showDataFetchError } from '@/utils/common'; -import { AxiosGetHelper, cancelRequests, PromiseAllSettledGetHelper } from '@/utils/axiosRequestHelper'; - -import { ClusterStateResponse, OverviewState, StorageReport } from '@/v2/types/overview.types'; +import { AxiosGetHelper } from '@/utils/axiosRequestHelper'; +import { showDataFetchError } from '@/utils/common'; +import { cancelRequests } from '@/utils/axiosRequestHelper'; +import { useApiData } from '@/v2/hooks/useAPIData.hook'; +import { useAutoReload } from '@/v2/hooks/useAutoReload.hook'; +import * as CONSTANTS from '@/v2/constants/overview.constants'; +import { ClusterStateResponse, KeysSummary, OverviewState, TaskStatus } from '@/v2/types/overview.types'; import './overview.less'; - +// ------------- Helper Functions -------------- // const size = filesize.partial({ round: 1 }); const getHealthIcon = (value: string): React.ReactElement => { @@ -82,165 +79,70 @@ const getSummaryTableValue = ( return size(value as number) } +// ------------- Main Component -------------- // const Overview: React.FC<{}> = () => { - - const cancelOverviewSignal = useRef(); const cancelOMDBSyncSignal = useRef(); - const [state, setState] = useState({ - loading: false, - datanodes: '', - pipelines: 0, - containers: 0, - volumes: 0, - buckets: 0, - keys: 0, - missingContainersCount: 0, - lastRefreshed: 0, - lastUpdatedOMDBDelta: 0, - lastUpdatedOMDBFull: 0, omStatus: '', - openContainers: 0, - deletedContainers: 0, - openSummarytotalUnrepSize: 0, - openSummarytotalRepSize: 0, - openSummarytotalOpenKeys: 0, - deletePendingSummarytotalUnrepSize: 0, - deletePendingSummarytotalRepSize: 0, - deletePendingSummarytotalDeletedKeys: 0, - scmServiceId: '', - omServiceId: '' - }) - const [storageReport, setStorageReport] = useState({ - capacity: 0, - used: 0, - remaining: 0, - committed: 0 - }) + lastRefreshed: 0 + }); - // Component mounted, fetch initial data - useEffect(() => { - loadOverviewPageData(); - autoReloadHelper.startPolling(); - return (() => { - // Component will Un-mount - autoReloadHelper.stopPolling(); - cancelRequests([ - cancelOMDBSyncSignal.current!, - cancelOverviewSignal.current! - ]); - }) - }, []) - - const loadOverviewPageData = () => { - setState({ - ...state, - loading: true - }); - - // Cancel any previous pending requests - cancelRequests([ - cancelOMDBSyncSignal.current!, - cancelOverviewSignal.current! - ]); + // Individual API calls using custom hook (no auto-refresh) + const clusterState = useApiData( + '/api/v1/clusterState', + CONSTANTS.DEFAULT_CLUSTER_STATE, + { + retryAttempts: 2, + initialFetch: false, + onError: (error) => showDataFetchError(error) + } + ); - const { requests, controller } = PromiseAllSettledGetHelper([ - '/api/v1/clusterState', - '/api/v1/task/status', - '/api/v1/keys/open/summary', - '/api/v1/keys/deletePending/summary' - ], cancelOverviewSignal.current); - cancelOverviewSignal.current = controller; + const taskStatus = useApiData( + '/api/v1/task/status', + CONSTANTS.DEFAULT_TASK_STATUS, + { + retryAttempts: 2, + initialFetch: false, + onError: (error) => showDataFetchError(error) + } + ); - requests.then(axios.spread(( - clusterStateResponse: Awaited>, - taskstatusResponse: Awaited>, - openResponse: Awaited>, - deletePendingResponse: Awaited> - ) => { + const openKeysSummary = useApiData( + '/api/v1/keys/open/summary', + CONSTANTS.DEFAULT_OPEN_KEYS_SUMMARY, + { + retryAttempts: 2, + initialFetch: false, + onError: (error) => showDataFetchError(error) + } + ); - checkResponseError([ - clusterStateResponse, - taskstatusResponse, - openResponse, - deletePendingResponse - ]); + const deletePendingKeysSummary = useApiData( + '/api/v1/keys/deletePending/summary', + CONSTANTS.DEFAULT_DELETE_PENDING_KEYS_SUMMARY, + { + retryAttempts: 2, + initialFetch: false, + onError: (error) => showDataFetchError(error) + } + ); - const clusterState: ClusterStateResponse = clusterStateResponse.value?.data ?? { - missingContainers: 'N/A', - totalDatanodes: 'N/A', - healthyDatanodes: 'N/A', - pipelines: 'N/A', - storageReport: { - capacity: 0, - used: 0, - remaining: 0, - committed: 0 - }, - containers: 'N/A', - volumes: 'N/A', - buckets: 'N/A', - keys: 'N/A', - openContainers: 'N/A', - deletedContainers: 'N/A', - keysPendingDeletion: 'N/A', - scmServiceId: 'N/A', - omServiceId: 'N/A', - }; - const taskStatus = taskstatusResponse.value?.data ?? [{ - taskName: 'N/A', - lastUpdatedTimestamp: 0, - lastUpdatedSeqNumber: 0 - }]; - const missingContainersCount = clusterState.missingContainers; - const omDBDeltaObject = taskStatus && taskStatus.find((item: any) => item.taskName === 'OmDeltaRequest'); - const omDBFullObject = taskStatus && taskStatus.find((item: any) => item.taskName === 'OmSnapshotRequest'); + const omDBDeltaObject = taskStatus.data?.find((item: TaskStatus) => item.taskName === 'OmDeltaRequest'); + const omDBFullObject = taskStatus.data?.find((item: TaskStatus) => item.taskName === 'OmSnapshotRequest'); - setState({ - ...state, - loading: false, - datanodes: `${clusterState.healthyDatanodes}/${clusterState.totalDatanodes}`, - pipelines: clusterState.pipelines, - containers: clusterState.containers, - volumes: clusterState.volumes, - buckets: clusterState.buckets, - keys: clusterState.keys, - missingContainersCount: missingContainersCount, - openContainers: clusterState.openContainers, - deletedContainers: clusterState.deletedContainers, - lastRefreshed: Number(moment()), - lastUpdatedOMDBDelta: omDBDeltaObject?.lastUpdatedTimestamp, - lastUpdatedOMDBFull: omDBFullObject?.lastUpdatedTimestamp, - openSummarytotalUnrepSize: openResponse?.value?.data?.totalUnreplicatedDataSize, - openSummarytotalRepSize: openResponse?.value?.data?.totalReplicatedDataSize, - openSummarytotalOpenKeys: openResponse?.value?.data?.totalOpenKeys, - deletePendingSummarytotalUnrepSize: deletePendingResponse?.value?.data?.totalUnreplicatedDataSize, - deletePendingSummarytotalRepSize: deletePendingResponse?.value?.data?.totalReplicatedDataSize, - deletePendingSummarytotalDeletedKeys: deletePendingResponse?.value?.data?.totalDeletedKeys, - scmServiceId: clusterState?.scmServiceId ?? 'N/A', - omServiceId: clusterState?.omServiceId ?? 'N/A' - }); - setStorageReport({ - ...storageReport, - ...clusterState.storageReport - }); - })).catch((error: Error) => { - setState({ - ...state, - loading: false - }); - showDataFetchError(error.toString()); - }); - } - - let autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadOverviewPageData); + const loadOverviewPageData = () => { + clusterState.refetch(); + taskStatus.refetch(); + openKeysSummary.refetch(); + deletePendingKeysSummary.refetch(); + setState(prev => ({ ...prev, lastRefreshed: Number(moment()) })); + }; + + const autoReload = useAutoReload(loadOverviewPageData); + // OM DB Sync function const syncOmData = () => { - setState({ - ...state, - loading: true - }); - const { request, controller } = AxiosGetHelper( '/api/v1/triggerdbsync/om', cancelOMDBSyncSignal.current, @@ -250,56 +152,36 @@ const Overview: React.FC<{}> = () => { request.then(omStatusResponse => { const omStatus = omStatusResponse.data; - setState({ - ...state, - loading: false, - omStatus: omStatus - }); + setState(prev => ({ ...prev, omStatus })); }).catch((error: Error) => { - setState({ - ...state, - loading: false - }); showDataFetchError(error.toString()); }); }; - const { - loading, datanodes, pipelines, - containers, volumes, buckets, - openSummarytotalUnrepSize, - openSummarytotalRepSize, - openSummarytotalOpenKeys, - deletePendingSummarytotalUnrepSize, - deletePendingSummarytotalRepSize, - deletePendingSummarytotalDeletedKeys, - keys, missingContainersCount, - lastRefreshed, lastUpdatedOMDBDelta, - lastUpdatedOMDBFull, - omStatus, openContainers, - deletedContainers, scmServiceId, omServiceId - } = state; + useEffect(() => { + return () => { + cancelRequests([cancelOMDBSyncSignal.current!]); + }; + }, []); const healthCardIndicators = ( <> Datanodes - {getHealthIcon(datanodes)} + {getHealthIcon(`${clusterState.data?.healthyDatanodes}/${clusterState.data?.totalDatanodes}`)} Containers - {getHealthIcon(`${(containers - missingContainersCount)}/${containers}`)} + {getHealthIcon(`${(clusterState.data?.containers || 0) - (clusterState.data?.missingContainers || 0)}/${clusterState.data?.containers}`)} - ) + ); const datanodesLink = ( - - ) + ); const containersLink = ( ) + const loading = clusterState.loading || taskStatus.loading || openKeysSummary.loading || deletePendingKeysSummary.loading; + const { + healthyDatanodes, + totalDatanodes, + containers, + missingContainers, + storageReport, + volumes, + buckets, + keys, + pipelines, + deletedContainers, + omServiceId, + scmServiceId + } = clusterState.data; + const { + totalReplicatedDataSize: openSummarytotalRepSize, + totalUnreplicatedDataSize: openSummarytotalUnrepSize, + totalOpenKeys: openSummarytotalOpenKeys, + } = openKeysSummary.data ?? {}; + const { + totalReplicatedDataSize: deletePendingSummarytotalRepSize, + totalUnreplicatedDataSize: deletePendingSummarytotalUnrepSize, + totalDeletedKeys: deletePendingSummarytotalDeletedKeys + } = deletePendingKeysSummary.data ?? {}; + return ( <>
Overview - +
= () => { title='Health' data={healthCardIndicators} showHeader={true} + loading={clusterState.loading} columns={[ { title: '', @@ -356,20 +265,21 @@ const Overview: React.FC<{}> = () => { { key: 'datanodes', name: 'Datanodes', - value: datanodes, + value: `${healthyDatanodes}/${totalDatanodes}`, action: datanodesLink }, { key: 'containers', name: 'Containers', - value: `${(containers - missingContainersCount)}/${containers}`, + value: `${containers - missingContainers}/${containers}`, action: containersLink } ]} + error={clusterState.error} /> - + = () => { + linkToUrl='/Volumes' + error={clusterState.error} /> + linkToUrl='/Buckets' + error={clusterState.error} /> + loading={clusterState.loading} + data={keys} + error={clusterState.error} /> + linkToUrl='/Pipelines' + error={clusterState.error} /> + loading={clusterState.loading} + data={deletedContainers} + error={clusterState.error} /> = () => { = () => { } ]} linkToUrl='/Om' - state={{activeTab: '2'}} /> + state={{activeTab: '2'}} + error={openKeysSummary.error} /> = () => { } ]} linkToUrl='/Om' - state={{activeTab: '3'}} /> + state={{activeTab: '3'}} + error={deletePendingKeysSummary.error} /> @@ -522,4 +439,4 @@ const Overview: React.FC<{}> = () => { ); } -export default Overview; \ No newline at end of file +export default Overview; diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx index f6ff87c7e132..19a3c9447a6d 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx @@ -31,7 +31,7 @@ import PipelinesTable, { COLUMNS } from '@/v2/components/tables/pipelinesTable'; import { showDataFetchError } from '@/utils/common'; import { AutoReloadHelper } from '@/utils/autoReloadHelper'; import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { Pipeline, 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 b4614d387f3a..a10b71282cea 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 @@ -31,7 +31,7 @@ import { showDataFetchError } from '@/utils/common'; import { AutoReloadHelper } from '@/utils/autoReloadHelper'; import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper"; import { LIMIT_OPTIONS } from '@/v2/constants/limit.constants'; -import { useDebounce } from '@/v2/hooks/debounce.hook'; +import { useDebounce } from '@/v2/hooks/useDebounce'; import { Volume, diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/overview.types.ts b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/overview.types.ts index f8390fd43468..ef3043f8955e 100644 --- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/overview.types.ts +++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/overview.types.ts @@ -33,29 +33,15 @@ export type ClusterStateResponse = { omServiceId: string; } -export type OverviewState = { - loading: boolean; - datanodes: string; - pipelines: number; - containers: number; - volumes: number; - buckets: number; - keys: number; - missingContainersCount: number; - lastRefreshed: number; - lastUpdatedOMDBDelta: number; - lastUpdatedOMDBFull: number; - omStatus: string; - openContainers: number; - deletedContainers: number; - openSummarytotalUnrepSize: number; - openSummarytotalRepSize: number; - openSummarytotalOpenKeys: number; - deletePendingSummarytotalUnrepSize: number; - deletePendingSummarytotalRepSize: number; - deletePendingSummarytotalDeletedKeys: number; - scmServiceId: string; - omServiceId: string; +export type TaskStatus = { + taskName: 'OmDeltaRequest' | 'OmSnapshotRequest' | string; + lastUpdatedTimestamp: number; + lastUpdatedSeqNumber: number; +} + +export type KeysSummary = { + totalUnreplicatedDataSize: number; + totalReplicatedDataSize: number; } export type StorageReport = { @@ -64,3 +50,8 @@ export type StorageReport = { remaining: number; committed: number; } + +export type OverviewState = { + omStatus: string; + lastRefreshed: number; +} \ No newline at end of file