From 3465b4872ba5fa785e74bf36ef356a627e001ab9 Mon Sep 17 00:00:00 2001 From: Timothy Asir Jeyasingh Date: Fri, 18 Oct 2024 16:16:25 +0530 Subject: [PATCH] Display non-blocking warnings for synchronization delays https://issues.redhat.com/browse/RHSTOR-5895 Signed-off-by: Timothy Asir Jeyasingh --- locales/en/plugin__odf-console.json | 2 + .../failover-relocate-modal-body.tsx | 61 ++++++--- .../failover-relocate-modal.tsx | 3 +- .../helper/error-messages.tsx | 12 ++ .../parser/argo-application-set-parser.tsx | 15 ++- .../subscriptions/dr-policy-selector.tsx | 1 + .../subscriptions/error-messages.tsx | 13 ++ .../failover-relocate-modal-body.tsx | 10 +- .../subscriptions/failover-relocate-modal.tsx | 2 +- .../subscriptions/peer-cluster-status.tsx | 121 +++++++++++++----- .../subscriptions/reducer.ts | 3 + .../protected-applications/list-page.tsx | 2 +- .../protected-applications/utils.tsx | 17 --- packages/mco/utils/disaster-recovery.tsx | 16 +++ 14 files changed, 205 insertions(+), 73 deletions(-) diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 53e46fda8..f5600a8c5 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -307,6 +307,8 @@ "Other applications may be affected.": "Other applications may be affected.", "<0>This application uses placement that are also used by other applications. Failing over will automatically trigger a failover for other applications sharing the same placement.": "<0>This application uses placement that are also used by other applications. Failing over will automatically trigger a failover for other applications sharing the same placement.", "<0>This application uses placement that are also used by other applications. Relocating will automatically trigger a relocate for other applications sharing the same placement.": "<0>This application uses placement that are also used by other applications. Relocating will automatically trigger a relocate for other applications sharing the same placement.", + "Inconsistent data on target cluster": "Inconsistent data on target cluster", + "The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing the failover could lead to data loss. Refer to the corresponding VolumeSynchronizationDelay OpenShift alert(s) for more information.": "The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing the failover could lead to data loss. Refer to the corresponding VolumeSynchronizationDelay OpenShift alert(s) for more information.", "Attention": "Attention", "A failover will occur for all namespaces currently under this DRPC.": "A failover will occur for all namespaces currently under this DRPC.", "You need to clean up manually to begin replication after a successful failover.": "You need to clean up manually to begin replication after a successful failover.", diff --git a/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal-body.tsx b/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal-body.tsx index 807eedded..8e9790916 100644 --- a/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal-body.tsx +++ b/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal-body.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { getReplicationHealth } from '@odf/mco/utils'; import { ACM_DEFAULT_DOC_VERSION } from '@odf/shared/constants'; import { utcDateTimeFormatter } from '@odf/shared/details-page/datetime'; import { useDocVersion, DOC_VERSION as mcoDocVersion } from '@odf/shared/hooks'; @@ -25,6 +26,7 @@ import { DRActionType, REPLICATION_TYPE, ACM_OPERATOR_SPEC_NAME, + VOLUME_REPLICATION_HEALTH, } from '../../../constants'; import { ErrorMessageType, @@ -79,30 +81,56 @@ const validatePlacement = ( action: DRActionType ): ErrorMessageType => { const isFailoverAction = action === DRActionType.FAILOVER; + + // Check if DR is disabled if (!placementControl?.drPlacementControlName) { - // DR is disabled return isFailoverAction ? ErrorMessageType.DR_IS_NOT_ENABLED_FAILOVER : ErrorMessageType.DR_IS_NOT_ENABLED_RELOCATE; - } else if (!placementControl?.isDRActionReady) { - // Either Peer is not ready for DR failover - // Or, Peer is not ready/available for DR relocate + } + + // Check if DR action is ready + // Either Peer is not ready for DR failover + // Or, Peer is not ready/available for DR relocate + if (!placementControl?.isDRActionReady) { return isFailoverAction ? ErrorMessageType.FAILOVER_READINESS_CHECK_FAILED : ErrorMessageType.RELOCATE_READINESS_CHECK_FAILED; - } else { - const errorMessage = isFailoverAction - ? failoverPreCheck(placementControl) - : relocatePreCheck(placementControl); - if (!errorMessage) { - if (placementControl?.areSiblingApplicationsFound) { - return isFailoverAction - ? ErrorMessageType.SIBLING_APPLICATIONS_FOUND_FAILOVER - : ErrorMessageType.SIBLING_APPLICATIONS_FOUND_RELOCATE; - } - } - return errorMessage; } + + // Perform failover or relocate pre-check + const preCheckError = isFailoverAction + ? failoverPreCheck(placementControl) + : relocatePreCheck(placementControl); + + if (preCheckError) { + return preCheckError; + } + + // Check volume replication health + if ( + [ + VOLUME_REPLICATION_HEALTH.CRITICAL, + VOLUME_REPLICATION_HEALTH.WARNING, + ].includes( + getReplicationHealth( + placementControl?.snapshotTakenTime, + placementControl?.schedulingInterval, + placementControl?.replicationType + ) + ) + ) { + return ErrorMessageType.VOLUME_SYNC_DELAY; + } + + // Check if sibling applications are found + if (placementControl?.areSiblingApplicationsFound) { + return isFailoverAction + ? ErrorMessageType.SIBLING_APPLICATIONS_FOUND_FAILOVER + : ErrorMessageType.SIBLING_APPLICATIONS_FOUND_RELOCATE; + } + + return null; }; const MessageStatus = ({ message }: { message: MessageKind }) => ( @@ -331,6 +359,7 @@ export type PlacementControlProps = Partial<{ isPrimaryClusterFenced: boolean; areSiblingApplicationsFound: boolean; kubeObjectLastSyncTime: string; + schedulingInterval: string; }>; export type ApplicationProps = { diff --git a/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal.tsx b/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal.tsx index 42569d164..1397bc19d 100644 --- a/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal.tsx +++ b/packages/mco/components/modals/app-failover-relocate/failover-relocate-modal.tsx @@ -60,7 +60,7 @@ export const FailoverRelocateModal: React.FC = ( const onClick = () => { setFooterStatus(ModalFooterStatus.INPROGRESS); - // Prefered cluster and failover cluster should not be same for failover and relocate. + // Preferred cluster and failover cluster should not be the same for failover and relocate. const patch = [ { op: 'replace', @@ -120,6 +120,7 @@ export const FailoverRelocateModal: React.FC = ( setCanInitiate={setCanInitiate} setPlacement={setPlacementControl} /> + {(!!errorMessage || !!loadError) && ( + {t( + "The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing the failover could lead to data loss. Refer to the corresponding VolumeSynchronizationDelay OpenShift alert(s) for more information." + )} +

+ ), + variant: AlertVariant.warning, + }, }); export const evaluateErrorMessage = ( diff --git a/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.tsx b/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.tsx index 4eb4a3db7..dea0e1674 100644 --- a/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.tsx +++ b/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.tsx @@ -85,9 +85,11 @@ export const ArogoApplicationSetParser = ( props: ArogoApplicationSetParserProps ) => { const { application, action, isOpen, close } = props; + const [drResources, drLoaded, drLoadError] = useDisasterRecoveryResourceWatch( getDRResources(getNamespace(application)) ); + const [aroAppSetResources, loaded, loadError] = useArgoApplicationSetResourceWatch( getApplicationSetResources( @@ -99,15 +101,22 @@ export const ArogoApplicationSetParser = ( drLoadError ) ); + const aroAppSetResource = aroAppSetResources?.formattedResources?.[0]; + const placementControls: PlacementControlProps[] = React.useMemo(() => { const { managedClusters, siblingApplications, placements: resourcePlacements, } = aroAppSetResource || {}; - const { drClusters, drPlacementControl, placementDecision, placement } = - resourcePlacements?.[0] || {}; + const { + drClusters, + drPolicy, + drPlacementControl, + placementDecision, + placement, + } = resourcePlacements?.[0] || {}; const deploymentClusters = findDeploymentClusters( placementDecision, drPlacementControl @@ -133,6 +142,7 @@ export const ArogoApplicationSetParser = ( targetCluster, MANAGED_CLUSTER_CONDITION_AVAILABLE ); + return loaded && !loadError ? [ { @@ -150,6 +160,7 @@ export const ArogoApplicationSetParser = ( isTargetClusterFenced: isDRClusterFenced(targetDRCluster), isPrimaryClusterFenced: isDRClusterFenced(primaryDRCluster), areSiblingApplicationsFound: !!siblingApplications?.length, + schedulingInterval: drPolicy?.spec?.schedulingInterval, }, ] : []; diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/dr-policy-selector.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/dr-policy-selector.tsx index dd6877220..5c31044b2 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/dr-policy-selector.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/dr-policy-selector.tsx @@ -89,6 +89,7 @@ export const DRPolicySelector: React.FC = ({ setSelected({ policyName: getName(drPolicy), drClusters: drPolicy?.spec?.drClusters, + schedulingInterval: drPolicy?.spec?.schedulingInterval, }); setOpen(false); }; diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx index 42ed20d49..4fc7de4da 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx @@ -17,6 +17,8 @@ export enum ErrorMessageType { PRIMARY_CLUSTER_IS_NOT_FENCED, TARGET_CLUSTER_IS_FENCED, NO_SUBSCRIPTION_GROUP_FOUND, + // Warning Messages + VOLUME_SYNC_DELAY, } export type MessageKind = Partial<{ @@ -243,4 +245,15 @@ export const ErrorMessages = ( title: t('No subscription groups are found.'), variant: AlertVariant.danger, }, + [ErrorMessageType.VOLUME_SYNC_DELAY]: { + title: t('Inconsistent data on target cluster'), + message: ( +

+ {t( + "The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing the failover could lead to data loss. Refer to the corresponding VolumeSynchronizationDelay OpenShift alert(s) for more information." + )} +

+ ), + variant: AlertVariant.warning, + }, }); diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal-body.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal-body.tsx index 6962d9a1f..6434f83af 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal-body.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal-body.tsx @@ -45,13 +45,17 @@ import { import { SubscriptionGroupSelector } from './subscription-group-selector'; import { TargetClusterSelector } from './target-cluster-selector'; -export const findErrorMessage = (errorMessage: ErrorMessage) => +export const findErrorMessage = ( + errorMessage: ErrorMessage, + includeWarning: Boolean +) => [ errorMessage.drPolicyControlStateErrorMessage, errorMessage.managedClustersErrorMessage, errorMessage.targetClusterErrorMessage, errorMessage.subscriptionGroupErrorMessage, errorMessage.peerStatusErrorMessage, + includeWarning && errorMessage.syncDelayWarningMessage, ] .filter(Boolean) .find((errorMessageItem) => errorMessageItem); @@ -250,11 +254,11 @@ export const FailoverRelocateModalBody: React.FC })} /> )) || - ((!!findErrorMessage(state.errorMessage) || + ((!!findErrorMessage(state.errorMessage, true) || !_.isEmpty(state.actionErrorMessage)) && ( ))} diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal.tsx index 2069c0fa4..8ef4a0899 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/failover-relocate-modal.tsx @@ -109,7 +109,7 @@ export const SubscriptionFailoverRelocateModal: React.FC - !findErrorMessage(state.errorMessage) && + !findErrorMessage(state.errorMessage, false) && !!state.selectedSubsGroups.length; const onClick = () => { diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/peer-cluster-status.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/peer-cluster-status.tsx index 75683877f..3d34c9a98 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/peer-cluster-status.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/peer-cluster-status.tsx @@ -10,8 +10,12 @@ import { import { TFunction } from 'i18next'; import { Flex, FlexItem } from '@patternfly/react-core'; import { UnknownIcon } from '@patternfly/react-icons'; -import { DRActionType } from '../../../../constants'; -import { checkDRActionReadiness } from '../../../../utils'; +import { DRActionType, VOLUME_REPLICATION_HEALTH } from '../../../../constants'; +import { + checkDRActionReadiness, + getReplicationHealth, + getReplicationType, +} from '../../../../utils'; import { DateTimeFormat } from '../failover-relocate-modal-body'; import { ErrorMessageType } from './error-messages'; import { @@ -35,6 +39,7 @@ const initalPeerStatus = (t: TFunction) => ({ dataLastSyncedOn: { text: '', }, + replicationHealth: VOLUME_REPLICATION_HEALTH.HEALTHY, }); const getPeerReadiness = ( @@ -75,29 +80,58 @@ const getPeerStatusSummary = ( drpcStateList: DRPolicyControlState[], subsGroups: string[], actionType: DRActionType, + schedulingInterval: string, t: TFunction ) => - // Verify all DRPC has Peer ready status - drpcStateList?.reduce( - (acc, drpcState) => - subsGroups.includes(getName(drpcState?.drPlacementControl)) - ? { - ...acc, - peerReadiness: getPeerReadiness( - acc.peerReadiness, - drpcState, - actionType, - t - ), - dataLastSyncedOn: getDataLastSyncTime( - acc.dataLastSyncedOn, - drpcState - ), - } - : acc, - initalPeerStatus(t) + drpcStateList?.reduce((acc, drpcState) => { + const drPlacementControl = drpcState?.drPlacementControl; + + if (subsGroups.includes(getName(drPlacementControl))) { + const lastGroupSyncTime = drPlacementControl?.status?.lastGroupSyncTime; + const higherSeverityHealth = getHigherSeverityHealth( + acc.replicationHealth, + lastGroupSyncTime, + schedulingInterval + ); + + return { + ...acc, + peerReadiness: getPeerReadiness( + acc.peerReadiness, + drpcState, + actionType, + t + ), + dataLastSyncedOn: getDataLastSyncTime(acc.dataLastSyncedOn, drpcState), + replicationHealth: higherSeverityHealth, + }; + } + return acc; + }, initalPeerStatus(t)); + +const getHigherSeverityHealth = ( + previousHealth: VOLUME_REPLICATION_HEALTH, + lastGroupSyncTime: string, + schedulingInterval: string +): VOLUME_REPLICATION_HEALTH => { + const replicationType = getReplicationType(schedulingInterval); + const currentHealth = getReplicationHealth( + lastGroupSyncTime, + schedulingInterval, + replicationType ); + if ( + [ + VOLUME_REPLICATION_HEALTH.CRITICAL, + VOLUME_REPLICATION_HEALTH.WARNING, + ].includes(currentHealth) + ) { + return currentHealth; + } + return previousHealth; +}; + type StatusProps = { icon?: JSX.Element; text: string; @@ -113,7 +147,12 @@ export const PeerClusterStatus: React.FC = ({ dispatch, }) => { const { t } = useCustomTranslation(); - const { selectedSubsGroups, drPolicyControlState, actionType } = state; + const { + selectedSubsGroups, + drPolicyControlState, + actionType, + selectedDRPolicy, + } = state; const [peerStatus, setPeerStatus] = React.useState( initalPeerStatus(t) ); @@ -121,9 +160,10 @@ export const PeerClusterStatus: React.FC = ({ (errorMessage: ErrorMessageType) => { dispatch({ type: FailoverAndRelocateType.SET_ERROR_MESSAGE, - payload: { - peerStatusErrorMessage: errorMessage, - }, + payload: + errorMessage !== ErrorMessageType.VOLUME_SYNC_DELAY + ? { peerStatusErrorMessage: errorMessage } + : { syncDelayWarningMessage: errorMessage }, }); }, [dispatch] @@ -135,15 +175,31 @@ export const PeerClusterStatus: React.FC = ({ drPolicyControlState, selectedSubsGroups, actionType, + selectedDRPolicy.schedulingInterval, t ); - peerCurrentStatus.peerReadiness.text === PEER_READINESS(t).PEER_NOT_READY - ? setErrorMessage( - actionType === DRActionType.FAILOVER - ? ErrorMessageType.FAILOVER_READINESS_CHECK_FAILED - : ErrorMessageType.RELOCATE_READINESS_CHECK_FAILED - ) - : setErrorMessage(0 as ErrorMessageType); + if ( + peerCurrentStatus.peerReadiness.text === + PEER_READINESS(t).PEER_NOT_READY + ) { + setErrorMessage( + actionType === DRActionType.FAILOVER + ? ErrorMessageType.FAILOVER_READINESS_CHECK_FAILED + : ErrorMessageType.RELOCATE_READINESS_CHECK_FAILED + ); + } else if ( + !!peerCurrentStatus.replicationHealth && + [ + VOLUME_REPLICATION_HEALTH.CRITICAL, + VOLUME_REPLICATION_HEALTH.WARNING, + ].includes( + peerCurrentStatus.replicationHealth as VOLUME_REPLICATION_HEALTH + ) + ) { + setErrorMessage(ErrorMessageType.VOLUME_SYNC_DELAY); + } else { + setErrorMessage(0 as ErrorMessageType); + } setPeerStatus(peerCurrentStatus); } else { // Default peer status is Unknown @@ -152,6 +208,7 @@ export const PeerClusterStatus: React.FC = ({ } }, [ selectedSubsGroups, + selectedDRPolicy.schedulingInterval, drPolicyControlState, actionType, t, diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/reducer.ts b/packages/mco/components/modals/app-failover-relocate/subscriptions/reducer.ts index 1e9ace5e6..52a6284e9 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/reducer.ts +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/reducer.ts @@ -19,6 +19,7 @@ export type DRPolicyControlState = ApplicationDRInfo; export type DRPolicyType = Partial<{ policyName: string; drClusters: string[]; + schedulingInterval: string; }>; export type TargetClusterType = Partial<{ @@ -33,6 +34,7 @@ export type ErrorMessage = Partial<{ targetClusterErrorMessage: number; subscriptionGroupErrorMessage: number; peerStatusErrorMessage: number; + syncDelayWarningMessage: number; }>; export type FailoverAndRelocateState = { @@ -72,6 +74,7 @@ export const failoverAndRelocateState = ( targetClusterErrorMessage: 0, subscriptionGroupErrorMessage: 0, peerStatusErrorMessage: 0, + syncDelayWarningMessage: 0, }, actionErrorMessage: {}, }); diff --git a/packages/mco/components/protected-applications/list-page.tsx b/packages/mco/components/protected-applications/list-page.tsx index a9b9a8e27..33162136f 100644 --- a/packages/mco/components/protected-applications/list-page.tsx +++ b/packages/mco/components/protected-applications/list-page.tsx @@ -34,6 +34,7 @@ import { getLastAppDeploymentClusterName, getDRPolicyName, getReplicationType, + getReplicationHealth, } from '../../utils'; import { EmptyRowMessage, @@ -50,7 +51,6 @@ import { getHeaderColumns, getColumnNames, getRowActions, - getReplicationHealth, isFailingOrRelocating, ReplicationHealthMap, getAppWorstSyncStatus, diff --git a/packages/mco/components/protected-applications/utils.tsx b/packages/mco/components/protected-applications/utils.tsx index ef48d5497..de8da68b6 100644 --- a/packages/mco/components/protected-applications/utils.tsx +++ b/packages/mco/components/protected-applications/utils.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { getTimeDifferenceInSeconds } from '@odf/shared/details-page/datetime'; import { ActionDropdownItems } from '@odf/shared/dropdown/action-dropdown'; import { getName, getNamespace } from '@odf/shared/selectors'; import { @@ -22,14 +21,12 @@ import { IAction } from '@patternfly/react-table'; import { VOLUME_REPLICATION_HEALTH, DRPC_STATUS, - LEAST_SECONDS_IN_PROMETHEUS, DR_BASE_ROUTE, DRActionType, REPLICATION_TYPE, } from '../../constants'; import { DRPlacementControlModel } from '../../models'; import { DRPlacementControlKind, Progression } from '../../types'; -import { getVolumeReplicationHealth } from '../../utils'; import { DiscoveredApplicationParser as DiscoveredApplicationModal } from '../modals/app-failover-relocate/parser/discovered-application-parser'; import RemoveDisasterRecoveryModal from '../modals/remove-disaster-recovery/remove-disaster-recovery'; @@ -85,20 +82,6 @@ export const isCleanupPending = (drpc: DRPlacementControlKind): boolean => drpc?.status?.phase as DRPC_STATUS ) && drpc?.status?.progression === Progression.WaitOnUserToCleanUp; -export const getReplicationHealth = ( - lastSyncTime: string, - schedulingInterval: string, - replicationType: REPLICATION_TYPE -): VOLUME_REPLICATION_HEALTH => { - if (replicationType === REPLICATION_TYPE.SYNC) { - return VOLUME_REPLICATION_HEALTH.HEALTHY; - } - const seconds = !!lastSyncTime - ? getTimeDifferenceInSeconds(lastSyncTime) - : LEAST_SECONDS_IN_PROMETHEUS; - return getVolumeReplicationHealth(seconds, schedulingInterval)[0]; -}; - export type ReplicationHealthMap = { title: string; icon: JSX.Element; diff --git a/packages/mco/utils/disaster-recovery.tsx b/packages/mco/utils/disaster-recovery.tsx index 5c09e68c8..852df91a6 100644 --- a/packages/mco/utils/disaster-recovery.tsx +++ b/packages/mco/utils/disaster-recovery.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { daysToSeconds, + getTimeDifferenceInSeconds, hoursToSeconds, minutesToSeconds, } from '@odf/shared/details-page/datetime'; @@ -36,6 +37,7 @@ import { PLACEMENT_RULE_REF_LABEL, MCV_NAME_TEMPLATE, NAME_NAMESPACE_SPLIT_CHAR, + LEAST_SECONDS_IN_PROMETHEUS, } from '../constants'; import { DRPC_NAMESPACE_ANNOTATION, @@ -246,6 +248,20 @@ export const getDRPoliciesCount = (drPolicies: DRPolicyMap) => export const getReplicationType = (interval: string) => interval !== '0m' ? REPLICATION_TYPE.ASYNC : REPLICATION_TYPE.SYNC; +export const getReplicationHealth = ( + lastSyncTime: string, + schedulingInterval: string, + replicationType: REPLICATION_TYPE +): VOLUME_REPLICATION_HEALTH => { + if (replicationType === REPLICATION_TYPE.SYNC) { + return VOLUME_REPLICATION_HEALTH.HEALTHY; + } + const seconds = !!lastSyncTime + ? getTimeDifferenceInSeconds(lastSyncTime) + : LEAST_SECONDS_IN_PROMETHEUS; + return getVolumeReplicationHealth(seconds, schedulingInterval)[0]; +}; + export const getPlacementKind = (subscription: ACMSubscriptionKind) => subscription?.spec?.placement?.placementRef?.kind;