mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 08:51:24 +00:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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%" }} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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,18 +69,42 @@ const OnlineCopyEnabled: React.FC = () => {
|
|||||||
handleFetchAccount();
|
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(() => {
|
React.useEffect(() => {
|
||||||
intervalRef.current = setInterval(() => {
|
|
||||||
handleFetchAccount();
|
|
||||||
}, 30 * 1000);
|
|
||||||
|
|
||||||
timeoutRef.current = setTimeout(
|
|
||||||
() => {
|
|
||||||
clearIntervalAndShowRefresh();
|
|
||||||
},
|
|
||||||
15 * 60 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user