Refactor Container Copy Jobs for Intra-account copy and Online operations (#2258)

* fix: for intra-account copy, validation screen should not visible

* fix: handle online operations using a button instead manual CLI commands

* reset validation cache on leaving of permission screen

* update same account logic

* fix: update job action menu list and permission screen messages

* uplift error handling to context level

* use of logError instead of console.error
This commit is contained in:
BChoudhury-ms
2025-11-19 22:41:13 +05:30
committed by GitHub
parent beccab02e7
commit 125b1c86b7
16 changed files with 166 additions and 92 deletions

View File

@@ -93,7 +93,7 @@ export class CapabilityNames {
public static readonly EnableDataMasking: string = "EnableDataMasking"; public static readonly EnableDataMasking: string = "EnableDataMasking";
public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking"; public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking";
public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures"; public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures";
public static readonly EnableOnlineCopyFeature: string = "EnableOnlineCopyFeature"; public static readonly EnableOnlineCopyFeature: string = "EnableOnlineContainerCopy";
} }
export enum CapacityMode { export enum CapacityMode {

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logError } from "../../../Common/Logger";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { import {
cancel, cancel,
@@ -159,7 +160,8 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
onSuccess(); onSuccess();
return response; return response;
} catch (error) { } 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; throw error;
} }
}; };
@@ -198,8 +200,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
pattern, pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`, `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
); );
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
throw error; throw error;
} }
}; };

View File

@@ -48,8 +48,10 @@ export default {
// Assign Permissions Screen // Assign Permissions Screen
assignPermissions: { 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.", "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: { toggleBtn: {
onText: "On", onText: "On",
@@ -115,7 +117,7 @@ export default {
}, },
onlineCopyEnabled: { onlineCopyEnabled: {
title: "Online copy enabled", 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", 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", 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", buttonText: "Enable Online Copy",

View File

@@ -39,16 +39,23 @@ const getInitialCopyJobState = (): CopyJobContextState => {
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => { const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState()); const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null); const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null);
const [contextError, setContextError] = React.useState<string | null>(null);
const resetCopyJobState = () => { const resetCopyJobState = () => {
setCopyJobState(getInitialCopyJobState()); setCopyJobState(getInitialCopyJobState());
}; };
return ( const contextValue: CopyJobContextProviderType = {
<CopyJobContext.Provider value={{ copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}> contextError,
{props.children} setContextError,
</CopyJobContext.Provider> copyJobState,
); setCopyJobState,
flow,
setFlow,
resetCopyJobState,
};
return <CopyJobContext.Provider value={contextValue}>{props.children}</CopyJobContext.Provider>;
}; };
export default CopyJobContextProvider; export default CopyJobContextProvider;

View File

@@ -106,7 +106,7 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) {
return null; return null;
} }
const pattern = new RegExp( const pattern = new RegExp(
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)", "/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB?/databaseAccounts/([^/]+)",
"i", "i",
); );
const matches = accountId.match(pattern); const matches = accountId.match(pattern);
@@ -114,3 +114,13 @@ export function getAccountDetailsFromResourceId(accountId: string | undefined) {
const [_, subscriptionId, resourceGroup, accountName] = matches || []; const [_, subscriptionId, resourceGroup, accountName] = matches || [];
return { subscriptionId, resourceGroup, accountName }; 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
);
}

View File

@@ -1,5 +1,6 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { logError } from "../../../../../Common/Logger";
import { assignRole } from "../../../../../Utils/arm/RbacUtils"; import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
@@ -21,7 +22,7 @@ type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => { const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false); const [readPermissionAssigned, onToggle] = useToggle(false);
const handleAddReadPermission = useCallback(async () => { const handleAddReadPermission = useCallback(async () => {
@@ -48,11 +49,14 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
})); }));
} }
} catch (error) { } catch (error) {
console.error("Error assigning read permission to default identity:", error); const errorMessage =
error.message || "Error assigning read permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
setContextError(errorMessage);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [copyJobState, setCopyJobState]); }, [copyJobState, setCopyJobState, setContextError]);
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>

View File

@@ -6,6 +6,7 @@ import WarningIcon from "../../../../../../images/warning.svg";
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../../CopyJobUtils";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection"; import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
@@ -39,6 +40,8 @@ const AssignPermissions = () => {
[], [],
); );
const isSameAccount = isIntraAccountCopy(copyJobState?.source?.account?.id, copyJobState?.target?.account?.id);
useEffect(() => { useEffect(() => {
const firstIncompleteSection = permissionSections.find((section) => !section.completed); const firstIncompleteSection = permissionSections.find((section) => !section.completed);
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : []; const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : [];
@@ -49,7 +52,13 @@ const AssignPermissions = () => {
return ( return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}> <Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span>{ContainerCopyMessages.assignPermissions.description}</span> <span>
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
copyJobState?.source?.account?.name || "",
)
: ContainerCopyMessages.assignPermissions.crossAccountDescription}
</span>
{permissionSections?.length === 0 ? ( {permissionSections?.length === 0 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} /> <ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} />
) : ( ) : (

View File

@@ -1,7 +1,10 @@
import { Link, PrimaryButton, Stack } from "@fluentui/react"; import { Link, PrimaryButton, Stack } from "@fluentui/react";
import { CapabilityNames } from "Common/Constants";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; 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 ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
@@ -19,8 +22,10 @@ const OnlineCopyEnabled: React.FC = () => {
const [showRefreshButton, setShowRefreshButton] = React.useState(false); const [showRefreshButton, setShowRefreshButton] = React.useState(false);
const intervalRef = React.useRef<NodeJS.Timeout | null>(null); const intervalRef = React.useRef<NodeJS.Timeout | null>(null);
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null); const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext(); const { setContextError, copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
const selectedSourceAccount = source?.account; const selectedSourceAccount = source?.account;
const sourceAccountCapabilities = selectedSourceAccount?.properties?.capabilities ?? [];
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
@@ -38,16 +43,24 @@ const OnlineCopyEnabled: React.FC = () => {
setLoading(false); setLoading(false);
} }
} catch (error) { } catch (error) {
console.error("Error fetching source account after enabling online copy:", error); const errorMessage =
setLoading(false); 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) { if (intervalRef.current) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
intervalRef.current = null; intervalRef.current = null;
} }
setLoading(false);
};
const clearIntervalAndShowRefresh = () => {
clearAccountFetchInterval();
setShowRefreshButton(true); setShowRefreshButton(true);
}; };
@@ -56,7 +69,23 @@ const OnlineCopyEnabled: React.FC = () => {
handleFetchAccount(); handleFetchAccount();
}; };
React.useEffect(() => { 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(() => { intervalRef.current = setInterval(() => {
handleFetchAccount(); handleFetchAccount();
}, 30 * 1000); }, 30 * 1000);
@@ -65,9 +94,17 @@ const OnlineCopyEnabled: React.FC = () => {
() => { () => {
clearIntervalAndShowRefresh(); clearIntervalAndShowRefresh();
}, },
15 * 60 * 1000, 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(() => {
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
@@ -89,32 +126,7 @@ const OnlineCopyEnabled: React.FC = () => {
</Link> </Link>
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<pre style={{ backgroundColor: "#f5f5f5", padding: "10px", borderRadius: "4px", overflow: "auto" }}> {showRefreshButton ? (
<code>
{`# Set shell variables
$resourceGroupName = <azure_resource_group>
$accountName = <azure_cosmos_db_account_name>
$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`}
</code>
</pre>
</Stack.Item>
{showRefreshButton && (
<Stack.Item>
<PrimaryButton <PrimaryButton
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={ContainerCopyMessages.refreshButtonLabel}
@@ -122,8 +134,16 @@ az cosmosdb update --capabilities $capabilitiesToAdd -n $accountName -g $resourc
onClick={handleRefresh} onClick={handleRefresh}
disabled={loading} disabled={loading}
/> />
</Stack.Item> ) : (
<PrimaryButton
className="fullWidth"
text={loading ? "" : ContainerCopyMessages.onlineCopyEnabled.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading}
onClick={handleOnlineCopyEnable}
/>
)} )}
</Stack.Item>
</Stack> </Stack>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { logError } from "../../../../../Common/Logger";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
@@ -63,17 +64,23 @@ const PointInTimeRestore: React.FC = () => {
setLoading(false); setLoading(false);
} }
} catch (error) { } catch (error) {
console.error("Error fetching source account after Point-in-Time Restore:", error); const errorMessage =
setLoading(false); 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) { if (intervalRef.current) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
intervalRef.current = null; intervalRef.current = null;
} }
setLoading(false); setLoading(false);
};
const clearIntervalAndShowRefresh = () => {
clearAccountFetchInterval();
setShowRefreshButton(true); setShowRefreshButton(true);
}; };
@@ -95,7 +102,7 @@ const PointInTimeRestore: React.FC = () => {
() => { () => {
clearIntervalAndShowRefresh(); clearIntervalAndShowRefresh();
}, },
15 * 60 * 1000, 10 * 60 * 1000,
); );
}; };

View File

@@ -1,5 +1,6 @@
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { logError } from "../../../../../../Common/Logger";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
@@ -19,7 +20,7 @@ interface UseManagedIdentityUpdaterReturn {
const useManagedIdentity = ( const useManagedIdentity = (
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"], updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"],
): UseManagedIdentityUpdaterReturn => { ): UseManagedIdentityUpdaterReturn => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const handleAddSystemIdentity = useCallback(async (): Promise<void> => { const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
@@ -40,7 +41,9 @@ const useManagedIdentity = (
})); }));
} }
} catch (error) { } 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 { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { CapabilityNames } from "../../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../../Common/Constants";
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils"; import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId, isIntraAccountCopy } from "../../../../CopyJobUtils";
import { import {
BackupPolicyType, BackupPolicyType,
CopyJobMigrationType, CopyJobMigrationType,
@@ -139,7 +139,9 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
const isValidatingRef = useRef(false); const isValidatingRef = useRef(false);
const sectionToValidate = useMemo(() => { 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) { if (state.migrationType === CopyJobMigrationType.Online) {
return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS]; return [...baseSections, ...PERMISSION_SECTIONS_FOR_ONLINE_JOBS];
} }

View File

@@ -1,5 +1,6 @@
import { MessageBar, MessageBarType, Stack } from "@fluentui/react"; import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import { useCopyJobContext } from "../../Context/CopyJobContext";
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation"; import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
import NavigationControls from "./Components/NavigationControls"; import NavigationControls from "./Components/NavigationControls";
@@ -12,24 +13,23 @@ const CreateCopyJobScreens: React.FC = () => {
handlePrevious, handlePrevious,
handleCancel, handleCancel,
primaryBtnText, primaryBtnText,
error,
setError,
} = useCopyJobNavigation(); } = useCopyJobNavigation();
const { contextError, setContextError } = useCopyJobContext();
return ( return (
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer"> <Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
<Stack.Item className="createCopyJobScreensContent"> <Stack.Item className="createCopyJobScreensContent">
{error && ( {contextError && (
<MessageBar <MessageBar
className="createCopyJobErrorMessageBar" className="createCopyJobErrorMessageBar"
messageBarType={MessageBarType.blocked} messageBarType={MessageBarType.blocked}
isMultiline={false} isMultiline={false}
onDismiss={() => setError(null)} onDismiss={() => setContextError(null)}
dismissButtonAriaLabel="Close" dismissButtonAriaLabel="Close"
truncated={true} truncated={true}
overflowButtonAriaLabel="See more" overflowButtonAriaLabel="See more"
> >
{error} {contextError}
</MessageBar> </MessageBar>
)} )}
{currentScreen?.component} {currentScreen?.component}

View File

@@ -2,6 +2,7 @@ import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels"; import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
export function useDropdownOptions( export function useDropdownOptions(
subscriptions: Subscription[], subscriptions: Subscription[],
@@ -36,6 +37,7 @@ export function useDropdownOptions(
type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"]; type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
export function useEventHandlers(setCopyJobState: setCopyJobStateType) { export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
const { setValidationCache } = useCopyJobPrerequisitesCache();
const handleSelectSourceAccount = React.useCallback( const handleSelectSourceAccount = React.useCallback(
(type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => { (type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => {
setCopyJobState((prevState: CopyJobContextState) => { setCopyJobState((prevState: CopyJobContextState) => {
@@ -60,8 +62,9 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
} }
return prevState; return prevState;
}); });
setValidationCache(new Map<string, boolean>());
}, },
[setCopyJobState], [setCopyJobState, setValidationCache],
); );
const handleMigrationTypeChange = React.useCallback( const handleMigrationTypeChange = React.useCallback(
@@ -70,8 +73,9 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
...prevState, ...prevState,
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online, migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
})); }));
setValidationCache(new Map<string, boolean>());
}, },
[setCopyJobState], [setCopyJobState, setValidationCache],
); );
return { handleSelectSourceAccount, handleMigrationTypeChange }; return { handleSelectSourceAccount, handleMigrationTypeChange };

View File

@@ -2,6 +2,7 @@ import { useCallback, useMemo, useReducer, useState } from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel"; import { useSidePanel } from "../../../../hooks/useSidePanel";
import { submitCreateCopyJob } from "../../Actions/CopyJobActions"; import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
import { useCopyJobContext } from "../../Context/CopyJobContext"; import { useCopyJobContext } from "../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../CopyJobUtils";
import { CopyJobMigrationType } from "../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../Enums/CopyJobEnums";
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache"; import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList"; import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
@@ -33,8 +34,7 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
export function useCopyJobNavigation() { export function useCopyJobNavigation() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const { copyJobState, resetCopyJobState, setContextError } = useCopyJobContext();
const { copyJobState, resetCopyJobState } = useCopyJobContext();
const screens = useCreateCopyJobScreensList(); const screens = useCreateCopyJobScreensList();
const { validationCache: cache } = useCopyJobPrerequisitesCache(); const { validationCache: cache } = useCopyJobPrerequisitesCache();
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] }); const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
@@ -71,18 +71,13 @@ export function useCopyJobNavigation() {
containerId: container?.containerId || "", containerId: container?.containerId || "",
}); });
const isSameAccount = (
sourceIds: ReturnType<typeof getContainerIdentifiers>,
targetIds: ReturnType<typeof getContainerIdentifiers>,
) => sourceIds.accountId === targetIds.accountId;
const areContainersIdentical = () => { const areContainersIdentical = () => {
const { source, target } = copyJobState; const { source, target } = copyJobState;
const sourceIds = getContainerIdentifiers(source); const sourceIds = getContainerIdentifiers(source);
const targetIds = getContainerIdentifiers(target); const targetIds = getContainerIdentifiers(target);
return ( return (
isSameAccount(sourceIds, targetIds) && isIntraAccountCopy(sourceIds.accountId, targetIds.accountId) &&
sourceIds.databaseId === targetIds.databaseId && sourceIds.databaseId === targetIds.databaseId &&
sourceIds.containerId === targetIds.containerId sourceIds.containerId === targetIds.containerId
); );
@@ -90,9 +85,10 @@ export function useCopyJobNavigation() {
const shouldNotShowPermissionScreen = () => { const shouldNotShowPermissionScreen = () => {
const { source, target, migrationType } = copyJobState; const { source, target, migrationType } = copyJobState;
const sourceIds = getContainerIdentifiers(source);
const targetIds = getContainerIdentifiers(target);
return ( return (
migrationType === CopyJobMigrationType.Offline && migrationType === CopyJobMigrationType.Offline && isIntraAccountCopy(sourceIds.accountId, targetIds.accountId)
isSameAccount(getContainerIdentifiers(source), getContainerIdentifiers(target))
); );
}; };
@@ -105,7 +101,7 @@ export function useCopyJobNavigation() {
error instanceof Error error instanceof Error
? error.message || "Failed to create copy job. Please try again later." ? error.message || "Failed to create copy job. Please try again later."
: "Failed to create copy job. Please try again later."; : "Failed to create copy job. Please try again later.";
setError(errorMessage); setContextError(errorMessage);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -113,11 +109,13 @@ export function useCopyJobNavigation() {
const handlePrimary = useCallback(() => { const handlePrimary = useCallback(() => {
if (currentScreenKey === SCREEN_KEYS.SelectSourceAndTargetContainers && areContainersIdentical()) { 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; return;
} }
setError(null); setContextError(null);
const transitions = { const transitions = {
[SCREEN_KEYS.SelectAccount]: shouldNotShowPermissionScreen() [SCREEN_KEYS.SelectAccount]: shouldNotShowPermissionScreen()
? SCREEN_KEYS.SelectSourceAndTargetContainers ? SCREEN_KEYS.SelectSourceAndTargetContainers
@@ -146,7 +144,5 @@ export function useCopyJobNavigation() {
handlePrevious, handlePrevious,
handleCancel, handleCancel,
primaryBtnText, primaryBtnText,
error,
setError,
}; };
} }

View File

@@ -11,7 +11,14 @@ interface CopyJobActionMenuProps {
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => { const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null); 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; return null;
} }
@@ -55,7 +62,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
[CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status) [CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status)
) { ) {
const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume); const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume);
if (job.Mode === CopyJobMigrationType.Online) { if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) {
filteredItems.push({ filteredItems.push({
key: CopyJobActions.complete, key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete, text: ContainerCopyMessages.MonitorJobs.Actions.complete,
@@ -67,7 +74,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
return filteredItems; 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); return baseItems.filter((item) => item.key === CopyJobActions.resume);
} }

View File

@@ -73,6 +73,8 @@ export interface CopyJobFlowType {
} }
export interface CopyJobContextProviderType { export interface CopyJobContextProviderType {
contextError: string | null;
setContextError: React.Dispatch<React.SetStateAction<string | null>>;
flow: CopyJobFlowType; flow: CopyJobFlowType;
setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>; setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>;
copyJobState: CopyJobContextState | null; copyJobState: CopyJobContextState | null;