mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
copy job process performance enhancement (#2273)
This commit is contained in:
@@ -44,8 +44,8 @@ export const getDatabaseEndpoint = (apiType: ApiType): string => {
|
|||||||
return "gremlinDatabases";
|
return "gremlinDatabases";
|
||||||
case "Tables":
|
case "Tables":
|
||||||
return "tables";
|
return "tables";
|
||||||
default:
|
|
||||||
case "SQL":
|
case "SQL":
|
||||||
|
default:
|
||||||
return "sqlDatabases";
|
return "sqlDatabases";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -58,8 +58,8 @@ export const getCollectionEndpoint = (apiType: ApiType): string => {
|
|||||||
return "tables";
|
return "tables";
|
||||||
case "Gremlin":
|
case "Gremlin":
|
||||||
return "graphs";
|
return "graphs";
|
||||||
default:
|
|
||||||
case "SQL":
|
case "SQL":
|
||||||
|
default:
|
||||||
return "containers";
|
return "containers";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ export interface DatabaseAccountSystemData {
|
|||||||
|
|
||||||
export interface DatabaseAccountBackupPolicy {
|
export interface DatabaseAccountBackupPolicy {
|
||||||
type: string;
|
type: string;
|
||||||
/* periodicModeProperties?: {
|
|
||||||
backupIntervalInMinutes: number;
|
|
||||||
backupRetentionIntervalInHours: number;
|
|
||||||
backupStorageRedundancy: string;
|
|
||||||
};
|
|
||||||
continuousModeProperties?: {
|
|
||||||
tier: string;
|
|
||||||
}; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountExtendedProperties {
|
export interface DatabaseAccountExtendedProperties {
|
||||||
@@ -73,6 +65,7 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
enablePriorityBasedExecution?: boolean;
|
enablePriorityBasedExecution?: boolean;
|
||||||
vcoreMongoEndpoint?: string;
|
vcoreMongoEndpoint?: string;
|
||||||
|
enableAllVersionsAndDeletesChangeFeed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
extractErrorMessage,
|
extractErrorMessage,
|
||||||
formatUTCDateTime,
|
formatUTCDateTime,
|
||||||
getAccountDetailsFromResourceId,
|
getAccountDetailsFromResourceId,
|
||||||
|
isIntraAccountCopy,
|
||||||
} from "../CopyJobUtils";
|
} from "../CopyJobUtils";
|
||||||
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
||||||
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
|
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
|
||||||
@@ -75,7 +76,6 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
}
|
}
|
||||||
copyJobsAbortController = null;
|
copyJobsAbortController = null;
|
||||||
|
|
||||||
/* added a lower bound to "0" and upper bound to "100" */
|
|
||||||
const calculateCompletionPercentage = (processed: number, total: number): number => {
|
const calculateCompletionPercentage = (processed: number, total: number): number => {
|
||||||
if (
|
if (
|
||||||
typeof processed !== "number" ||
|
typeof processed !== "number" ||
|
||||||
@@ -139,11 +139,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
|
|||||||
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
||||||
userContext.databaseAccount?.id || "",
|
userContext.databaseAccount?.id || "",
|
||||||
);
|
);
|
||||||
|
const isSameAccount = isIntraAccountCopy(source?.account?.id, target?.account?.id);
|
||||||
const body = {
|
const body = {
|
||||||
properties: {
|
properties: {
|
||||||
source: {
|
source: {
|
||||||
component: "CosmosDBSql",
|
component: "CosmosDBSql",
|
||||||
remoteAccountName: source?.account?.name,
|
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
|
||||||
databaseName: source?.databaseId,
|
databaseName: source?.databaseId,
|
||||||
containerName: source?.containerId,
|
containerName: source?.containerId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,13 +55,15 @@ export default {
|
|||||||
"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) =>
|
intraAccountOnlineDescription: (accountName: string) =>
|
||||||
`Follow the steps below to enable online copy on your "${accountName}" account.`,
|
`Follow the steps below to enable online copy on your "${accountName}" account.`,
|
||||||
commonConfiguration: {
|
crossAccountConfiguration: {
|
||||||
title: "Common configuration",
|
title: "Cross-account container copy",
|
||||||
description: "Basic permissions required for copy operations",
|
description: (sourceAccount: string, destinationAccount: string) =>
|
||||||
|
`Please follow the instruction below to grant requisite permissions to copy data from "${sourceAccount}" to "${destinationAccount}".`,
|
||||||
},
|
},
|
||||||
onlineConfiguration: {
|
onlineConfiguration: {
|
||||||
title: "Online copy configuration",
|
title: "Online container copy",
|
||||||
description: "Additional permissions required for online copy operations",
|
description: (accountName: string) =>
|
||||||
|
`Please follow the instructions below to enable online copy on your "${accountName}" account.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
toggleBtn: {
|
toggleBtn: {
|
||||||
@@ -129,10 +131,17 @@ export default {
|
|||||||
},
|
},
|
||||||
onlineCopyEnabled: {
|
onlineCopyEnabled: {
|
||||||
title: "Online copy enabled",
|
title: "Online copy enabled",
|
||||||
description: (accountName: string) => `Enable Online copy on "${accountName}".`,
|
description: (accountName: string) =>
|
||||||
|
`Enable online container copy by clicking the button below on your "${accountName}" account.`,
|
||||||
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",
|
||||||
|
validateAllVersionsAndDeletesChangeFeedSpinnerLabel:
|
||||||
|
"Validating All versions and deletes change feed mode (preview)...",
|
||||||
|
enablingAllVersionsAndDeletesChangeFeedSpinnerLabel:
|
||||||
|
"Enabling All versions and deletes change feed mode (preview)...",
|
||||||
|
enablingOnlineCopySpinnerLabel: (accountName: string) =>
|
||||||
|
`Enabling online copy on your "${accountName}" account ...`,
|
||||||
},
|
},
|
||||||
MonitorJobs: {
|
MonitorJobs: {
|
||||||
Columns: {
|
Columns: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||||
import React, { useCallback } from "react";
|
import React from "react";
|
||||||
import { logError } from "../../../../../Common/Logger";
|
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";
|
||||||
@@ -25,7 +25,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
|||||||
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
|
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
|
||||||
const [readPermissionAssigned, onToggle] = useToggle(false);
|
const [readPermissionAssigned, onToggle] = useToggle(false);
|
||||||
|
|
||||||
const handleAddReadPermission = useCallback(async () => {
|
const handleAddReadPermission = async () => {
|
||||||
const { source, target } = copyJobState;
|
const { source, target } = copyJobState;
|
||||||
const selectedSourceAccount = source?.account;
|
const selectedSourceAccount = source?.account;
|
||||||
try {
|
try {
|
||||||
@@ -56,7 +56,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [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" }}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const validatorFn: AccountValidatorFn = (prev: DatabaseAccount, next: DatabaseAc
|
|||||||
|
|
||||||
const OnlineCopyEnabled: React.FC = () => {
|
const OnlineCopyEnabled: React.FC = () => {
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [loaderMessage, setLoaderMessage] = React.useState("");
|
||||||
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);
|
||||||
@@ -75,6 +76,21 @@ const OnlineCopyEnabled: React.FC = () => {
|
|||||||
setShowRefreshButton(false);
|
setShowRefreshButton(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel);
|
||||||
|
const sourAccountBeforeUpdate = await fetchDatabaseAccount(
|
||||||
|
sourceSubscriptionId,
|
||||||
|
sourceResourceGroup,
|
||||||
|
sourceAccountName,
|
||||||
|
);
|
||||||
|
if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) {
|
||||||
|
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel);
|
||||||
|
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||||
|
properties: {
|
||||||
|
enableAllVersionsAndDeletesChangeFeed: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName));
|
||||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||||
properties: {
|
properties: {
|
||||||
enableAllVersionsAndDeletesChangeFeed: true,
|
enableAllVersionsAndDeletesChangeFeed: true,
|
||||||
@@ -120,7 +136,7 @@ const OnlineCopyEnabled: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
<LoadingOverlay isLoading={loading} label={loaderMessage} />
|
||||||
<Stack.Item className="info-message">
|
<Stack.Item className="info-message">
|
||||||
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")} 
|
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")} 
|
||||||
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer">
|
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer">
|
||||||
|
|||||||
@@ -44,10 +44,9 @@ const useManagedIdentity = (
|
|||||||
const errorMessage = error.message || "Error enabling system-assigned managed identity. Please try again later.";
|
const errorMessage = error.message || "Error enabling system-assigned managed identity. Please try again later.";
|
||||||
logError(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity");
|
logError(errorMessage, "CopyJob/useManagedIdentity.handleAddSystemIdentity");
|
||||||
setContextError(errorMessage);
|
setContextError(errorMessage);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [copyJobState, updateIdentityFn, setCopyJobState]);
|
}, [copyJobState?.target?.account?.id, updateIdentityFn, setCopyJobState]);
|
||||||
|
|
||||||
return { loading, handleAddSystemIdentity };
|
return { loading, handleAddSystemIdentity };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,15 +186,20 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
|
|||||||
|
|
||||||
const groupsToValidate = useMemo(() => {
|
const groupsToValidate = useMemo(() => {
|
||||||
const isSameAccount = isIntraAccountCopy(sourceAccount.accountId, targetAccount.accountId);
|
const isSameAccount = isIntraAccountCopy(sourceAccount.accountId, targetAccount.accountId);
|
||||||
const commonSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
|
const crossAccountSections = isSameAccount ? [] : [...PERMISSION_SECTIONS_CONFIG];
|
||||||
const groups: PermissionGroupConfig[] = [];
|
const groups: PermissionGroupConfig[] = [];
|
||||||
|
const sourceAccountName = state.source?.account?.name || "";
|
||||||
|
const targetAccountName = state.target?.account?.name || "";
|
||||||
|
|
||||||
if (commonSections.length > 0) {
|
if (crossAccountSections.length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
id: "commonConfigs",
|
id: "crossAccountConfigs",
|
||||||
title: ContainerCopyMessages.assignPermissions.commonConfiguration.title,
|
title: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.title,
|
||||||
description: ContainerCopyMessages.assignPermissions.commonConfiguration.description,
|
description: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.description(
|
||||||
sections: commonSections,
|
sourceAccountName,
|
||||||
|
targetAccountName,
|
||||||
|
),
|
||||||
|
sections: crossAccountSections,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +207,7 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
|
|||||||
groups.push({
|
groups.push({
|
||||||
id: "onlineConfigs",
|
id: "onlineConfigs",
|
||||||
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title,
|
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title,
|
||||||
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description,
|
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description(sourceAccountName),
|
||||||
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
|
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,25 +11,19 @@ export function useDropdownOptions(
|
|||||||
subscriptionOptions: DropdownOptionType[];
|
subscriptionOptions: DropdownOptionType[];
|
||||||
accountOptions: DropdownOptionType[];
|
accountOptions: DropdownOptionType[];
|
||||||
} {
|
} {
|
||||||
const subscriptionOptions = React.useMemo(
|
const subscriptionOptions =
|
||||||
() =>
|
|
||||||
subscriptions?.map((sub) => ({
|
subscriptions?.map((sub) => ({
|
||||||
key: sub.subscriptionId,
|
key: sub.subscriptionId,
|
||||||
text: sub.displayName,
|
text: sub.displayName,
|
||||||
data: sub,
|
data: sub,
|
||||||
})) || [],
|
})) || [];
|
||||||
[subscriptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const accountOptions = React.useMemo(
|
const accountOptions =
|
||||||
() =>
|
|
||||||
accounts?.map((account) => ({
|
accounts?.map((account) => ({
|
||||||
key: account.id,
|
key: account.id,
|
||||||
text: account.name,
|
text: account.name,
|
||||||
data: account,
|
data: account,
|
||||||
})) || [],
|
})) || [];
|
||||||
[accounts],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { subscriptionOptions, accountOptions };
|
return { subscriptionOptions, accountOptions };
|
||||||
}
|
}
|
||||||
@@ -38,8 +32,10 @@ type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
|
|||||||
|
|
||||||
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
||||||
const { setValidationCache } = useCopyJobPrerequisitesCache();
|
const { setValidationCache } = useCopyJobPrerequisitesCache();
|
||||||
const handleSelectSourceAccount = React.useCallback(
|
const handleSelectSourceAccount = (
|
||||||
(type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => {
|
type: "subscription" | "account",
|
||||||
|
data: (Subscription & DatabaseAccount) | undefined,
|
||||||
|
) => {
|
||||||
setCopyJobState((prevState: CopyJobContextState) => {
|
setCopyJobState((prevState: CopyJobContextState) => {
|
||||||
if (type === "subscription") {
|
if (type === "subscription") {
|
||||||
return {
|
return {
|
||||||
@@ -63,20 +59,15 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
|
|||||||
return prevState;
|
return prevState;
|
||||||
});
|
});
|
||||||
setValidationCache(new Map<string, boolean>());
|
setValidationCache(new Map<string, boolean>());
|
||||||
},
|
};
|
||||||
[setCopyJobState, setValidationCache],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMigrationTypeChange = React.useCallback(
|
const handleMigrationTypeChange = React.useCallback((_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
|
||||||
(_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
|
|
||||||
setCopyJobState((prevState: CopyJobContextState) => ({
|
setCopyJobState((prevState: CopyJobContextState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
|
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
|
||||||
}));
|
}));
|
||||||
setValidationCache(new Map<string, boolean>());
|
setValidationCache(new Map<string, boolean>());
|
||||||
},
|
}, []);
|
||||||
[setCopyJobState, setValidationCache],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { handleSelectSourceAccount, handleMigrationTypeChange };
|
return { handleSelectSourceAccount, handleMigrationTypeChange };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
|||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
|
import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
|
||||||
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
|
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
|
||||||
import { useMemoizedSourceAndTargetData } from "./memoizedData";
|
import { useSourceAndTargetData } from "./memoizedData";
|
||||||
|
|
||||||
type SelectSourceAndTargetContainers = {
|
type SelectSourceAndTargetContainers = {
|
||||||
showAddCollectionPanel?: () => void;
|
showAddCollectionPanel?: () => void;
|
||||||
@@ -16,31 +16,35 @@ type SelectSourceAndTargetContainers = {
|
|||||||
const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourceAndTargetContainers) => {
|
const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourceAndTargetContainers) => {
|
||||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||||
const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
|
const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
|
||||||
useMemoizedSourceAndTargetData(copyJobState);
|
useSourceAndTargetData(copyJobState);
|
||||||
|
|
||||||
const sourceDatabases = useDatabases(...sourceDbParams) || [];
|
if (!source) {
|
||||||
const sourceContainers = useDataContainers(...sourceContainerParams) || [];
|
return null;
|
||||||
const targetDatabases = useDatabases(...targetDbParams) || [];
|
}
|
||||||
const targetContainers = useDataContainers(...targetContainerParams) || [];
|
|
||||||
|
const sourceDatabases = useDatabases(...sourceDbParams);
|
||||||
|
const sourceContainers = useDataContainers(...sourceContainerParams);
|
||||||
|
const targetDatabases = useDatabases(...targetDbParams);
|
||||||
|
const targetContainers = useDataContainers(...targetContainerParams);
|
||||||
|
|
||||||
const sourceDatabaseOptions = React.useMemo(
|
const sourceDatabaseOptions = React.useMemo(
|
||||||
() => sourceDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
|
() => sourceDatabases?.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })) || [],
|
||||||
[sourceDatabases],
|
[sourceDatabases],
|
||||||
);
|
);
|
||||||
const sourceContainerOptions = React.useMemo(
|
const sourceContainerOptions = React.useMemo(
|
||||||
() => sourceContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
|
() => sourceContainers?.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })) || [],
|
||||||
[sourceContainers],
|
[sourceContainers],
|
||||||
);
|
);
|
||||||
const targetDatabaseOptions = React.useMemo(
|
const targetDatabaseOptions = React.useMemo(
|
||||||
() => targetDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
|
() => targetDatabases?.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })) || [],
|
||||||
[targetDatabases],
|
[targetDatabases],
|
||||||
);
|
);
|
||||||
const targetContainerOptions = React.useMemo(
|
const targetContainerOptions = React.useMemo(
|
||||||
() => targetContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
|
() => targetContainers?.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })) || [],
|
||||||
[targetContainers],
|
[targetContainers],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
|
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
|
||||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||||
import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types/CopyJobTypes";
|
import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types/CopyJobTypes";
|
||||||
|
|
||||||
export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState) {
|
export function useSourceAndTargetData(copyJobState: CopyJobContextState) {
|
||||||
const { source, target } = copyJobState ?? {};
|
const { source, target } = copyJobState ?? {};
|
||||||
const selectedSourceAccount = source?.account;
|
const selectedSourceAccount = source?.account;
|
||||||
const selectedTargetAccount = target?.account;
|
const selectedTargetAccount = target?.account;
|
||||||
@@ -17,27 +16,22 @@ export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState
|
|||||||
accountName: targetAccountName,
|
accountName: targetAccountName,
|
||||||
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
|
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
|
||||||
|
|
||||||
const sourceDbParams = React.useMemo(
|
const sourceDbParams = [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams;
|
||||||
() => [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams,
|
const sourceContainerParams = [
|
||||||
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName],
|
sourceSubscriptionId,
|
||||||
);
|
sourceResourceGroup,
|
||||||
|
sourceAccountName,
|
||||||
const sourceContainerParams = React.useMemo(
|
source?.databaseId,
|
||||||
() =>
|
"SQL",
|
||||||
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId, "SQL"] as DataContainerParams,
|
] as DataContainerParams;
|
||||||
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId],
|
const targetDbParams = [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams;
|
||||||
);
|
const targetContainerParams = [
|
||||||
|
targetSubscriptionId,
|
||||||
const targetDbParams = React.useMemo(
|
targetResourceGroup,
|
||||||
() => [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams,
|
targetAccountName,
|
||||||
[targetSubscriptionId, targetResourceGroup, targetAccountName],
|
target?.databaseId,
|
||||||
);
|
"SQL",
|
||||||
|
] as DataContainerParams;
|
||||||
const targetContainerParams = React.useMemo(
|
|
||||||
() =>
|
|
||||||
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId, "SQL"] as DataContainerParams,
|
|
||||||
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams };
|
return { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
@@ -34,7 +35,11 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
|||||||
[CopyJobStatusType.Completed]: "CompletedSolid",
|
[CopyJobStatusType.Completed]: "CompletedSolid",
|
||||||
};
|
};
|
||||||
|
|
||||||
const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status }) => {
|
export interface CopyJobStatusWithIconProps {
|
||||||
|
status: CopyJobStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => {
|
||||||
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown";
|
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown";
|
||||||
|
|
||||||
const isSpinnerStatus = [
|
const isSpinnerStatus = [
|
||||||
@@ -57,6 +62,11 @@ const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status
|
|||||||
<Text>{statusText}</Text>
|
<Text>{statusText}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CopyJobStatusWithIcon.displayName = "CopyJobStatusWithIcon";
|
||||||
|
CopyJobStatusWithIcon.propTypes = {
|
||||||
|
status: PropTypes.oneOf(Object.values(CopyJobStatusType)).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CopyJobStatusWithIcon;
|
export default CopyJobStatusWithIcon;
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CopyJobsNotFound;
|
export default React.memo(CopyJobsNotFound);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable react/prop-types */
|
||||||
import {
|
import {
|
||||||
ConstrainMode,
|
ConstrainMode,
|
||||||
DetailsListLayoutMode,
|
DetailsListLayoutMode,
|
||||||
DetailsRow,
|
DetailsRow,
|
||||||
IColumn,
|
IColumn,
|
||||||
|
IDetailsRowProps,
|
||||||
ScrollablePane,
|
ScrollablePane,
|
||||||
ScrollbarVisibility,
|
ScrollbarVisibility,
|
||||||
ShimmeredDetailsList,
|
ShimmeredDetailsList,
|
||||||
@@ -58,22 +60,19 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
setStartIndex(0);
|
setStartIndex(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: IColumn[] = React.useMemo(
|
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
|
||||||
() => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending),
|
|
||||||
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending],
|
|
||||||
);
|
|
||||||
|
|
||||||
const _handleRowClick = React.useCallback((job: CopyJobType) => {
|
const _handleRowClick = (job: CopyJobType) => {
|
||||||
openCopyJobDetailsPanel(job);
|
openCopyJobDetailsPanel(job);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const _onRenderRow = React.useCallback((props: any) => {
|
const _onRenderRow = (props: IDetailsRowProps) => {
|
||||||
return (
|
return (
|
||||||
<div onClick={_handleRowClick.bind(null, props.item)}>
|
<div onClick={_handleRowClick.bind(null, props.item)}>
|
||||||
<DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
|
<DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import ShimmerTree, { IndentLevel } from "Common/ShimmerTree/ShimmerTree";
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle } from "react";
|
import React, { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||||
import { getCopyJobs, updateCopyJobStatus } from "../Actions/CopyJobActions";
|
import { getCopyJobs, updateCopyJobStatus } from "../Actions/CopyJobActions";
|
||||||
import { convertToCamelCase } from "../CopyJobUtils";
|
import { convertToCamelCase, isEqual } from "../CopyJobUtils";
|
||||||
import { CopyJobStatusType } from "../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../Enums/CopyJobEnums";
|
||||||
import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
||||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||||
import CopyJobsList from "./Components/CopyJobsList";
|
import CopyJobsList from "./Components/CopyJobsList";
|
||||||
|
|
||||||
const FETCH_INTERVAL_MS = 30 * 1000;
|
const FETCH_INTERVAL_MS = 30 * 1000;
|
||||||
|
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||||
|
|
||||||
interface MonitorCopyJobsProps {
|
interface MonitorCopyJobsProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -27,8 +28,6 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
const isUpdatingRef = React.useRef(false);
|
const isUpdatingRef = React.useRef(false);
|
||||||
const isFirstFetchRef = React.useRef(true);
|
const isFirstFetchRef = React.useRef(true);
|
||||||
|
|
||||||
const indentLevels = React.useMemo<IndentLevel[]>(() => Array(7).fill({ level: 0, width: "100%" }), []);
|
|
||||||
|
|
||||||
const fetchJobs = React.useCallback(async () => {
|
const fetchJobs = React.useCallback(async () => {
|
||||||
if (isUpdatingRef.current) {
|
if (isUpdatingRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -41,8 +40,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
|
|
||||||
const response = await getCopyJobs();
|
const response = await getCopyJobs();
|
||||||
setJobs((prevJobs) => {
|
setJobs((prevJobs) => {
|
||||||
const isSame = JSON.stringify(prevJobs) === JSON.stringify(response);
|
return isEqual(prevJobs, response) ? prevJobs : response;
|
||||||
return isSame ? prevJobs : response;
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error.message || "Failed to load copy jobs. Please try again later.");
|
setError(error.message || "Failed to load copy jobs. Please try again later.");
|
||||||
@@ -111,7 +109,9 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="monitorCopyJobs flexContainer">
|
<Stack className="monitorCopyJobs flexContainer">
|
||||||
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: "100%", padding: "1rem 2.5rem" }} />}
|
{loading && (
|
||||||
|
<ShimmerTree indentLevels={SHIMMER_INDENT_LEVELS} style={{ width: "100%", padding: "1rem 2.5rem" }} />
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
|
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DatabaseAccount } from "Contracts/DataModels";
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { buildArmUrl } from "Utils/arm/armUtils";
|
import { buildArmUrl } from "Utils/arm/armUtils";
|
||||||
|
|
||||||
const apiVersion = "2025-04-15";
|
const apiVersion = "2025-05-01-preview";
|
||||||
export type FetchAccountDetailsParams = {
|
export type FetchAccountDetailsParams = {
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
resourceGroupName: string;
|
resourceGroupName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user