diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 74d5ef925..802bfd590 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -93,7 +93,7 @@ export class CapabilityNames { public static readonly EnableDataMasking: string = "EnableDataMasking"; public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking"; public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures"; - public static readonly EnableOnlineCopyFeature: string = "EnableOnlineCopyFeature"; + public static readonly EnableOnlineCopyFeature: string = "EnableOnlineContainerCopy"; } export enum CapacityMode { diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx index 6394359db..14198b700 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx @@ -1,5 +1,6 @@ import React from "react"; import { userContext } from "UserContext"; +import { logError } from "../../../Common/Logger"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { cancel, @@ -159,7 +160,8 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: onSuccess(); return response; } catch (error) { - console.error("Error submitting create copy job:", error); + const errorMessage = error.message || "Error submitting create copy job. Please try again later."; + logError(errorMessage, "CopyJob/CopyJobActions.submitCreateCopyJob"); throw error; } }; @@ -198,8 +200,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro pattern, `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`, ); - - console.error(`Error updating copy job status: ${normalizedErrorMessage}`); + logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus"); throw error; } }; diff --git a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts b/src/Explorer/ContainerCopy/ContainerCopyMessages.ts index 80c9c658f..0be496783 100644 --- a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts +++ b/src/Explorer/ContainerCopy/ContainerCopyMessages.ts @@ -48,8 +48,10 @@ export default { // Assign Permissions Screen assignPermissions: { - description: + crossAccountDescription: "To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.", + intraAccountOnlineDescription: (accountName: string) => + `Follow the steps below to enable online copy on your "${accountName}" account.`, }, toggleBtn: { onText: "On", @@ -115,7 +117,7 @@ export default { }, onlineCopyEnabled: { title: "Online copy enabled", - description: (accountName: string) => `Use Azure CLI to enable Online copy on "${accountName}".`, + description: (accountName: string) => `Enable Online copy on "${accountName}".`, hrefText: "Learn more about online copy jobs", href: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy", buttonText: "Enable Online Copy", diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx index 16f17598b..d89f839e2 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx @@ -39,16 +39,23 @@ const getInitialCopyJobState = (): CopyJobContextState => { const CopyJobContextProvider: React.FC = (props) => { const [copyJobState, setCopyJobState] = React.useState(getInitialCopyJobState()); const [flow, setFlow] = React.useState(null); + const [contextError, setContextError] = React.useState(null); const resetCopyJobState = () => { setCopyJobState(getInitialCopyJobState()); }; - return ( - - {props.children} - - ); + const contextValue: CopyJobContextProviderType = { + contextError, + setContextError, + copyJobState, + setCopyJobState, + flow, + setFlow, + resetCopyJobState, + }; + + return {props.children}; }; export default CopyJobContextProvider; diff --git a/src/Explorer/ContainerCopy/CopyJobUtils.ts b/src/Explorer/ContainerCopy/CopyJobUtils.ts index a37fa660a..1a9e46aad 100644 --- a/src/Explorer/ContainerCopy/CopyJobUtils.ts +++ b/src/Explorer/ContainerCopy/CopyJobUtils.ts @@ -106,7 +106,7 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) { return null; } const pattern = new RegExp( - "/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)", + "/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB?/databaseAccounts/([^/]+)", "i", ); const matches = accountId.match(pattern); @@ -114,3 +114,13 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) { const [_, subscriptionId, resourceGroup, accountName] = matches || []; return { subscriptionId, resourceGroup, accountName }; } + +export function isIntraAccountCopy(sourceAccountId: string | undefined, targetAccountId: string | undefined): boolean { + const sourceAccountDetails = getAccountDetailsFromResourceId(sourceAccountId); + const targetAccountDetails = getAccountDetailsFromResourceId(targetAccountId); + return ( + sourceAccountDetails?.subscriptionId === targetAccountDetails?.subscriptionId && + sourceAccountDetails?.resourceGroup === targetAccountDetails?.resourceGroup && + sourceAccountDetails?.accountName === targetAccountDetails?.accountName + ); +} diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx index 4c3f5e98d..160c6d973 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx @@ -1,5 +1,6 @@ import { Link, Stack, Text, Toggle } from "@fluentui/react"; import React, { useCallback } from "react"; +import { logError } from "../../../../../Common/Logger"; import { assignRole } from "../../../../../Utils/arm/RbacUtils"; import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; @@ -21,7 +22,7 @@ type AddReadPermissionToDefaultIdentityProps = Partial; const AddReadPermissionToDefaultIdentity: React.FC = () => { const [loading, setLoading] = React.useState(false); - const { copyJobState, setCopyJobState } = useCopyJobContext(); + const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); const [readPermissionAssigned, onToggle] = useToggle(false); const handleAddReadPermission = useCallback(async () => { @@ -48,11 +49,14 @@ const AddReadPermissionToDefaultIdentity: React.FC diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx index 6a5e69154..1f3861753 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx @@ -6,6 +6,7 @@ import WarningIcon from "../../../../../../images/warning.svg"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; +import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection"; @@ -39,6 +40,8 @@ const AssignPermissions = () => { [], ); + const isSameAccount = isIntraAccountCopy(copyJobState?.source?.account?.id, copyJobState?.target?.account?.id); + useEffect(() => { const firstIncompleteSection = permissionSections.find((section) => !section.completed); const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : []; @@ -49,7 +52,13 @@ const AssignPermissions = () => { return ( - {ContainerCopyMessages.assignPermissions.description} + + {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online + ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( + copyJobState?.source?.account?.name || "", + ) + : ContainerCopyMessages.assignPermissions.crossAccountDescription} + {permissionSections?.length === 0 ? ( ) : ( diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx index f616df1c6..9d9279e1a 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx @@ -1,7 +1,10 @@ import { Link, PrimaryButton, Stack } from "@fluentui/react"; +import { CapabilityNames } from "Common/Constants"; import { DatabaseAccount } from "Contracts/DataModels"; import React from "react"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; +import { logError } from "../../../../../Common/Logger"; +import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; @@ -19,8 +22,10 @@ const OnlineCopyEnabled: React.FC = () => { const [showRefreshButton, setShowRefreshButton] = React.useState(false); const intervalRef = React.useRef(null); const timeoutRef = React.useRef(null); - const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext(); + const { setContextError, copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext(); const selectedSourceAccount = source?.account; + const sourceAccountCapabilities = selectedSourceAccount?.properties?.capabilities ?? []; + const { subscriptionId: sourceSubscriptionId, resourceGroup: sourceResourceGroup, @@ -38,16 +43,24 @@ const OnlineCopyEnabled: React.FC = () => { setLoading(false); } } catch (error) { - console.error("Error fetching source account after enabling online copy:", error); - setLoading(false); + const errorMessage = + error.message || "Error fetching source account after enabling online copy. Please try again later."; + logError(errorMessage, "CopyJob/OnlineCopyEnabled.handleFetchAccount"); + setContextError(errorMessage); + clearAccountFetchInterval(); } }; - const clearIntervalAndShowRefresh = () => { + const clearAccountFetchInterval = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } + setLoading(false); + }; + + const clearIntervalAndShowRefresh = () => { + clearAccountFetchInterval(); setShowRefreshButton(true); }; @@ -56,18 +69,42 @@ const OnlineCopyEnabled: React.FC = () => { handleFetchAccount(); }; + const handleOnlineCopyEnable = async () => { + setLoading(true); + setShowRefreshButton(false); + + try { + await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { + properties: { + enableAllVersionsAndDeletesChangeFeed: true, + }, + }); + + await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { + properties: { + capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }], + }, + }); + + intervalRef.current = setInterval(() => { + handleFetchAccount(); + }, 30 * 1000); + + timeoutRef.current = setTimeout( + () => { + clearIntervalAndShowRefresh(); + }, + 10 * 60 * 1000, + ); + } catch (error) { + const errorMessage = error.message || "Failed to enable online copy feature. Please try again later."; + logError(errorMessage, "CopyJob/OnlineCopyEnabled.handleOnlineCopyEnable"); + setContextError(errorMessage); + setLoading(false); + } + }; + React.useEffect(() => { - intervalRef.current = setInterval(() => { - handleFetchAccount(); - }, 30 * 1000); - - timeoutRef.current = setTimeout( - () => { - clearIntervalAndShowRefresh(); - }, - 15 * 60 * 1000, - ); - return () => { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -89,32 +126,7 @@ const OnlineCopyEnabled: React.FC = () => { -
-          
-            {`# Set shell variables
-$resourceGroupName = 
-$accountName = 
-$EnableOnlineContainerCopy = "EnableOnlineContainerCopy"
-
-# List down existing capabilities of your account
-$cosmosdb = az cosmosdb show --resource-group $resourceGroupName --name $accountName
-
-$capabilities = (($cosmosdb | ConvertFrom-Json).capabilities)
-
-# Append EnableOnlineContainerCopy capability in the list of capabilities
-$capabilitiesToAdd = @()
-foreach ($item in $capabilities) {
-  $capabilitiesToAdd += $item.name
-}
-$capabilitiesToAdd += $EnableOnlineContainerCopy
-
-# Update Cosmos DB account
-az cosmosdb update --capabilities $capabilitiesToAdd -n $accountName -g $resourceGroupName`}
-          
-        
-
- {showRefreshButton && ( - + {showRefreshButton ? ( - - )} + ) : ( + + )} +
); }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx index eb072d92b..30c104ec2 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx @@ -2,6 +2,7 @@ import { Link, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DatabaseAccount } from "Contracts/DataModels"; import React, { useEffect, useRef, useState } from "react"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; +import { logError } from "../../../../../Common/Logger"; import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; @@ -63,17 +64,23 @@ const PointInTimeRestore: React.FC = () => { setLoading(false); } } catch (error) { - console.error("Error fetching source account after Point-in-Time Restore:", error); - setLoading(false); + const errorMessage = + error.message || "Error fetching source account after Point-in-Time Restore. Please try again later."; + logError(errorMessage, "CopyJob/PointInTimeRestore.handleFetchAccount"); + clearAccountFetchInterval(); } }; - const clearIntervalAndShowRefresh = () => { + const clearAccountFetchInterval = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setLoading(false); + }; + + const clearIntervalAndShowRefresh = () => { + clearAccountFetchInterval(); setShowRefreshButton(true); }; @@ -95,7 +102,7 @@ const PointInTimeRestore: React.FC = () => { () => { clearIntervalAndShowRefresh(); }, - 15 * 60 * 1000, + 10 * 60 * 1000, ); }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.tsx index 08c79da3b..9ac826e8c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/useManagedIdentity.tsx @@ -1,5 +1,6 @@ import { DatabaseAccount } from "Contracts/DataModels"; import { useCallback, useState } from "react"; +import { logError } from "../../../../../../Common/Logger"; import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; @@ -19,7 +20,7 @@ interface UseManagedIdentityUpdaterReturn { const useManagedIdentity = ( updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"], ): UseManagedIdentityUpdaterReturn => { - const { copyJobState, setCopyJobState } = useCopyJobContext(); + const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); const [loading, setLoading] = useState(false); const handleAddSystemIdentity = useCallback(async (): Promise => { @@ -40,7 +41,9 @@ const useManagedIdentity = ( })); } } catch (error) { - console.error("Error enabling system-assigned managed identity:", error); + const errorMessage = error.message || "Error enabling system-assigned managed identity. Please try again later."; + logError(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity"); + setContextError(errorMessage); } finally { setLoading(false); } diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.tsx index 53db66ff8..01687101c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/hooks/usePermissionsSection.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { CapabilityNames } from "../../../../../../Common/Constants"; import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils"; import ContainerCopyMessages from "../../../../ContainerCopyMessages"; -import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; +import { getAccountDetailsFromResourceId, isIntraAccountCopy } from "../../../../CopyJobUtils"; import { BackupPolicyType, CopyJobMigrationType, @@ -139,7 +139,9 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon const isValidatingRef = useRef(false); const sectionToValidate = useMemo(() => { - const baseSections = sourceAccountId === targetAccountId ? [] : [...PERMISSION_SECTIONS_CONFIG]; + const isSameAccount = isIntraAccountCopy(sourceAccountId, targetAccountId); + + const baseSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG]; if (state.migrationType === CopyJobMigrationType.Online) { return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS]; } diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/CreateCopyJobScreens.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/CreateCopyJobScreens.tsx index cd7f39bf1..1ee9a89be 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/CreateCopyJobScreens.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/CreateCopyJobScreens.tsx @@ -1,5 +1,6 @@ import { MessageBar, MessageBarType, Stack } from "@fluentui/react"; import React from "react"; +import { useCopyJobContext } from "../../Context/CopyJobContext"; import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation"; import NavigationControls from "./Components/NavigationControls"; @@ -12,24 +13,23 @@ const CreateCopyJobScreens: React.FC = () => { handlePrevious, handleCancel, primaryBtnText, - error, - setError, } = useCopyJobNavigation(); + const { contextError, setContextError } = useCopyJobContext(); return ( - {error && ( + {contextError && ( setError(null)} + onDismiss={() => setContextError(null)} dismissButtonAriaLabel="Close" truncated={true} overflowButtonAriaLabel="See more" > - {error} + {contextError} )} {currentScreen?.component} diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx index 16d17c33b..adb36b3a1 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/SelectAccount/Utils/selectAccountUtils.tsx @@ -2,6 +2,7 @@ import React from "react"; import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes"; +import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache"; export function useDropdownOptions( subscriptions: Subscription[], @@ -36,6 +37,7 @@ export function useDropdownOptions( type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"]; export function useEventHandlers(setCopyJobState: setCopyJobStateType) { + const { setValidationCache } = useCopyJobPrerequisitesCache(); const handleSelectSourceAccount = React.useCallback( (type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => { setCopyJobState((prevState: CopyJobContextState) => { @@ -60,8 +62,9 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) { } return prevState; }); + setValidationCache(new Map()); }, - [setCopyJobState], + [setCopyJobState, setValidationCache], ); const handleMigrationTypeChange = React.useCallback( @@ -70,8 +73,9 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) { ...prevState, migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online, })); + setValidationCache(new Map()); }, - [setCopyJobState], + [setCopyJobState, setValidationCache], ); return { handleSelectSourceAccount, handleMigrationTypeChange }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts b/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts index 4bd552455..ea313ae12 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Utils/useCopyJobNavigation.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo, useReducer, useState } from "react"; import { useSidePanel } from "../../../../hooks/useSidePanel"; import { submitCreateCopyJob } from "../../Actions/CopyJobActions"; import { useCopyJobContext } from "../../Context/CopyJobContext"; +import { isIntraAccountCopy } from "../../CopyJobUtils"; import { CopyJobMigrationType } from "../../Enums/CopyJobEnums"; import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache"; import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList"; @@ -33,8 +34,7 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt export function useCopyJobNavigation() { const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { copyJobState, resetCopyJobState } = useCopyJobContext(); + const { copyJobState, resetCopyJobState, setContextError } = useCopyJobContext(); const screens = useCreateCopyJobScreensList(); const { validationCache: cache } = useCopyJobPrerequisitesCache(); const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] }); @@ -71,18 +71,13 @@ export function useCopyJobNavigation() { containerId: container?.containerId || "", }); - const isSameAccount = ( - sourceIds: ReturnType, - targetIds: ReturnType, - ) => sourceIds.accountId === targetIds.accountId; - const areContainersIdentical = () => { const { source, target } = copyJobState; const sourceIds = getContainerIdentifiers(source); const targetIds = getContainerIdentifiers(target); return ( - isSameAccount(sourceIds, targetIds) && + isIntraAccountCopy(sourceIds.accountId, targetIds.accountId) && sourceIds.databaseId === targetIds.databaseId && sourceIds.containerId === targetIds.containerId ); @@ -90,9 +85,10 @@ export function useCopyJobNavigation() { const shouldNotShowPermissionScreen = () => { const { source, target, migrationType } = copyJobState; + const sourceIds = getContainerIdentifiers(source); + const targetIds = getContainerIdentifiers(target); return ( - migrationType === CopyJobMigrationType.Offline && - isSameAccount(getContainerIdentifiers(source), getContainerIdentifiers(target)) + migrationType === CopyJobMigrationType.Offline && isIntraAccountCopy(sourceIds.accountId, targetIds.accountId) ); }; @@ -105,7 +101,7 @@ export function useCopyJobNavigation() { error instanceof Error ? error.message || "Failed to create copy job. Please try again later." : "Failed to create copy job. Please try again later."; - setError(errorMessage); + setContextError(errorMessage); } finally { setIsLoading(false); } @@ -113,11 +109,13 @@ export function useCopyJobNavigation() { const handlePrimary = useCallback(() => { if (currentScreenKey === SCREEN_KEYS.SelectSourceAndTargetContainers && areContainersIdentical()) { - setError("Source and destination containers cannot be the same. Please select different containers to proceed."); + setContextError( + "Source and destination containers cannot be the same. Please select different containers to proceed.", + ); return; } - setError(null); + setContextError(null); const transitions = { [SCREEN_KEYS.SelectAccount]: shouldNotShowPermissionScreen() ? SCREEN_KEYS.SelectSourceAndTargetContainers @@ -146,7 +144,5 @@ export function useCopyJobNavigation() { handlePrevious, handleCancel, primaryBtnText, - error, - setError, }; } diff --git a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobActionMenu.tsx b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobActionMenu.tsx index 57bc99acd..b43307be9 100644 --- a/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobActionMenu.tsx +++ b/src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobActionMenu.tsx @@ -11,7 +11,14 @@ interface CopyJobActionMenuProps { const CopyJobActionMenu: React.FC = ({ job, handleClick }) => { const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null); - if ([CopyJobStatusType.Completed, CopyJobStatusType.Cancelled].includes(job.Status)) { + if ( + [ + CopyJobStatusType.Completed, + CopyJobStatusType.Cancelled, + CopyJobStatusType.Failed, + CopyJobStatusType.Faulted, + ].includes(job.Status) + ) { return null; } @@ -55,7 +62,7 @@ const CopyJobActionMenu: React.FC = ({ job, handleClick [CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status) ) { const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume); - if (job.Mode === CopyJobMigrationType.Online) { + if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) { filteredItems.push({ key: CopyJobActions.complete, text: ContainerCopyMessages.MonitorJobs.Actions.complete, @@ -67,7 +74,7 @@ const CopyJobActionMenu: React.FC = ({ job, handleClick return filteredItems; } - if ([CopyJobStatusType.Failed, CopyJobStatusType.Faulted, CopyJobStatusType.Skipped].includes(job.Status)) { + if ([CopyJobStatusType.Skipped].includes(job.Status)) { return baseItems.filter((item) => item.key === CopyJobActions.resume); } diff --git a/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts b/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts index 1c031bb33..30f6ee2e4 100644 --- a/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts +++ b/src/Explorer/ContainerCopy/Types/CopyJobTypes.ts @@ -73,6 +73,8 @@ export interface CopyJobFlowType { } export interface CopyJobContextProviderType { + contextError: string | null; + setContextError: React.Dispatch>; flow: CopyJobFlowType; setFlow: React.Dispatch>; copyJobState: CopyJobContextState | null;