Fix lint & typescript checks

This commit is contained in:
Bikram Choudhury
2025-10-28 18:05:05 +05:30
parent 5ba7ce2f10
commit 58e187aeb2
62 changed files with 2376 additions and 2413 deletions
+1 -1
View File
@@ -62,4 +62,4 @@ export const getCollectionEndpoint = (apiType: ApiType): string => {
case "SQL": case "SQL":
return "containers"; return "containers";
} }
}; };
+28 -30
View File
@@ -2,41 +2,39 @@ import { Shimmer, ShimmerElementType, Stack } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
export interface IndentLevel { export interface IndentLevel {
level: number, level: number;
width?: string width?: string;
} }
interface ShimmerTreeProps { interface ShimmerTreeProps {
indentLevels: IndentLevel[]; indentLevels: IndentLevel[];
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => { const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => {
/** /**
* indentLevels - Array of indent levels for shimmer tree * indentLevels - Array of indent levels for shimmer tree
* 0 - Root * 0 - Root
* 1 - Level 1 * 1 - Level 1
* 2 - Level 2 * 2 - Level 2
* 3 - Level 3 * 3 - Level 3
* n - Level n * n - Level n
* */ * */
const renderShimmers = (indent: IndentLevel) => ( const renderShimmers = (indent: IndentLevel) => (
<Shimmer <Shimmer
key={Math.random()} key={Math.random()}
shimmerElements={[ shimmerElements={[
{ type: ShimmerElementType.gap, width: `${indent.level * 20}px` }, // Indent for hierarchy { type: ShimmerElementType.gap, width: `${indent.level * 20}px` }, // Indent for hierarchy
{ type: ShimmerElementType.line, height: 16, width: indent.width || "100%" }, { type: ShimmerElementType.line, height: 16, width: indent.width || "100%" },
]} ]}
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
/> />
); );
return ( return (
<Stack tokens={{ childrenGap: 8 }} style={{ width: "50%", ...style }} data-testid="shimmer-stack"> <Stack tokens={{ childrenGap: 8 }} style={{ width: "50%", ...style }} data-testid="shimmer-stack">
{ {indentLevels.map((indentLevel: IndentLevel) => renderShimmers(indentLevel))}
indentLevels.map((indentLevel: IndentLevel) => renderShimmers(indentLevel)) </Stack>
} );
</Stack>
);
}; };
export default ShimmerTree; export default ShimmerTree;
+2 -2
View File
@@ -14,7 +14,7 @@ export interface DatabaseAccountUserAssignedIdentity {
[key: string]: { [key: string]: {
principalId: string; principalId: string;
clientId: string; clientId: string;
} };
} }
export interface DatabaseAccountIdentity { export interface DatabaseAccountIdentity {
@@ -226,7 +226,7 @@ export interface Database extends Resource {
collections?: Collection[]; collections?: Collection[];
} }
export interface DocumentId extends Resource { } export interface DocumentId extends Resource {}
export interface ConflictId extends Resource { export interface ConflictId extends Resource {
resourceId?: string; resourceId?: string;
@@ -2,22 +2,25 @@ import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { import {
cancel, cancel,
complete, complete,
create, create,
listByDatabaseAccount, listByDatabaseAccount,
pause, pause,
resume resume,
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; } from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
import { CreateJobRequest, DataTransferJobGetResults } from "../../../Utils/arm/generatedClients/dataTransferService/types"; import {
CreateJobRequest,
DataTransferJobGetResults,
} from "../../../Utils/arm/generatedClients/dataTransferService/types";
import ContainerCopyMessages from "../ContainerCopyMessages"; import ContainerCopyMessages from "../ContainerCopyMessages";
import { import {
convertTime, convertTime,
convertToCamelCase, convertToCamelCase,
COSMOS_SQL_COMPONENT, COSMOS_SQL_COMPONENT,
extractErrorMessage, extractErrorMessage,
formatUTCDateTime, formatUTCDateTime,
getAccountDetailsFromResourceId getAccountDetailsFromResourceId,
} from "../CopyJobUtils"; } from "../CopyJobUtils";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider"; import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobActions, CopyJobStatusType } from "../Enums"; import { CopyJobActions, CopyJobStatusType } from "../Enums";
@@ -25,154 +28,159 @@ import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefSta
import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from "../Types"; import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from "../Types";
export const openCreateCopyJobPanel = () => { export const openCreateCopyJobPanel = () => {
const sidePanelState = useSidePanel.getState() const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false); sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel( sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle, ContainerCopyMessages.createCopyJobPanelTitle,
<CreateCopyJobScreensProvider />, <CreateCopyJobScreensProvider />,
"650px" "650px",
); );
} };
let copyJobsAbortController: AbortController | null = null; let copyJobsAbortController: AbortController | null = null;
export const getCopyJobs = async (): Promise<CopyJobType[]> => { export const getCopyJobs = async (): Promise<CopyJobType[]> => {
// Abort previous request if still in-flight // Abort previous request if still in-flight
if (copyJobsAbortController) { if (copyJobsAbortController) {
copyJobsAbortController.abort(); copyJobsAbortController.abort();
}
copyJobsAbortController = new AbortController();
try {
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
copyJobsAbortController.signal,
);
const jobs = response.value || [];
if (!Array.isArray(jobs)) {
throw new Error("Invalid migration job status response: Expected an array of jobs.");
} }
copyJobsAbortController = new AbortController(); copyJobsAbortController = null;
try {
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || "");
const response = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
copyJobsAbortController.signal
);
const jobs = response.value || []; /* added a lower bound to "0" and upper bound to "100" */
if (!Array.isArray(jobs)) { const calculateCompletionPercentage = (processed: number, total: number): number => {
throw new Error("Invalid migration job status response: Expected an array of jobs."); if (
} typeof processed !== "number" ||
copyJobsAbortController = null; typeof total !== "number" ||
!isFinite(processed) ||
!isFinite(total) ||
total <= 0
) {
return 0;
}
/* added a lower bound to "0" and upper bound to "100" */ const percentage = Math.round((processed / total) * 100);
const calculateCompletionPercentage = (processed: number, total: number): number => { return Math.max(0, Math.min(100, percentage));
if ( };
typeof processed !== 'number' ||
typeof total !== 'number' ||
!isFinite(processed) ||
!isFinite(total) ||
total <= 0
) {
return 0;
}
const percentage = Math.round((processed / total) * 100); const formattedJobs: CopyJobType[] = jobs
return Math.max(0, Math.min(100, percentage)); .filter(
}; (job: DataTransferJobGetResults) =>
job.properties?.source?.component === COSMOS_SQL_COMPONENT &&
job.properties?.destination?.component === COSMOS_SQL_COMPONENT,
)
.sort(
(current: DataTransferJobGetResults, next: DataTransferJobGetResults) =>
new Date(next.properties.lastUpdatedUtcTime).getTime() -
new Date(current.properties.lastUpdatedUtcTime).getTime(),
)
.map((job: DataTransferJobGetResults, index: number) => {
const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime);
const formattedJobs: CopyJobType[] = jobs return {
.filter((job: DataTransferJobGetResults) => ID: (index + 1).toString(),
job.properties?.source?.component === COSMOS_SQL_COMPONENT && Mode: job.properties.mode,
job.properties?.destination?.component === COSMOS_SQL_COMPONENT Name: job.properties.jobName,
) Status: convertToCamelCase(job.properties.status) as CopyJobType["Status"],
.sort((current: DataTransferJobGetResults, next: DataTransferJobGetResults) => CompletionPercentage: calculateCompletionPercentage(job.properties.processedCount, job.properties.totalCount),
new Date(next.properties.lastUpdatedUtcTime).getTime() - new Date(current.properties.lastUpdatedUtcTime).getTime() Duration: convertTime(job.properties.duration),
) LastUpdatedTime: dateTimeObj.formattedDateTime,
.map((job: DataTransferJobGetResults, index: number) => { timestamp: dateTimeObj.timestamp,
const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime); Error: job.properties.error ? extractErrorMessage(job.properties.error as unknown as CopyJobErrorType) : null,
} as CopyJobType;
return { });
ID: (index + 1).toString(), return formattedJobs;
Mode: job.properties.mode, } catch (error) {
Name: job.properties.jobName, const errorContent = JSON.stringify(error.content || error);
Status: convertToCamelCase(job.properties.status) as CopyJobType["Status"], console.error(`Error fetching copy jobs: ${errorContent}`);
CompletionPercentage: calculateCompletionPercentage(job.properties.processedCount, job.properties.totalCount), throw error;
Duration: convertTime(job.properties.duration), }
LastUpdatedTime: dateTimeObj.formattedDateTime, };
timestamp: dateTimeObj.timestamp,
Error: job.properties.error ? extractErrorMessage(job.properties.error as unknown as CopyJobErrorType) : null,
} as CopyJobType;
});
return formattedJobs;
} catch (error) {
const errorContent = JSON.stringify(error.content || error);
console.error(`Error fetching copy jobs: ${errorContent}`);
throw error;
}
}
export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: () => void) => { export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: () => void) => {
try { try {
const { source, target, migrationType, jobName } = state; const { source, target, migrationType, jobName } = state;
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || ""); const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
const body = { userContext.databaseAccount?.id || "",
properties: { );
source: { const body = {
component: "CosmosDBSql", properties: {
remoteAccountName: source?.account?.name, source: {
databaseName: source?.databaseId, component: "CosmosDBSql",
containerName: source?.containerId remoteAccountName: source?.account?.name,
}, databaseName: source?.databaseId,
destination: { containerName: source?.containerId,
component: "CosmosDBSql", },
databaseName: target?.databaseId, destination: {
containerName: target?.containerId component: "CosmosDBSql",
}, databaseName: target?.databaseId,
mode: migrationType containerName: target?.containerId,
} },
} as unknown as CreateJobRequest; mode: migrationType,
},
} as unknown as CreateJobRequest;
const response = await create( const response = await create(subscriptionId, resourceGroup, accountName, jobName, body);
subscriptionId, MonitorCopyJobsRefState.getState().ref?.refreshJobList();
resourceGroup, onSuccess();
accountName, return response;
jobName, } catch (error) {
body, console.error("Error submitting create copy job:", error);
); throw error;
MonitorCopyJobsRefState.getState().ref?.refreshJobList(); }
onSuccess(); };
return response;
} catch (error) {
console.error("Error submitting create copy job:", error);
throw error;
}
}
export const updateCopyJobStatus = async (job: CopyJobType, action: string): Promise<DataTransferJobGetResults> => { export const updateCopyJobStatus = async (job: CopyJobType, action: string): Promise<DataTransferJobGetResults> => {
try { try {
let updateFn = null;
let updateFn = null; switch (action.toLowerCase()) {
switch (action.toLowerCase()) { case CopyJobActions.pause:
case CopyJobActions.pause: updateFn = pause;
updateFn = pause; break;
break; case CopyJobActions.resume:
case CopyJobActions.resume: updateFn = resume;
updateFn = resume; break;
break; case CopyJobActions.cancel:
case CopyJobActions.cancel: updateFn = cancel;
updateFn = cancel; break;
break; case CopyJobActions.complete:
case CopyJobActions.complete: updateFn = complete;
updateFn = complete; break;
break; default:
default: throw new Error(`Unsupported action: ${action}`);
throw new Error(`Unsupported action: ${action}`);
}
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || "");
const response = await updateFn?.(subscriptionId, resourceGroup, accountName, job.Name);
return response;
} catch (error) {
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
const pattern = new RegExp(`'(${statusList.join('|')})'`, 'g');
const normalizedErrorMessage = errorMessage.replace(pattern, `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`);
console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
throw error;
} }
} const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await updateFn?.(subscriptionId, resourceGroup, accountName, job.Name);
return response;
} catch (error) {
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
const normalizedErrorMessage = errorMessage.replace(
pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
);
console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
throw error;
}
};
@@ -8,24 +8,24 @@ import { getCommandBarButtons } from "./Utils";
const backgroundColor = StyleConstants.BaseLight; const backgroundColor = StyleConstants.BaseLight;
const rootStyle = { const rootStyle = {
root: { root: {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
}, },
}; };
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ container }) => { const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ container }) => {
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(container); const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(container);
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor); const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
return ( return (
<div className="commandBarContainer"> <div className="commandBarContainer">
<FluentCommandBar <FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands" ariaLabel="Use left and right arrow keys to navigate between commands"
styles={rootStyle} styles={rootStyle}
items={controlButtons} items={controlButtons}
/> />
</div> </div>
); );
} };
export default CopyJobCommandBar; export default CopyJobCommandBar;
+39 -39
View File
@@ -10,49 +10,49 @@ import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefSta
import { CopyJobCommandBarBtnType } from "../Types"; import { CopyJobCommandBarBtnType } from "../Types";
function getCopyJobBtns(): CopyJobCommandBarBtnType[] { function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState(state => state.ref); const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const buttons: CopyJobCommandBarBtnType[] = [ const buttons: CopyJobCommandBarBtnType[] = [
{ {
key: "createCopyJob", key: "createCopyJob",
iconSrc: AddIcon, iconSrc: AddIcon,
label: ContainerCopyMessages.createCopyJobButtonLabel, label: ContainerCopyMessages.createCopyJobButtonLabel,
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel, ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel,
onClick: Actions.openCreateCopyJobPanel, onClick: Actions.openCreateCopyJobPanel,
}, },
{ {
key: "refresh", key: "refresh",
iconSrc: RefreshIcon, iconSrc: RefreshIcon,
label: ContainerCopyMessages.refreshButtonLabel, label: ContainerCopyMessages.refreshButtonLabel,
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
onClick: () => monitorCopyJobsRef?.refreshJobList(), onClick: () => monitorCopyJobsRef?.refreshJobList(),
}, },
]; ];
if (configContext.platform === Platform.Portal) { if (configContext.platform === Platform.Portal) {
buttons.push({ buttons.push({
key: "feedback", key: "feedback",
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel, label: ContainerCopyMessages.feedbackButtonLabel,
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel, ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel,
onClick: () => { }, onClick: () => {},
}); });
} }
return buttons; return buttons;
} }
function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProps { function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProps {
return { return {
iconSrc: config.iconSrc, iconSrc: config.iconSrc,
iconAlt: config.label, iconAlt: config.label,
onCommandClick: config.onClick, onCommandClick: config.onClick,
commandButtonLabel: undefined as string | undefined, commandButtonLabel: undefined as string | undefined,
ariaLabel: config.ariaLabel, ariaLabel: config.ariaLabel,
tooltipText: config.label, tooltipText: config.label,
hasPopup: false, hasPopup: false,
disabled: config.disabled ?? false, disabled: config.disabled ?? false,
}; };
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function getCommandBarButtons(_container: Explorer): CommandButtonComponentProps[] { export function getCommandBarButtons(_container: Explorer): CommandButtonComponentProps[] {
return getCopyJobBtns().map(btnMapper); return getCopyJobBtns().map(btnMapper);
} }
@@ -1,117 +1,132 @@
export default { export default {
// Copy Job Command Bar // Copy Job Command Bar
feedbackButtonLabel: "Feedback", feedbackButtonLabel: "Feedback",
feedbackButtonAriaLabel: "Provide feedback on copy jobs", feedbackButtonAriaLabel: "Provide feedback on copy jobs",
refreshButtonLabel: "Refresh", refreshButtonLabel: "Refresh",
refreshButtonAriaLabel: "Refresh copy jobs", refreshButtonAriaLabel: "Refresh copy jobs",
createCopyJobButtonLabel: "Create Copy Job", createCopyJobButtonLabel: "Create Copy Job",
createCopyJobButtonAriaLabel: "Create a new container copy job", createCopyJobButtonAriaLabel: "Create a new container copy job",
// No Copy Jobs Found // No Copy Jobs Found
noCopyJobsTitle: "No copy jobs to show", noCopyJobsTitle: "No copy jobs to show",
createCopyJobButtonText: "Create a container copy job", createCopyJobButtonText: "Create a container copy job",
// Create Copy Job Panel // Create Copy Job Panel
createCopyJobPanelTitle: "Copy container", createCopyJobPanelTitle: "Copy container",
// Select Account Screen // Select Account Screen
selectAccountDescription: "Please select a source account from which to copy.", selectAccountDescription: "Please select a source account from which to copy.",
subscriptionDropdownLabel: "Subscription", subscriptionDropdownLabel: "Subscription",
subscriptionDropdownPlaceholder: "Select a subscription", subscriptionDropdownPlaceholder: "Select a subscription",
sourceAccountDropdownLabel: "Account", sourceAccountDropdownLabel: "Account",
sourceAccountDropdownPlaceholder: "Select an account", sourceAccountDropdownPlaceholder: "Select an account",
migrationTypeCheckboxLabel: "Copy container in offline mode", migrationTypeCheckboxLabel: "Copy container in offline mode",
// Select Source and Target Containers Screen // Select Source and Target Containers Screen
selectSourceAndTargetContainersDescription: "Please select a source container and a destination container to copy to.", selectSourceAndTargetContainersDescription:
sourceContainerSubHeading: "Source container", "Please select a source container and a destination container to copy to.",
targetContainerSubHeading: "Destination container", sourceContainerSubHeading: "Source container",
databaseDropdownLabel: "Database", targetContainerSubHeading: "Destination container",
databaseDropdownPlaceholder: "Select a database", databaseDropdownLabel: "Database",
containerDropdownLabel: "Container", databaseDropdownPlaceholder: "Select a database",
containerDropdownPlaceholder: "Select a container", containerDropdownLabel: "Container",
containerDropdownPlaceholder: "Select a container",
// Preview and Create Screen // Preview and Create Screen
jobNameLabel: "Job name", jobNameLabel: "Job name",
sourceSubscriptionLabel: "Source subscription", sourceSubscriptionLabel: "Source subscription",
sourceAccountLabel: "Source account", sourceAccountLabel: "Source account",
sourceDatabaseLabel: "Source database", sourceDatabaseLabel: "Source database",
sourceContainerLabel: "Source container", sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database", targetDatabaseLabel: "Destination database",
targetContainerLabel: "Destination container", targetContainerLabel: "Destination container",
// Assign Permissions Screen // Assign Permissions Screen
assignPermissions: { assignPermissions: {
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
},
toggleBtn: {
onText: "On",
offText: "Off",
},
addManagedIdentity: {
title: "System assigned managed identity enabled",
description:
"Enable a system assigned managed identity for the destination account to allow the copy job to access it.",
toggleLabel: "System assigned managed identity",
managedIdentityTooltip:
"A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.",
userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity",
enablementTitle: "Enable system assigned managed identity",
enablementDescription: (identityName: string) =>
identityName
? `'${identityName}' will be registered with Microsoft Entra ID. Once it is registered, '${identityName}' can be granted permissions to access resources protected by Microsoft Entra ID. Do you want to enable the system assigned managed identity for '${identityName}'?`
: "",
},
defaultManagedIdentity: {
title: "System assigned managed identity enabled as default",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip:
"A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.",
popoverTitle: "System assigned managed identity set as default",
popoverDescription:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
readPermissionAssigned: {
title: "Read permission assigned to default identity",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip:
"A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.",
popoverTitle: "Read permission assigned to default identity",
popoverDescription:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Point In Time Restore",
},
onlineCopyEnabled: {
title: "Online copy enabled",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Online Copy",
},
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
name: "Job name",
status: "Status",
completionPercentage: "Completion %",
duration: "Duration",
error: "Error message",
mode: "Mode",
actions: "Actions",
}, },
toggleBtn: { Actions: {
onText: "On", pause: "Pause",
offText: "Off" resume: "Resume",
cancel: "Cancel",
complete: "Complete",
viewDetails: "View Details",
}, },
addManagedIdentity: { Status: {
title: "System assigned managed identity enabled", Pending: "Pending",
description: "Enable a system assigned managed identity for the destination account to allow the copy job to access it.", InProgress: "In Progress",
toggleLabel: "System assigned managed identity", Running: "In Progress",
managedIdentityTooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.", Partitioning: "In Progress",
userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.", Paused: "Paused",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.", Completed: "Completed",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity", Failed: "Failed",
enablementTitle: "Enable system assigned managed identity", Faulted: "Failed",
enablementDescription: (identityName: string) => identityName ? `'${identityName}' will be registered with Microsoft Entra ID. Once it is registered, '${identityName}' can be granted permissions to access resources protected by Microsoft Entra ID. Do you want to enable the system assigned managed identity for '${identityName}'?` : "", Skipped: "Cancelled",
Cancelled: "Cancelled",
}, },
defaultManagedIdentity: { },
title: "System assigned managed identity enabled as default", };
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.",
popoverTitle: "System assigned managed identity set as default",
popoverDescription: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
readPermissionAssigned: {
title: "Read permission assigned to default identity",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.",
popoverTitle: "Read permission assigned to default identity",
popoverDescription: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Point In Time Restore",
},
onlineCopyEnabled: {
title: "Online copy enabled",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Online Copy",
},
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
name: "Job name",
status: "Status",
completionPercentage: "Completion %",
duration: "Duration",
error: "Error message",
mode: "Mode",
actions: "Actions",
},
Actions: {
pause: "Pause",
resume: "Resume",
cancel: "Cancel",
complete: "Complete",
viewDetails: "View Details",
},
Status: {
Pending: "Pending",
InProgress: "In Progress",
Running: "In Progress",
Partitioning: "In Progress",
Paused: "Paused",
Completed: "Completed",
Failed: "Failed",
Faulted: "Failed",
Skipped: "Cancelled",
Cancelled: "Cancelled",
}
}
}
@@ -5,50 +5,50 @@ import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null); export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
export const useCopyJobContext = (): CopyJobContextProviderType => { export const useCopyJobContext = (): CopyJobContextProviderType => {
const context = React.useContext(CopyJobContext); const context = React.useContext(CopyJobContext);
if (!context) { if (!context) {
throw new Error("useCopyJobContext must be used within a CopyJobContextProvider"); throw new Error("useCopyJobContext must be used within a CopyJobContextProvider");
} }
return context; return context;
} };
interface CopyJobContextProviderProps { interface CopyJobContextProviderProps {
children: React.ReactNode; children: React.ReactNode;
} }
const getInitialCopyJobState = (): CopyJobContextState => { const getInitialCopyJobState = (): CopyJobContextState => {
return { return {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null, subscription: null,
account: null, account: null,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: userContext.subscriptionId || "", subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null, account: userContext.databaseAccount || null,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false sourceReadAccessFromTarget: false,
} };
} };
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 resetCopyJobState = () => { const resetCopyJobState = () => {
setCopyJobState(getInitialCopyJobState()); setCopyJobState(getInitialCopyJobState());
} };
return ( return (
<CopyJobContext.Provider value={{ copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}> <CopyJobContext.Provider value={{ copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
{props.children} {props.children}
</CopyJobContext.Provider> </CopyJobContext.Provider>
); );
} };
export default CopyJobContextProvider; export default CopyJobContextProvider;
+74 -59
View File
@@ -2,89 +2,104 @@ import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobErrorType } from "./Types"; import { CopyJobErrorType } from "./Types";
export const buildResourceLink = (resource: DatabaseAccount): string => { export const buildResourceLink = (resource: DatabaseAccount): string => {
const resourceId = resource.id; const resourceId = resource.id;
// TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax) // TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax)
return `https://ms.portal.azure.com/#resource${resourceId}`; return `https://ms.portal.azure.com/#resource${resourceId}`;
} };
export const COSMOS_SQL_COMPONENT = "CosmosDBSql"; export const COSMOS_SQL_COMPONENT = "CosmosDBSql";
export const COPY_JOB_API_VERSION = "2025-05-01-preview"; export const COPY_JOB_API_VERSION = "2025-05-01-preview";
export function buildDataTransferJobPath({ export function buildDataTransferJobPath({
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
jobName, jobName,
action, action,
}: { }: {
subscriptionId: string; subscriptionId: string;
resourceGroup: string; resourceGroup: string;
accountName: string; accountName: string;
jobName?: string; jobName?: string;
action?: string; action?: string;
}) { }) {
let path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`; let path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
if (jobName) path += `/${jobName}`; if (jobName) {
if (action) path += `/${action}`; path += `/${jobName}`;
return path; }
if (action) {
path += `/${action}`;
}
return path;
} }
export function convertTime(timeStr: string): string | null { export function convertTime(timeStr: string): string | null {
const timeParts = timeStr.split(":").map(Number); const timeParts = timeStr.split(":").map(Number);
if (timeParts.length !== 3 || timeParts.some(isNaN)) { if (timeParts.length !== 3 || timeParts.some(isNaN)) {
return null; // Return null for invalid format return null; // Return null for invalid format
}
const formatPart = (value: number, unit: string) => {
if (unit === "seconds") {
value = Math.round(value);
} }
const formatPart = (value: number, unit: string) => { return value > 0 ? `${value.toString().padStart(2, "0")} ${unit}` : "";
if (unit === "seconds") { };
value = Math.round(value);
}
return value > 0 ? `${value.toString().padStart(2, "0")} ${unit}` : "";
};
const [hours, minutes, seconds] = timeParts; const [hours, minutes, seconds] = timeParts;
const formattedTimeParts = [formatPart(hours, "hours"), formatPart(minutes, "minutes"), formatPart(seconds, "seconds")] const formattedTimeParts = [
.filter(Boolean) formatPart(hours, "hours"),
.join(", "); formatPart(minutes, "minutes"),
formatPart(seconds, "seconds"),
]
.filter(Boolean)
.join(", ");
return formattedTimeParts || "0 seconds"; // Return "0 seconds" if all parts are zero return formattedTimeParts || "0 seconds"; // Return "0 seconds" if all parts are zero
} }
export function formatUTCDateTime(utcStr: string): { formattedDateTime: string, timestamp: number } | null { export function formatUTCDateTime(utcStr: string): { formattedDateTime: string; timestamp: number } | null {
const date = new Date(utcStr); const date = new Date(utcStr);
if (isNaN(date.getTime())) return null; if (isNaN(date.getTime())) {
return null;
}
return { return {
formattedDateTime: new Intl.DateTimeFormat("en-US", { formattedDateTime: new Intl.DateTimeFormat("en-US", {
dateStyle: "short", dateStyle: "short",
timeStyle: "medium", timeStyle: "medium",
timeZone: "UTC", timeZone: "UTC",
}).format(date), }).format(date),
timestamp: date.getTime(), timestamp: date.getTime(),
}; };
} }
export function convertToCamelCase(str: string): string { export function convertToCamelCase(str: string): string {
const formattedStr = str.split(/\s+/).map( const formattedStr = str
word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() .split(/\s+/)
).join(''); .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
return formattedStr; .join("");
return formattedStr;
} }
export function extractErrorMessage(error: CopyJobErrorType): CopyJobErrorType { export function extractErrorMessage(error: CopyJobErrorType): CopyJobErrorType {
return { return {
...error, ...error,
message: error.message.split("\r\n\r\n")[0] message: error.message.split("\r\n\r\n")[0],
} };
} }
export function getAccountDetailsFromResourceId(accountId: string | undefined) { export function getAccountDetailsFromResourceId(accountId: string | undefined) {
if (!accountId) { if (!accountId) {
return null; return null;
} }
const pattern = new RegExp('/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)', 'i'); const pattern = new RegExp(
const matches = accountId.match(pattern); "/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)",
const [_, subscriptionId, resourceGroup, accountName] = matches || []; "i",
return { subscriptionId, resourceGroup, accountName }; );
} const matches = accountId.match(pattern);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, subscriptionId, resourceGroup, accountName] = matches || [];
return { subscriptionId, resourceGroup, accountName };
}
@@ -18,49 +18,50 @@ const textStyle = { display: "flex", alignItems: "center" };
type AddManagedIdentityProps = Partial<PermissionSectionConfig>; type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => { const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
const { copyJobState } = useCopyJobContext(); const { copyJobState } = useCopyJobContext();
const [systemAssigned, onToggle] = useToggle(false); const [systemAssigned, onToggle] = useToggle(false);
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateSystemIdentity); const { loading, handleAddSystemIdentity } = useManagedIdentity(updateSystemIdentity);
const manageIdentityLink = useMemo(() => { const manageIdentityLink = useMemo(() => {
const { target } = copyJobState; const { target } = copyJobState;
const resourceUri = buildResourceLink(target.account); const resourceUri = buildResourceLink(target.account);
return target?.account?.id ? `${resourceUri}/ManagedIdentitiesBlade` : "#"; return target?.account?.id ? `${resourceUri}/ManagedIdentitiesBlade` : "#";
}, [copyJobState]); }, [copyJobState]);
return ( return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Toggle <Toggle
label={ label={
<Text className="toggle-label" style={textStyle}> <Text className="toggle-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.toggleLabel}&nbsp;<InfoTooltip content={managedIdentityTooltip} /> {ContainerCopyMessages.addManagedIdentity.toggleLabel}&nbsp;
</Text> <InfoTooltip content={managedIdentityTooltip} />
} </Text>
checked={systemAssigned} }
onText={ContainerCopyMessages.toggleBtn.onText} checked={systemAssigned}
offText={ContainerCopyMessages.toggleBtn.offText} onText={ContainerCopyMessages.toggleBtn.onText}
onChange={onToggle} offText={ContainerCopyMessages.toggleBtn.offText}
/> onChange={onToggle}
<Text className="user-assigned-label" style={textStyle}> />
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;<InfoTooltip content={userAssignedTooltip} /> <Text className="user-assigned-label" style={textStyle}>
</Text> {ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;
<div style={{ marginTop: 8 }}> <InfoTooltip content={userAssignedTooltip} />
<Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer"> </Text>
{ContainerCopyMessages.addManagedIdentity.createUserAssignedIdentityLink} <div style={{ marginTop: 8 }}>
</Link> <Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer">
</div> {ContainerCopyMessages.addManagedIdentity.createUserAssignedIdentityLink}
<PopoverMessage </Link>
isLoading={loading} </div>
visible={systemAssigned} <PopoverMessage
title={ContainerCopyMessages.addManagedIdentity.enablementTitle} isLoading={loading}
onCancel={() => onToggle(null, false)} visible={systemAssigned}
onPrimary={handleAddSystemIdentity} title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
onCancel={() => onToggle(null, false)}
> onPrimary={handleAddSystemIdentity}
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)} >
</PopoverMessage> {ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
</Stack> </PopoverMessage>
); </Stack>
);
}; };
export default AddManagedIdentity; export default AddManagedIdentity;
@@ -1,4 +1,4 @@
import { ITooltipHostStyles, Stack, Toggle } from "@fluentui/react"; import { Stack, Toggle } from "@fluentui/react";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { assignRole } from "../../../../../Utils/arm/RbacUtils"; import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../ContainerCopyMessages";
@@ -10,71 +10,71 @@ import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useToggle from "./hooks/useToggle"; import useToggle from "./hooks/useToggle";
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip; const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
type AddManagedIdentityProps = Partial<PermissionSectionConfig>; type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = () => { const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = () => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false); const [readPermissionAssigned, onToggle] = useToggle(false);
const handleAddReadPermission = useCallback(async () => { const handleAddReadPermission = useCallback(async () => {
const { source, target } = copyJobState; const { source, target } = copyJobState;
const selectedSourceAccount = source?.account; const selectedSourceAccount = source?.account;
try { try {
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
accountName: sourceAccountName accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
setLoading(true); setLoading(true);
const assignedRole = await assignRole( const assignedRole = await assignRole(
sourceSubscriptionId, sourceSubscriptionId,
sourceResourceGroup, sourceResourceGroup,
sourceAccountName, sourceAccountName,
target?.account?.identity?.principalId!, target?.account?.identity?.principalId ?? "",
); );
if (assignedRole) { if (assignedRole) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
sourceReadAccessFromTarget: true, sourceReadAccessFromTarget: true,
})); }));
} }
} catch (error) { } catch (error) {
console.error("Error assigning read permission to default identity:", error); console.error("Error assigning read permission to default identity:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [copyJobState]); }, [copyJobState, setCopyJobState]);
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description} &nbsp;<InfoTooltip content={TooltipContent} /> {ContainerCopyMessages.readPermissionAssigned.description} &nbsp;
</div> <InfoTooltip content={TooltipContent} />
<Toggle </div>
checked={readPermissionAssigned} <Toggle
onText={ContainerCopyMessages.toggleBtn.onText} checked={readPermissionAssigned}
offText={ContainerCopyMessages.toggleBtn.offText} onText={ContainerCopyMessages.toggleBtn.onText}
onChange={onToggle} offText={ContainerCopyMessages.toggleBtn.offText}
inlineLabel onChange={onToggle}
styles={{ inlineLabel
root: { marginTop: 8, marginBottom: 12 }, styles={{
label: { display: "none" }, root: { marginTop: 8, marginBottom: 12 },
}} label: { display: "none" },
/> }}
<PopoverMessage />
isLoading={loading} <PopoverMessage
visible={readPermissionAssigned} isLoading={loading}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle} visible={readPermissionAssigned}
onCancel={() => onToggle(null, false)} title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
onPrimary={handleAddReadPermission} onCancel={() => onToggle(null, false)}
> onPrimary={handleAddReadPermission}
{ContainerCopyMessages.readPermissionAssigned.popoverDescription} >
</PopoverMessage> {ContainerCopyMessages.readPermissionAssigned.popoverDescription}
</Stack> </PopoverMessage>
); </Stack>
);
}; };
export default AddReadPermissionToDefaultIdentity; export default AddReadPermissionToDefaultIdentity;
@@ -12,36 +12,37 @@ const managedIdentityTooltip = ContainerCopyMessages.defaultManagedIdentity.tool
type AddManagedIdentityProps = Partial<PermissionSectionConfig>; type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => { const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
const [defaultSystemAssigned, onToggle] = useToggle(false); const [defaultSystemAssigned, onToggle] = useToggle(false);
const { loading, handleAddSystemIdentity } = useManagedIdentity(updateDefaultIdentity); const { loading, handleAddSystemIdentity } = useManagedIdentity(updateDefaultIdentity);
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description} &nbsp;<InfoTooltip content={managedIdentityTooltip} /> {ContainerCopyMessages.defaultManagedIdentity.description} &nbsp;
</div> <InfoTooltip content={managedIdentityTooltip} />
<Toggle </div>
checked={defaultSystemAssigned} <Toggle
onText={ContainerCopyMessages.toggleBtn.onText} checked={defaultSystemAssigned}
offText={ContainerCopyMessages.toggleBtn.offText} onText={ContainerCopyMessages.toggleBtn.onText}
onChange={onToggle} offText={ContainerCopyMessages.toggleBtn.offText}
inlineLabel onChange={onToggle}
styles={{ inlineLabel
root: { marginTop: 8, marginBottom: 12 }, styles={{
label: { display: "none" }, root: { marginTop: 8, marginBottom: 12 },
}} label: { display: "none" },
/> }}
<PopoverMessage />
isLoading={loading} <PopoverMessage
visible={defaultSystemAssigned} isLoading={loading}
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle} visible={defaultSystemAssigned}
onCancel={() => onToggle(null, false)} title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
onPrimary={handleAddSystemIdentity} onCancel={() => onToggle(null, false)}
> onPrimary={handleAddSystemIdentity}
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription} >
</PopoverMessage> {ContainerCopyMessages.defaultManagedIdentity.popoverDescription}
</Stack> </PopoverMessage>
); </Stack>
);
}; };
export default DefaultManagedIdentity; export default DefaultManagedIdentity;
@@ -8,25 +8,21 @@ import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
type AddManagedIdentityProps = Partial<PermissionSectionConfig>; type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => { const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => {
const { copyJobState: { source } = {} } = useCopyJobContext(); const { copyJobState: { source } = {} } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account); const sourceAccountLink = buildResourceLink(source?.account);
const onlineCopyUrl = `${sourceAccountLink}/Features`; const onlineCopyUrl = `${sourceAccountLink}/Features`;
const onWindowClosed = () => { const onWindowClosed = () => {
console.log('Online copy window closed'); // eslint-disable-next-line no-console
}; console.log("Online copy window closed");
const openWindowAndMonitor = useWindowOpenMonitor(onlineCopyUrl, onWindowClosed); };
const openWindowAndMonitor = useWindowOpenMonitor(onlineCopyUrl, onWindowClosed);
return ( return (
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">{ContainerCopyMessages.onlineCopyEnabled.description}</div>
{ContainerCopyMessages.onlineCopyEnabled.description} <PrimaryButton text={ContainerCopyMessages.onlineCopyEnabled.buttonText} onClick={openWindowAndMonitor} />
</div> </Stack>
<PrimaryButton );
text={ContainerCopyMessages.onlineCopyEnabled.buttonText}
onClick={openWindowAndMonitor}
/>
</Stack>
);
}; };
export default OnlineCopyEnabled; export default OnlineCopyEnabled;
@@ -9,53 +9,47 @@ import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
type AddManagedIdentityProps = Partial<PermissionSectionConfig>; type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => { const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext(); const { copyJobState: { source } = {}, setCopyJobState } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account); const sourceAccountLink = buildResourceLink(source?.account);
const pitrUrl = `${sourceAccountLink}/backupRestore`; const pitrUrl = `${sourceAccountLink}/backupRestore`;
const onWindowClosed = useCallback(async () => { const onWindowClosed = useCallback(async () => {
try { try {
const selectedSourceAccount = source?.account; const selectedSourceAccount = source?.account;
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
accountName: sourceAccountName accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
setLoading(true); setLoading(true);
const account = await fetchDatabaseAccount( const account = await fetchDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName);
sourceSubscriptionId, if (account) {
sourceResourceGroup, setCopyJobState((prevState) => ({
sourceAccountName ...prevState,
); source: { ...prevState.source, account: account },
if (account) { }));
setCopyJobState((prevState) => ({ }
...prevState, } catch (error) {
source: { ...prevState.source, account: account } console.error("Error fetching database account after PITR window closed:", error);
})); } finally {
} setLoading(false);
} catch (error) { }
console.error("Error fetching database account after PITR window closed:", error); }, []);
} finally { const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
setLoading(false);
}
}, [])
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
return ( return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">{ContainerCopyMessages.pointInTimeRestore.description}</div>
{ContainerCopyMessages.pointInTimeRestore.description} <PrimaryButton
</div> text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
<PrimaryButton {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText} disabled={loading}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} onClick={openWindowAndMonitor}
disabled={loading} />
onClick={openWindowAndMonitor} </Stack>
/> );
</Stack>
);
}; };
export default PointInTimeRestore; export default PointInTimeRestore;
@@ -4,53 +4,49 @@ import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
interface UseManagedIdentityUpdaterParams { interface UseManagedIdentityUpdaterParams {
updateIdentityFn: ( updateIdentityFn: (
subscriptionId: string, subscriptionId: string,
resourceGroup?: string, resourceGroup?: string,
accountName?: string accountName?: string,
) => Promise<DatabaseAccount | undefined>; ) => Promise<DatabaseAccount | undefined>;
} }
interface UseManagedIdentityUpdaterReturn { interface UseManagedIdentityUpdaterReturn {
loading: boolean; loading: boolean;
handleAddSystemIdentity: () => Promise<void>; handleAddSystemIdentity: () => Promise<void>;
} }
const useManagedIdentity = ( const useManagedIdentity = (
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"] updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"],
): UseManagedIdentityUpdaterReturn => { ): UseManagedIdentityUpdaterReturn => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = 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> => {
try { try {
setLoading(true); setLoading(true);
const selectedTargetAccount = copyJobState?.target?.account; const selectedTargetAccount = copyJobState?.target?.account;
const { const {
subscriptionId: targetSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup, resourceGroup: targetResourceGroup,
accountName: targetAccountName accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const updatedAccount = await updateIdentityFn( const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName);
targetSubscriptionId, if (updatedAccount) {
targetResourceGroup, setCopyJobState((prevState) => ({
targetAccountName ...prevState,
); target: { ...prevState.target, account: updatedAccount },
if (updatedAccount) { }));
setCopyJobState((prevState) => ({ }
...prevState, } catch (error) {
target: { ...prevState.target, account: updatedAccount } console.error("Error enabling system-assigned managed identity:", error);
})); } finally {
} setLoading(false);
} catch (error) { }
console.error("Error enabling system-assigned managed identity:", error); }, [copyJobState, updateIdentityFn, setCopyJobState]);
} finally {
setLoading(false);
}
}, [copyJobState, updateIdentityFn, setCopyJobState]);
return { loading, handleAddSystemIdentity }; return { loading, handleAddSystemIdentity };
}; };
export default useManagedIdentity; export default useManagedIdentity;
@@ -1,17 +1,8 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
fetchRoleAssignments,
fetchRoleDefinitions,
RoleDefinitionType
} from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
import { import { BackupPolicyType, CopyJobMigrationType, DefaultIdentityType, IdentityType } from "../../../../Enums";
BackupPolicyType,
CopyJobMigrationType,
DefaultIdentityType,
IdentityType
} from "../../../../Enums";
import { CopyJobContextState } from "../../../../Types"; import { CopyJobContextState } from "../../../../Types";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache"; import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity"; import AddManagedIdentity from "../AddManagedIdentity";
@@ -21,112 +12,110 @@ import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore"; import PointInTimeRestore from "../PointInTimeRestore";
export interface PermissionSectionConfig { export interface PermissionSectionConfig {
id: string; id: string;
title: string; title: string;
Component: React.ComponentType Component: React.ComponentType;
disabled: boolean; disabled: boolean;
completed?: boolean; completed?: boolean;
validate?: (state: CopyJobContextState) => boolean | Promise<boolean>; validate?: (state: CopyJobContextState) => boolean | Promise<boolean>;
} }
// Section IDs for maintainability // Section IDs for maintainability
export const SECTION_IDS = { export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity", addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity", defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned", readPermissionAssigned: "readPermissionAssigned",
pointInTimeRestore: "pointInTimeRestore", pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled" onlineCopyEnabled: "onlineCopyEnabled",
} as const; } as const;
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.addManagedIdentity, id: SECTION_IDS.addManagedIdentity,
title: ContainerCopyMessages.addManagedIdentity.title, title: ContainerCopyMessages.addManagedIdentity.title,
Component: AddManagedIdentity, Component: AddManagedIdentity,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase(); const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase();
return ( return (
targetAccountIdentityType === IdentityType.SystemAssigned || targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned targetAccountIdentityType === IdentityType.UserAssigned
); );
}
}, },
{ },
id: SECTION_IDS.defaultManagedIdentity, {
title: ContainerCopyMessages.defaultManagedIdentity.title, id: SECTION_IDS.defaultManagedIdentity,
Component: DefaultManagedIdentity, title: ContainerCopyMessages.defaultManagedIdentity.title,
disabled: true, Component: DefaultManagedIdentity,
validate: (state: CopyJobContextState) => { disabled: true,
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase(); validate: (state: CopyJobContextState) => {
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity; const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
} return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
}, },
{ },
id: SECTION_IDS.readPermissionAssigned, {
title: ContainerCopyMessages.readPermissionAssigned.title, id: SECTION_IDS.readPermissionAssigned,
Component: AddReadPermissionToDefaultIdentity, title: ContainerCopyMessages.readPermissionAssigned.title,
disabled: true, Component: AddReadPermissionToDefaultIdentity,
validate: async (state: CopyJobContextState) => { disabled: true,
const principalId = state?.target?.account?.identity?.principalId; validate: async (state: CopyJobContextState) => {
const selectedSourceAccount = state?.source?.account; const principalId = state?.target?.account?.identity?.principalId;
const { const selectedSourceAccount = state?.source?.account;
subscriptionId: sourceSubscriptionId, const {
resourceGroup: sourceResourceGroup, subscriptionId: sourceSubscriptionId,
accountName: sourceAccountName resourceGroup: sourceResourceGroup,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
const rolesAssigned = await fetchRoleAssignments( const rolesAssigned = await fetchRoleAssignments(
sourceSubscriptionId, sourceSubscriptionId,
sourceResourceGroup, sourceResourceGroup,
sourceAccountName, sourceAccountName,
principalId principalId,
); );
const roleDefinitions = await fetchRoleDefinitions( const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
rolesAssigned ?? [] return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
); },
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []); },
}
}
]; ];
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.pointInTimeRestore, id: SECTION_IDS.pointInTimeRestore,
title: ContainerCopyMessages.pointInTimeRestore.title, title: ContainerCopyMessages.pointInTimeRestore.title,
Component: PointInTimeRestore, Component: PointInTimeRestore,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const sourceAccountBackupPolicy = state?.source?.account?.properties?.backupPolicy?.type ?? ""; const sourceAccountBackupPolicy = state?.source?.account?.properties?.backupPolicy?.type ?? "";
return sourceAccountBackupPolicy === BackupPolicyType.Continuous; return sourceAccountBackupPolicy === BackupPolicyType.Continuous;
}
}, },
{ },
id: SECTION_IDS.onlineCopyEnabled, {
title: ContainerCopyMessages.onlineCopyEnabled.title, id: SECTION_IDS.onlineCopyEnabled,
Component: OnlineCopyEnabled, title: ContainerCopyMessages.onlineCopyEnabled.title,
disabled: true, Component: OnlineCopyEnabled,
validate: (_state: CopyJobContextState) => { disabled: true,
return false; // eslint-disable-next-line @typescript-eslint/no-unused-vars
} validate: (_state: CopyJobContextState) => {
} return false;
},
},
]; ];
/** /**
* Checks if the user has the Reader role based on role definitions. * Checks if the user has the Reader role based on role definitions.
*/ */
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean { export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some( return roleDefinitions?.some(
role => (role) =>
role.name === "00000000-0000-0000-0000-000000000001" || role.name === "00000000-0000-0000-0000-000000000001" ||
role.permissions.some( role.permissions.some(
permission => (permission) =>
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") && permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") &&
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read") permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
) ),
); );
} }
/** /**
@@ -134,75 +123,76 @@ export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinition
* Memoizes derived values for performance and decouples logic for testability. * Memoizes derived values for performance and decouples logic for testability.
*/ */
const usePermissionSections = (state: CopyJobContextState): PermissionSectionConfig[] => { const usePermissionSections = (state: CopyJobContextState): PermissionSectionConfig[] => {
const { validationCache, setValidationCache } = useCopyJobPrerequisitesCache(); const { validationCache, setValidationCache } = useCopyJobPrerequisitesCache();
const [permissionSections, setPermissionSections] = useState<PermissionSectionConfig[] | null>(null); const [permissionSections, setPermissionSections] = useState<PermissionSectionConfig[] | null>(null);
const isValidatingRef = useRef(false); const isValidatingRef = useRef(false);
const sectionToValidate = useMemo(() => { const sectionToValidate = useMemo(() => {
const baseSections = [...PERMISSION_SECTIONS_CONFIG]; const baseSections = [...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];
}
return baseSections;
}, [state.migrationType]);
const memoizedValidationCache = useMemo(() => {
if (state.migrationType === CopyJobMigrationType.Offline) {
validationCache.delete(SECTION_IDS.pointInTimeRestore);
validationCache.delete(SECTION_IDS.onlineCopyEnabled);
}
return validationCache;
}, [state.migrationType]);
useEffect(() => {
const validateSections = async () => {
if (isValidatingRef.current) {
return;
}
isValidatingRef.current = true;
const result: PermissionSectionConfig[] = [];
const newValidationCache = new Map(memoizedValidationCache);
for (let i = 0; i < sectionToValidate.length; i++) {
const section = sectionToValidate[i];
// Check if this section was already validated and passed
if (newValidationCache.has(section.id) && newValidationCache.get(section.id) === true) {
result.push({ ...section, completed: true });
continue;
} }
return baseSections; // We've reached the first non-cached section - validate it
}, [state.migrationType]); if (section.validate) {
const isValid = await section.validate(state);
const memoizedValidationCache = useMemo(() => { newValidationCache.set(section.id, isValid);
if (state.migrationType === CopyJobMigrationType.Offline) { result.push({ ...section, completed: isValid });
validationCache.delete(SECTION_IDS.pointInTimeRestore); // Stop validation if current section failed
validationCache.delete(SECTION_IDS.onlineCopyEnabled); if (!isValid) {
} for (let j = i + 1; j < sectionToValidate.length; j++) {
return validationCache; result.push({ ...sectionToValidate[j], completed: false });
}, [state.migrationType]);
useEffect(() => {
const validateSections = async () => {
if (isValidatingRef.current) return;
isValidatingRef.current = true;
const result: PermissionSectionConfig[] = [];
const newValidationCache = new Map(memoizedValidationCache);
for (let i = 0; i < sectionToValidate.length; i++) {
const section = sectionToValidate[i];
// Check if this section was already validated and passed
if (newValidationCache.has(section.id) && newValidationCache.get(section.id) === true) {
result.push({ ...section, completed: true });
continue;
}
// We've reached the first non-cached section - validate it
if (section.validate) {
const isValid = await section.validate(state);
newValidationCache.set(section.id, isValid);
result.push({ ...section, completed: isValid });
// Stop validation if current section failed
if (!isValid) {
for (let j = i + 1; j < sectionToValidate.length; j++) {
result.push({ ...sectionToValidate[j], completed: false });
}
break;
}
}
else {
// Section has no validate method
newValidationCache.set(section.id, false);
result.push({ ...section, completed: false });
}
} }
break;
setValidationCache(newValidationCache); }
setPermissionSections(result); } else {
isValidatingRef.current = false; // Section has no validate method
newValidationCache.set(section.id, false);
result.push({ ...section, completed: false });
} }
}
validateSections(); setValidationCache(newValidationCache);
setPermissionSections(result);
isValidatingRef.current = false;
};
return () => { validateSections();
isValidatingRef.current = false;
}
}, [state, sectionToValidate]);
return permissionSections ?? []; return () => {
isValidatingRef.current = false;
};
}, [state, sectionToValidate]);
return permissionSections ?? [];
}; };
export default usePermissionSections; export default usePermissionSections;
@@ -1,11 +1,11 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
const useToggle = (initialState = false) => { const useToggle = (initialState = false) => {
const [state, setState] = useState<boolean>(initialState); const [state, setState] = useState<boolean>(initialState);
const onToggle = useCallback((_, checked?: boolean) => { const onToggle = useCallback((_, checked?: boolean) => {
setState(!!checked); setState(!!checked);
}, []); }, []);
return [state, onToggle] as const; return [state, onToggle] as const;
}; };
export default useToggle; export default useToggle;
@@ -1,32 +1,31 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
const useWindowOpenMonitor = (url: string, onClose?: () => void, intervalMs = 500) => { const useWindowOpenMonitor = (url: string, onClose?: () => void, intervalMs = 500) => {
const intervalRef = useRef<NodeJS.Timeout | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null);
const openWindowAndMonitor = () => { const openWindowAndMonitor = () => {
const newWindow = window.open(url, '_blank'); const newWindow = window.open(url, "_blank");
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
if (newWindow?.closed) { if (newWindow?.closed) {
clearInterval(intervalRef.current!); clearInterval(intervalRef.current!);
intervalRef.current = null; intervalRef.current = null;
console.log('New window has been closed!'); if (onClose) {
if (onClose) { onClose();
onClose(); }
} }
} }, intervalMs);
}, intervalMs); };
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}; };
}, []);
useEffect(() => { return openWindowAndMonitor;
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
return openWindowAndMonitor;
}; };
export default useWindowOpenMonitor; export default useWindowOpenMonitor;
@@ -1,10 +1,5 @@
import { Image, Stack, Text } from "@fluentui/react"; import { Image, Stack, Text } from "@fluentui/react";
import { import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
Accordion,
AccordionHeader,
AccordionItem,
AccordionPanel
} from "@fluentui/react-components";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg"; import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg"; import WarningIcon from "../../../../../../images/warning.svg";
@@ -14,72 +9,58 @@ import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums"; import { CopyJobMigrationType } from "../../../Enums";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection"; import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
id, <AccordionItem key={id} value={id} disabled={disabled}>
title, <AccordionHeader className="accordionHeader">
Component, <Text className="accordionHeaderText" variant="medium">
completed, {title}
disabled </Text>
}) => ( <Image
<AccordionItem key={id} value={id} disabled={disabled}> className="statusIcon"
<AccordionHeader className="accordionHeader"> src={completed ? CheckmarkIcon : WarningIcon}
<Text className="accordionHeaderText" variant="medium">{title}</Text> alt={completed ? "Checkmark icon" : "Warning icon"}
<Image width={completed ? 20 : 24}
className="statusIcon" height={completed ? 20 : 24}
src={completed ? CheckmarkIcon : WarningIcon} />
alt={completed ? "Checkmark icon" : "Warning icon"} </AccordionHeader>
width={completed ? 20 : 24} <AccordionPanel aria-disabled={disabled} className="accordionPanel">
height={completed ? 20 : 24} <Component />
/> </AccordionPanel>
</AccordionHeader> </AccordionItem>
<AccordionPanel aria-disabled={disabled} className="accordionPanel" >
<Component />
</AccordionPanel>
</AccordionItem>
); );
const AssignPermissions = () => { const AssignPermissions = () => {
const { copyJobState } = useCopyJobContext(); const { copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState); const permissionSections = usePermissionSections(copyJobState);
const [openItems, setOpenItems] = React.useState<string[]>([]); const [openItems, setOpenItems] = React.useState<string[]>([]);
const indentLevels = React.useMemo<IndentLevel[]>( const indentLevels = React.useMemo<IndentLevel[]>(
() => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }), () => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }),
[] [],
); );
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] : [];
if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) { if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) {
setOpenItems(nextOpenItems); setOpenItems(nextOpenItems);
} }
}, [permissionSections]); }, [permissionSections]);
return ( return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}> <Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span> <span>{ContainerCopyMessages.assignPermissions.description}</span>
{ContainerCopyMessages.assignPermissions.description} {permissionSections?.length === 0 ? (
</span> <ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} />
{ ) : (
permissionSections?.length === 0 ? ( <Accordion className="permissionsAccordion" collapsible openItems={openItems}>
<ShimmerTree indentLevels={indentLevels} style={{ width: '100%' }} /> {permissionSections.map((section) => (
) : ( <PermissionSection key={section.id} {...section} />
<Accordion ))}
className="permissionsAccordion" </Accordion>
collapsible )}
openItems={openItems} </Stack>
> );
{
permissionSections.map(section => (
<PermissionSection key={section.id} {...section} />
))
}
</Accordion>
)
}
</Stack>
);
}; };
export default AssignPermissions; export default AssignPermissions;
@@ -2,26 +2,24 @@ import { Stack } from "@fluentui/react";
import React from "react"; import React from "react";
interface FieldRowProps { interface FieldRowProps {
label?: string; label?: string;
children: React.ReactNode; children: React.ReactNode;
labelClassName?: string; labelClassName?: string;
} }
const FieldRow: React.FC<FieldRowProps> = ({ label = "", children, labelClassName = "" }) => { const FieldRow: React.FC<FieldRowProps> = ({ label = "", children, labelClassName = "" }) => {
return ( return (
<Stack horizontal horizontalAlign="space-between" className="flex-row"> <Stack horizontal horizontalAlign="space-between" className="flex-row">
{ {label && (
label && ( <Stack.Item align="center" className="flex-fixed-width">
<Stack.Item align="center" className="flex-fixed-width"> <label className={`field-label ${labelClassName}`}>{label}: </label>
<label className={`field-label ${labelClassName}`}>{label}: </label> </Stack.Item>
</Stack.Item> )}
) <Stack.Item align="center" className="flex-grow-col">
} {children}
<Stack.Item align="center" className="flex-grow-col"> </Stack.Item>
{children} </Stack>
</Stack.Item> );
</Stack>
);
}; };
export default FieldRow; export default FieldRow;
@@ -3,13 +3,15 @@ import React from "react";
import InfoIcon from "../../../../../../images/Info.svg"; import InfoIcon from "../../../../../../images/Info.svg";
const InfoTooltip: React.FC<{ content?: string }> = ({ content }) => { const InfoTooltip: React.FC<{ content?: string }> = ({ content }) => {
if (!content) return null; if (!content) {
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } }; return null;
return ( }
<TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}> const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
<Image src={InfoIcon} alt="Information" width={14} height={14} /> return (
</TooltipHost> <TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}>
); <Image src={InfoIcon} alt="Information" width={14} height={14} />
</TooltipHost>
);
}; };
export default React.memo(InfoTooltip); export default React.memo(InfoTooltip);
@@ -2,27 +2,27 @@ import { DefaultButton, PrimaryButton, Stack } from "@fluentui/react";
import React from "react"; import React from "react";
type NavigationControlsProps = { type NavigationControlsProps = {
primaryBtnText: string; primaryBtnText: string;
onPrimary: () => void; onPrimary: () => void;
onPrevious: () => void; onPrevious: () => void;
onCancel: () => void; onCancel: () => void;
isPrimaryDisabled: boolean; isPrimaryDisabled: boolean;
isPreviousDisabled: boolean; isPreviousDisabled: boolean;
}; };
const NavigationControls: React.FC<NavigationControlsProps> = ({ const NavigationControls: React.FC<NavigationControlsProps> = ({
primaryBtnText, primaryBtnText,
onPrimary, onPrimary,
onPrevious, onPrevious,
onCancel, onCancel,
isPrimaryDisabled, isPrimaryDisabled,
isPreviousDisabled, isPreviousDisabled,
}) => ( }) => (
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} /> <PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} /> <DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
<DefaultButton text="Cancel" onClick={onCancel} /> <DefaultButton text="Cancel" onClick={onCancel} />
</Stack> </Stack>
); );
export default React.memo(NavigationControls); export default React.memo(NavigationControls);
@@ -1,48 +1,67 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react"; import React from "react";
interface PopoverContainerProps { interface PopoverContainerProps {
isLoading?: boolean; isLoading?: boolean;
title?: string; title?: string;
children?: React.ReactNode; children?: React.ReactNode;
onPrimary: () => void; onPrimary: () => void;
onCancel: () => void; onCancel: () => void;
} }
const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ isLoading = false, title, children, onPrimary, onCancel }) => { const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
({ isLoading = false, title, children, onPrimary, onCancel }) => {
return ( return (
<Stack className={`popover-container foreground ${isLoading ? "loading" : ""}`} tokens={{ childrenGap: 20 }} style={{ maxWidth: 450 }}> <Stack
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>{title}</Text> className={`popover-container foreground ${isLoading ? "loading" : ""}`}
<Text>{children}</Text> tokens={{ childrenGap: 20 }}
<Stack horizontal tokens={{ childrenGap: 20 }}> style={{ maxWidth: 450 }}
<PrimaryButton >
text={isLoading ? "" : "Yes"} <Text variant="mediumPlus" style={{ fontWeight: 600 }}>
{...(isLoading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {title}
onClick={onPrimary} </Text>
disabled={isLoading} <Text>{children}</Text>
/> <Stack horizontal tokens={{ childrenGap: 20 }}>
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} /> <PrimaryButton
</Stack> text={isLoading ? "" : "Yes"}
{...(isLoading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
onClick={onPrimary}
disabled={isLoading}
/>
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
</Stack> </Stack>
</Stack>
); );
}); },
);
interface PopoverMessageProps { interface PopoverMessageProps {
isLoading?: boolean; isLoading?: boolean;
visible: boolean; visible: boolean;
title: string; title: string;
onCancel: () => void; onCancel: () => void;
onPrimary: () => void; onPrimary: () => void;
children: React.ReactNode; children: React.ReactNode;
} }
const PopoverMessage: React.FC<PopoverMessageProps> = ({ isLoading = false, visible, title, onCancel, onPrimary, children }) => { const PopoverMessage: React.FC<PopoverMessageProps> = ({
if (!visible) return null; isLoading = false,
return ( visible,
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}> title,
{children} onCancel,
</PopoverContainer> onPrimary,
); children,
}) => {
if (!visible) {
return null;
}
return (
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}>
{children}
</PopoverContainer>
);
}; };
export default PopoverMessage; export default PopoverMessage;
@@ -4,31 +4,31 @@ import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
import NavigationControls from "./Components/NavigationControls"; import NavigationControls from "./Components/NavigationControls";
const CreateCopyJobScreens: React.FC = () => { const CreateCopyJobScreens: React.FC = () => {
const { const {
currentScreen, currentScreen,
isPrimaryDisabled, isPrimaryDisabled,
isPreviousDisabled, isPreviousDisabled,
handlePrimary, handlePrimary,
handlePrevious, handlePrevious,
handleCancel, handleCancel,
primaryBtnText primaryBtnText,
} = useCopyJobNavigation(); } = useCopyJobNavigation();
return ( return (
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer"> <Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
<Stack.Item className="createCopyJobScreensContent">{currentScreen?.component}</Stack.Item> <Stack.Item className="createCopyJobScreensContent">{currentScreen?.component}</Stack.Item>
<Stack.Item className="createCopyJobScreensFooter"> <Stack.Item className="createCopyJobScreensFooter">
<NavigationControls <NavigationControls
primaryBtnText={primaryBtnText} primaryBtnText={primaryBtnText}
onPrimary={handlePrimary} onPrimary={handlePrimary}
onPrevious={handlePrevious} onPrevious={handlePrevious}
onCancel={handleCancel} onCancel={handleCancel}
isPrimaryDisabled={isPrimaryDisabled} isPrimaryDisabled={isPrimaryDisabled}
isPreviousDisabled={isPreviousDisabled} isPreviousDisabled={isPreviousDisabled}
/> />
</Stack.Item> </Stack.Item>
</Stack> </Stack>
); );
}; };
export default CreateCopyJobScreens; export default CreateCopyJobScreens;
@@ -3,11 +3,11 @@ import CopyJobContextProvider from "../../Context/CopyJobContext";
import CreateCopyJobScreens from "./CreateCopyJobScreens"; import CreateCopyJobScreens from "./CreateCopyJobScreens";
const CreateCopyJobScreensProvider = () => { const CreateCopyJobScreensProvider = () => {
return ( return (
<CopyJobContextProvider> <CopyJobContextProvider>
<CreateCopyJobScreens /> <CreateCopyJobScreens />
</CopyJobContextProvider> </CopyJobContextProvider>
); );
}; };
export default CreateCopyJobScreensProvider; export default CreateCopyJobScreensProvider;
@@ -2,42 +2,42 @@ import { IColumn } from "@fluentui/react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
const commonProps = { const commonProps = {
minWidth: 130, minWidth: 130,
maxWidth: 140, maxWidth: 140,
styles: { styles: {
root: { root: {
whiteSpace: 'normal', whiteSpace: "normal",
lineHeight: '1.2', lineHeight: "1.2",
wordBreak: 'break-word' wordBreak: "break-word",
} },
} },
}; };
export const getPreviewCopyJobDetailsListColumns = function (): IColumn[] { export const getPreviewCopyJobDetailsListColumns = (): IColumn[] => {
return [ return [
{ {
key: 'sourcedbname', key: "sourcedbname",
name: ContainerCopyMessages.sourceDatabaseLabel, name: ContainerCopyMessages.sourceDatabaseLabel,
fieldName: 'sourceDatabaseName', fieldName: "sourceDatabaseName",
...commonProps ...commonProps,
}, },
{ {
key: 'sourcecolname', key: "sourcecolname",
name: ContainerCopyMessages.sourceContainerLabel, name: ContainerCopyMessages.sourceContainerLabel,
fieldName: 'sourceContainerName', fieldName: "sourceContainerName",
...commonProps ...commonProps,
}, },
{ {
key: 'targetdbname', key: "targetdbname",
name: ContainerCopyMessages.targetDatabaseLabel, name: ContainerCopyMessages.targetDatabaseLabel,
fieldName: 'targetDatabaseName', fieldName: "targetDatabaseName",
...commonProps ...commonProps,
}, },
{ {
key: 'targetcolname', key: "targetcolname",
name: ContainerCopyMessages.targetContainerLabel, name: ContainerCopyMessages.targetContainerLabel,
fieldName: 'targetContainerName', fieldName: "targetContainerName",
...commonProps ...commonProps,
} },
]; ];
}; };
@@ -1,53 +1,52 @@
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from '@fluentui/react'; import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react";
import FieldRow from 'Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow'; import FieldRow from "Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow";
import React from 'react'; import React from "react";
import ContainerCopyMessages from '../../../ContainerCopyMessages'; import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from '../../../Context/CopyJobContext'; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getPreviewCopyJobDetailsListColumns } from './Utils/PreviewCopyJobUtils'; import { getPreviewCopyJobDetailsListColumns } from "./Utils/PreviewCopyJobUtils";
const PreviewCopyJob: React.FC = () => { const PreviewCopyJob: React.FC = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedDatabaseAndContainers = [{ const selectedDatabaseAndContainers = [
sourceDatabaseName: copyJobState.source?.databaseId, {
sourceContainerName: copyJobState.source?.containerId, sourceDatabaseName: copyJobState.source?.databaseId,
targetDatabaseName: copyJobState.target?.databaseId, sourceContainerName: copyJobState.source?.containerId,
targetContainerName: copyJobState.target?.containerId, targetDatabaseName: copyJobState.target?.databaseId,
}]; targetContainerName: copyJobState.target?.containerId,
const jobName = copyJobState.jobName; },
];
const jobName = copyJobState.jobName;
const onJobNameChange = (_ev?: React.FormEvent, newValue?: string) => { const onJobNameChange = (_ev?: React.FormEvent, newValue?: string) => {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
jobName: newValue || '', jobName: newValue || "",
})); }));
}; };
return ( return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer"> <Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
<FieldRow label={ContainerCopyMessages.jobNameLabel}> <FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField <TextField value={jobName} onChange={onJobNameChange} />
value={jobName} </FieldRow>
onChange={onJobNameChange} <Stack>
/> <Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
</FieldRow> <Text>{copyJobState.source?.subscription?.displayName}</Text>
<Stack> </Stack>
<Text className='bold'>{ContainerCopyMessages.sourceSubscriptionLabel}</Text> <Stack>
<Text>{copyJobState.source?.subscription?.displayName}</Text> <Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
</Stack> <Text>{copyJobState.source?.account?.name}</Text>
<Stack> </Stack>
<Text className='bold'>{ContainerCopyMessages.sourceAccountLabel}</Text> <Stack>
<Text>{copyJobState.source?.account?.name}</Text> <DetailsList
</Stack> items={selectedDatabaseAndContainers}
<Stack> layoutMode={DetailsListLayoutMode.justified}
<DetailsList checkboxVisibility={2}
items={selectedDatabaseAndContainers} columns={getPreviewCopyJobDetailsListColumns()}
layoutMode={DetailsListLayoutMode.justified} />
checkboxVisibility={2} </Stack>
columns={getPreviewCopyJobDetailsListColumns()} </Stack>
/> );
</Stack>
</Stack>
);
}; };
export default PreviewCopyJob; export default PreviewCopyJob;
@@ -1,3 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -5,24 +7,24 @@ import { DropdownOptionType } from "../../../../Types";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps { interface AccountDropdownProps {
options: DropdownOptionType[]; options: DropdownOptionType[];
selectedKey?: string; selectedKey?: string;
disabled: boolean; disabled: boolean;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void; onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
} }
export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo( export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo(
({ options, selectedKey, disabled, onChange }) => ( ({ options, selectedKey, disabled, onChange }) => (
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}> <FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder} placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel} ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
options={options} options={options}
disabled={disabled} disabled={disabled}
required required
selectedKey={selectedKey} selectedKey={selectedKey}
onChange={onChange} onChange={onChange}
/> />
</FieldRow> </FieldRow>
) ),
); );
@@ -1,20 +1,16 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Checkbox, Stack } from "@fluentui/react"; import { Checkbox, Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
interface MigrationTypeCheckboxProps { interface MigrationTypeCheckboxProps {
checked: boolean; checked: boolean;
onChange: (_ev?: React.FormEvent, checked?: boolean) => void; onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
} }
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo( export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
({ checked, onChange }) => ( <Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow"> <Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
<Checkbox </Stack>
label={ContainerCopyMessages.migrationTypeCheckboxLabel} ));
checked={checked}
onChange={onChange}
/>
</Stack>
)
);
@@ -1,3 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -5,22 +7,22 @@ import { DropdownOptionType } from "../../../../Types";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
interface SubscriptionDropdownProps { interface SubscriptionDropdownProps {
options: DropdownOptionType[]; options: DropdownOptionType[];
selectedKey?: string; selectedKey?: string;
onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void; onChange: (_ev?: React.FormEvent, option?: DropdownOptionType) => void;
} }
export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.memo( export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.memo(
({ options, selectedKey, onChange }) => ( ({ options, selectedKey, onChange }) => (
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}> <FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder} placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel} ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel}
options={options} options={options}
required required
selectedKey={selectedKey} selectedKey={selectedKey}
onChange={onChange} onChange={onChange}
/> />
</FieldRow> </FieldRow>
) ),
); );
@@ -4,75 +4,75 @@ import { CopyJobMigrationType } from "../../../../Enums";
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types"; import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function useDropdownOptions( export function useDropdownOptions(
subscriptions: Subscription[], subscriptions: Subscription[],
accounts: DatabaseAccount[] accounts: DatabaseAccount[],
): { ): {
subscriptionOptions: DropdownOptionType[], subscriptionOptions: DropdownOptionType[];
accountOptions: DropdownOptionType[] accountOptions: DropdownOptionType[];
} { } {
const subscriptionOptions = React.useMemo( const subscriptionOptions = React.useMemo(
() => () =>
subscriptions?.map((sub) => ({ subscriptions?.map((sub) => ({
key: sub.subscriptionId, key: sub.subscriptionId,
text: sub.displayName, text: sub.displayName,
data: sub, data: sub,
})) || [], })) || [],
[subscriptions] [subscriptions],
); );
const accountOptions = React.useMemo( const accountOptions = React.useMemo(
() => () =>
accounts?.map((account) => ({ accounts?.map((account) => ({
key: account.id, key: account.id,
text: account.name, text: account.name,
data: account, data: account,
})) || [], })) || [],
[accounts] [accounts],
); );
return { subscriptionOptions, accountOptions }; return { subscriptionOptions, accountOptions };
} }
type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"]; type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
export function useEventHandlers(setCopyJobState: setCopyJobStateType) { export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
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) => {
if (type === "subscription") { if (type === "subscription") {
return { return {
...prevState, ...prevState,
source: { source: {
...prevState.source, ...prevState.source,
subscription: data || null, subscription: data || null,
account: null, // reset on subscription change account: null, // reset on subscription change
}, },
}; };
} }
if (type === "account") { if (type === "account") {
return { return {
...prevState, ...prevState,
source: { source: {
...prevState.source, ...prevState.source,
account: data || null, account: data || null,
}, },
}; };
} }
return prevState; return prevState;
}); });
}, },
[setCopyJobState] [setCopyJobState],
); );
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,
})); }));
}, },
[setCopyJobState] [setCopyJobState],
); );
return { handleSelectSourceAccount, handleMigrationTypeChange }; return { handleSelectSourceAccount, handleMigrationTypeChange };
} }
@@ -1,3 +1,4 @@
/* eslint-disable react/display-name */
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels"; import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
@@ -11,46 +12,41 @@ import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown"; import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils"; import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
interface SelectAccountProps { } const SelectAccount = React.memo(() => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const SelectAccount = React.memo( const subscriptions: Subscription[] = useSubscriptions();
(_props: SelectAccountProps) => { const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const { copyJobState, setCopyJobState } = useCopyJobContext(); const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter(
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; (account) => account.type === "SQL" || account.kind === "GlobalDocumentDB",
);
const subscriptions: Subscription[] = useSubscriptions(); const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts);
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter(account => account.type === "SQL" || account.kind === "GlobalDocumentDB");
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts); const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline;
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline; return (
<Stack className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<span>{ContainerCopyMessages.selectAccountDescription}</span>
return ( <SubscriptionDropdown
<Stack className="selectAccountContainer" tokens={{ childrenGap: 15 }}> options={subscriptionOptions}
<span>{ContainerCopyMessages.selectAccountDescription}</span> selectedKey={selectedSubscriptionId}
onChange={(_ev, option) => handleSelectSourceAccount("subscription", option?.data)}
/>
<SubscriptionDropdown <AccountDropdown
options={subscriptionOptions} options={accountOptions}
selectedKey={selectedSubscriptionId} selectedKey={copyJobState?.source?.account?.id}
onChange={(_ev, option) => handleSelectSourceAccount("subscription", option?.data)} disabled={!selectedSubscriptionId}
/> onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/>
<AccountDropdown <MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
options={accountOptions} </Stack>
selectedKey={copyJobState?.source?.account?.id} );
disabled={!selectedSubscriptionId} });
onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/>
<MigrationTypeCheckbox
checked={migrationTypeChecked}
onChange={handleMigrationTypeChange}
/>
</Stack>
);
}
);
export default SelectAccount; export default SelectAccount;
@@ -1,36 +1,35 @@
import React from "react";
import { CopyJobContextState, DropdownOptionType } from "../../../../Types"; import { CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function dropDownChangeHandler( export function dropDownChangeHandler(setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>) {
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>> return (type: "sourceDatabase" | "sourceContainer" | "targetDatabase" | "targetContainer") =>
) { (_evnt: React.FormEvent, option: DropdownOptionType) => {
return (type: "sourceDatabase" | "sourceContainer" | "targetDatabase" | "targetContainer") => const value = option.key;
(_evnt: any, option: DropdownOptionType) => { setCopyJobState((prevState) => {
const value = option.key; switch (type) {
setCopyJobState((prevState) => { case "sourceDatabase":
switch (type) { return {
case "sourceDatabase": ...prevState,
return { source: { ...prevState.source, databaseId: value, containerId: undefined },
...prevState, };
source: { ...prevState.source, databaseId: value, containerId: undefined } case "sourceContainer":
}; return {
case "sourceContainer": ...prevState,
return { source: { ...prevState.source, containerId: value },
...prevState, };
source: { ...prevState.source, containerId: value } case "targetDatabase":
}; return {
case "targetDatabase": ...prevState,
return { target: { ...prevState.target, databaseId: value, containerId: undefined },
...prevState, };
target: { ...prevState.target, databaseId: value, containerId: undefined } case "targetContainer":
}; return {
case "targetContainer": ...prevState,
return { target: { ...prevState.target, containerId: value },
...prevState, };
target: { ...prevState.target, containerId: value } default:
}; return prevState;
default:
return prevState;
}
});
} }
});
};
} }
@@ -5,39 +5,39 @@ import { DatabaseContainerSectionProps } from "../../../../Types";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
export const DatabaseContainerSection = ({ export const DatabaseContainerSection = ({
heading, heading,
databaseOptions, databaseOptions,
selectedDatabase, selectedDatabase,
databaseDisabled, databaseDisabled,
databaseOnChange, databaseOnChange,
containerOptions, containerOptions,
selectedContainer, selectedContainer,
containerDisabled, containerDisabled,
containerOnChange containerOnChange,
}: DatabaseContainerSectionProps) => ( }: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection"> <Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label> <label className="subHeading">{heading}</label>
<FieldRow label={ContainerCopyMessages.databaseDropdownLabel}> <FieldRow label={ContainerCopyMessages.databaseDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.databaseDropdownPlaceholder} placeholder={ContainerCopyMessages.databaseDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.databaseDropdownLabel} ariaLabel={ContainerCopyMessages.databaseDropdownLabel}
options={databaseOptions} options={databaseOptions}
required required
disabled={!!databaseDisabled} disabled={!!databaseDisabled}
selectedKey={selectedDatabase} selectedKey={selectedDatabase}
onChange={databaseOnChange} onChange={databaseOnChange}
/> />
</FieldRow> </FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}> <FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.containerDropdownPlaceholder} placeholder={ContainerCopyMessages.containerDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.containerDropdownLabel} ariaLabel={ContainerCopyMessages.containerDropdownLabel}
options={containerOptions} options={containerOptions}
required required
disabled={!!containerDisabled} disabled={!!containerDisabled}
selectedKey={selectedContainer} selectedKey={selectedContainer}
onChange={containerOnChange} onChange={containerOnChange}
/> />
</FieldRow> </FieldRow>
</Stack> </Stack>
); );
@@ -1,4 +1,5 @@
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import { DatabaseModel } from "Contracts/DataModels";
import React from "react"; import React from "react";
import { useDatabases } from "../../../../../hooks/useDatabases"; import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers"; import { useDataContainers } from "../../../../../hooks/useDataContainers";
@@ -8,73 +9,64 @@ import { DatabaseContainerSection } from "./components/DatabaseContainerSection"
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler"; import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
import { useMemoizedSourceAndTargetData } from "./memoizedData"; import { useMemoizedSourceAndTargetData } from "./memoizedData";
interface SelectSourceAndTargetContainersProps { } const SelectSourceAndTargetContainers = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
useMemoizedSourceAndTargetData(copyJobState);
const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainersProps) => { // Custom hooks
const { copyJobState, setCopyJobState } = useCopyJobContext(); const sourceDatabases = useDatabases(...sourceDbParams) || [];
const { const sourceContainers = useDataContainers(...sourceContainerParams) || [];
source, const targetDatabases = useDatabases(...targetDbParams) || [];
target, const targetContainers = useDataContainers(...targetContainerParams) || [];
sourceDbParams,
sourceContainerParams,
targetDbParams,
targetContainerParams
} = useMemoizedSourceAndTargetData(copyJobState);
// Custom hooks // Memoize option objects for dropdowns
const sourceDatabases = useDatabases(...sourceDbParams) || []; const sourceDatabaseOptions = React.useMemo(
const sourceContainers = useDataContainers(...sourceContainerParams) || []; () => sourceDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
const targetDatabases = useDatabases(...targetDbParams) || []; [sourceDatabases],
const targetContainers = useDataContainers(...targetContainerParams) || []; );
const sourceContainerOptions = React.useMemo(
() => sourceContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
[sourceContainers],
);
const targetDatabaseOptions = React.useMemo(
() => targetDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
[targetDatabases],
);
const targetContainerOptions = React.useMemo(
() => targetContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
[targetContainers],
);
// Memoize option objects for dropdowns const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
const sourceDatabaseOptions = React.useMemo(
() => sourceDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })),
[sourceDatabases]
);
const sourceContainerOptions = React.useMemo(
() => sourceContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })),
[sourceContainers]
);
const targetDatabaseOptions = React.useMemo(
() => targetDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })),
[targetDatabases]
);
const targetContainerOptions = React.useMemo(
() => targetContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })),
[targetContainers]
);
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]); return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
return ( <span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}> <DatabaseContainerSection
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span> heading={ContainerCopyMessages.sourceContainerSubHeading}
<DatabaseContainerSection databaseOptions={sourceDatabaseOptions}
heading={ContainerCopyMessages.sourceContainerSubHeading} selectedDatabase={source?.databaseId}
databaseOptions={sourceDatabaseOptions} databaseDisabled={false}
selectedDatabase={source?.databaseId} databaseOnChange={onDropdownChange("sourceDatabase")}
databaseDisabled={false} containerOptions={sourceContainerOptions}
databaseOnChange={onDropdownChange("sourceDatabase")} selectedContainer={source?.containerId}
containerOptions={sourceContainerOptions} containerDisabled={!source?.databaseId}
selectedContainer={source?.containerId} containerOnChange={onDropdownChange("sourceContainer")}
containerDisabled={!source?.databaseId} />
containerOnChange={onDropdownChange("sourceContainer")} <DatabaseContainerSection
/> heading={ContainerCopyMessages.targetContainerSubHeading}
<DatabaseContainerSection databaseOptions={targetDatabaseOptions}
heading={ContainerCopyMessages.targetContainerSubHeading} selectedDatabase={target?.databaseId}
databaseOptions={targetDatabaseOptions} databaseDisabled={false}
selectedDatabase={target?.databaseId} databaseOnChange={onDropdownChange("targetDatabase")}
databaseDisabled={false} containerOptions={targetContainerOptions}
databaseOnChange={onDropdownChange("targetDatabase")} selectedContainer={target?.containerId}
containerOptions={targetContainerOptions} containerDisabled={!target?.databaseId}
selectedContainer={target?.containerId} containerOnChange={onDropdownChange("targetContainer")}
containerDisabled={!target?.databaseId} />
containerOnChange={onDropdownChange("targetContainer")} </Stack>
/> );
</Stack>
);
}; };
export default SelectSourceAndTargetContainers;
export default SelectSourceAndTargetContainers;
@@ -3,63 +3,41 @@ import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types"; import { CopyJobContextState, DatabaseParams, DataContainerParams } from "../../../Types";
export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState) { export function useMemoizedSourceAndTargetData(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;
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
accountName: sourceAccountName accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
const { const {
subscriptionId: targetSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup, resourceGroup: targetResourceGroup,
accountName: targetAccountName accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const sourceDbParams = React.useMemo( const sourceDbParams = React.useMemo(
() => () => [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, "SQL"] as DatabaseParams,
[ [sourceSubscriptionId, sourceResourceGroup, sourceAccountName],
sourceSubscriptionId, );
sourceResourceGroup,
sourceAccountName,
'SQL',
] as DatabaseParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName]
);
const sourceContainerParams = React.useMemo( const sourceContainerParams = React.useMemo(
() => () =>
[ [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId, "SQL"] as DataContainerParams,
sourceSubscriptionId, [sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId],
sourceResourceGroup, );
sourceAccountName,
source?.databaseId,
'SQL',
] as DataContainerParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId]
);
const targetDbParams = React.useMemo( const targetDbParams = React.useMemo(
() => [ () => [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams,
targetSubscriptionId, [targetSubscriptionId, targetResourceGroup, targetAccountName],
targetResourceGroup, );
targetAccountName,
'SQL',
] as DatabaseParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName]
);
const targetContainerParams = React.useMemo( const targetContainerParams = React.useMemo(
() => [ () =>
targetSubscriptionId, [targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId, "SQL"] as DataContainerParams,
targetResourceGroup, [targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId],
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 };
} }
@@ -6,90 +6,84 @@ import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList"; import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
type NavigationState = { type NavigationState = {
screenHistory: string[]; screenHistory: string[];
}; };
type Action = type Action = { type: "NEXT"; nextScreen: string } | { type: "PREVIOUS" } | { type: "RESET" };
| { type: "NEXT"; nextScreen: string }
| { type: "PREVIOUS" }
| { type: "RESET" };
function navigationReducer(state: NavigationState, action: Action): NavigationState { function navigationReducer(state: NavigationState, action: Action): NavigationState {
switch (action.type) { switch (action.type) {
case "NEXT": case "NEXT":
return { return {
screenHistory: [...state.screenHistory, action.nextScreen], screenHistory: [...state.screenHistory, action.nextScreen],
}; };
case "PREVIOUS": case "PREVIOUS":
return { return {
screenHistory: state.screenHistory.length > 1 ? state.screenHistory.slice(0, -1) : state.screenHistory, screenHistory: state.screenHistory.length > 1 ? state.screenHistory.slice(0, -1) : state.screenHistory,
}; };
case "RESET": case "RESET":
return { return {
screenHistory: [SCREEN_KEYS.SelectAccount], screenHistory: [SCREEN_KEYS.SelectAccount],
}; };
default: default:
return state; return state;
} }
} }
export function useCopyJobNavigation() { export function useCopyJobNavigation() {
const { copyJobState, resetCopyJobState } = 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] });
const currentScreenKey = state.screenHistory[state.screenHistory.length - 1]; const currentScreenKey = state.screenHistory[state.screenHistory.length - 1];
const currentScreen = screens.find((screen) => screen.key === currentScreenKey); const currentScreen = screens.find((screen) => screen.key === currentScreenKey);
const isPrimaryDisabled = useMemo( const isPrimaryDisabled = useMemo(() => {
() => { const context = currentScreenKey === SCREEN_KEYS.AssignPermissions ? cache : copyJobState;
const context = currentScreenKey === SCREEN_KEYS.AssignPermissions ? cache : copyJobState; return !currentScreen?.validations.every((v) => v.validate(context));
return !currentScreen?.validations.every((v) => v.validate(context)); }, [currentScreen.key, copyJobState, cache]);
}, const primaryBtnText = useMemo(() => {
[currentScreen.key, copyJobState, cache] if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
); return "Copy";
const primaryBtnText = useMemo(() => { }
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) { return "Next";
return "Copy"; }, [currentScreenKey]);
}
return "Next";
}, [currentScreenKey]);
const isPreviousDisabled = state.screenHistory.length <= 1; const isPreviousDisabled = state.screenHistory.length <= 1;
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
dispatch({ type: "RESET" }); dispatch({ type: "RESET" });
resetCopyJobState(); resetCopyJobState();
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
}, []); }, []);
const handlePrimary = useCallback(() => { const handlePrimary = useCallback(() => {
const transitions = { const transitions = {
[SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions, [SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions,
[SCREEN_KEYS.AssignPermissions]: SCREEN_KEYS.SelectSourceAndTargetContainers, [SCREEN_KEYS.AssignPermissions]: SCREEN_KEYS.SelectSourceAndTargetContainers,
[SCREEN_KEYS.SelectSourceAndTargetContainers]: SCREEN_KEYS.PreviewCopyJob, [SCREEN_KEYS.SelectSourceAndTargetContainers]: SCREEN_KEYS.PreviewCopyJob,
};
const nextScreen = transitions[currentScreenKey];
if (nextScreen) {
dispatch({ type: "NEXT", nextScreen });
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
submitCreateCopyJob(copyJobState, handleCancel);
}
}, [currentScreenKey, copyJobState]);
const handlePrevious = useCallback(() => {
dispatch({ type: "PREVIOUS" });
}, []);
return {
currentScreen,
isPrimaryDisabled,
isPreviousDisabled,
handlePrimary,
handlePrevious,
handleCancel,
primaryBtnText,
}; };
const nextScreen = transitions[currentScreenKey];
if (nextScreen) {
dispatch({ type: "NEXT", nextScreen });
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
submitCreateCopyJob(copyJobState, handleCancel);
}
}, [currentScreenKey, copyJobState]);
const handlePrevious = useCallback(() => {
dispatch({ type: "PREVIOUS" });
}, []);
return {
currentScreen,
isPrimaryDisabled,
isPreviousDisabled,
handlePrimary,
handlePrevious,
handleCancel,
primaryBtnText,
};
} }
@@ -1,11 +1,11 @@
import create from "zustand"; import create from "zustand";
interface CopyJobPrerequisitesCacheState { interface CopyJobPrerequisitesCacheState {
validationCache: Map<string, boolean>; validationCache: Map<string, boolean>;
setValidationCache: (cache: Map<string, boolean>) => void; setValidationCache: (cache: Map<string, boolean>) => void;
} }
export const useCopyJobPrerequisitesCache = create<CopyJobPrerequisitesCacheState>((set) => ({ export const useCopyJobPrerequisitesCache = create<CopyJobPrerequisitesCacheState>((set) => ({
validationCache: new Map<string, boolean>(), validationCache: new Map<string, boolean>(),
setValidationCache: (cache) => set({ validationCache: cache }) setValidationCache: (cache) => set({ validationCache: cache }),
})); }));
@@ -6,82 +6,82 @@ import SelectAccount from "../Screens/SelectAccount";
import SelectSourceAndTargetContainers from "../Screens/SelectSourceAndTargetContainers"; import SelectSourceAndTargetContainers from "../Screens/SelectSourceAndTargetContainers";
const SCREEN_KEYS = { const SCREEN_KEYS = {
SelectAccount: "SelectAccount", SelectAccount: "SelectAccount",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers", SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
PreviewCopyJob: "PreviewCopyJob", PreviewCopyJob: "PreviewCopyJob",
AssignPermissions: "AssignPermissions", AssignPermissions: "AssignPermissions",
}; };
type Validation = { type Validation = {
validate: (state: CopyJobContextState | Map<string, boolean>) => boolean; validate: (state: CopyJobContextState | Map<string, boolean>) => boolean;
message: string; message: string;
}; };
type Screen = { type Screen = {
key: string; key: string;
component: React.ReactElement; component: React.ReactElement;
validations: Validation[]; validations: Validation[];
}; };
function useCreateCopyJobScreensList() { function useCreateCopyJobScreensList() {
return React.useMemo<Screen[]>( return React.useMemo<Screen[]>(
() => [ () => [
{ {
key: SCREEN_KEYS.SelectAccount, key: SCREEN_KEYS.SelectAccount,
component: <SelectAccount />, component: <SelectAccount />,
validations: [ validations: [
{ {
validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account, validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account,
message: "Please select a subscription and account to proceed", message: "Please select a subscription and account to proceed",
}, },
],
},
{
key: SCREEN_KEYS.SelectSourceAndTargetContainers,
component: <SelectSourceAndTargetContainers />,
validations: [
{
validate: (state: CopyJobContextState) => (
!!state?.source?.databaseId && !!state?.source?.containerId && !!state?.target?.databaseId && !!state?.target?.containerId
),
message: "Please select source and target containers to proceed",
},
],
},
{
key: SCREEN_KEYS.PreviewCopyJob,
component: <PreviewCopyJob />,
validations: [
{
validate: (state: CopyJobContextState) => !!(
typeof state?.jobName === "string"
&& state?.jobName
&& /^[a-zA-Z0-9-.]+$/.test(state?.jobName)
),
message: "Please enter a job name to proceed",
},
],
},
{
key: SCREEN_KEYS.AssignPermissions,
component: <AssignPermissions />,
validations: [
{
validate: (cache: Map<string, boolean>) => {
const cacheValuesIterator = Array.from(cache.values());
if (cacheValuesIterator.length === 0) return false;
const allValid = cacheValuesIterator.every((isValid: boolean) => isValid);
return allValid;
},
message: "Please ensure all previous steps are valid to proceed",
}
],
},
], ],
[] },
); {
key: SCREEN_KEYS.SelectSourceAndTargetContainers,
component: <SelectSourceAndTargetContainers />,
validations: [
{
validate: (state: CopyJobContextState) =>
!!state?.source?.databaseId &&
!!state?.source?.containerId &&
!!state?.target?.databaseId &&
!!state?.target?.containerId,
message: "Please select source and target containers to proceed",
},
],
},
{
key: SCREEN_KEYS.PreviewCopyJob,
component: <PreviewCopyJob />,
validations: [
{
validate: (state: CopyJobContextState) =>
!!(typeof state?.jobName === "string" && state?.jobName && /^[a-zA-Z0-9-.]+$/.test(state?.jobName)),
message: "Please enter a job name to proceed",
},
],
},
{
key: SCREEN_KEYS.AssignPermissions,
component: <AssignPermissions />,
validations: [
{
validate: (cache: Map<string, boolean>) => {
const cacheValuesIterator = Array.from(cache.values());
if (cacheValuesIterator.length === 0) {
return false;
}
const allValid = cacheValuesIterator.every((isValid: boolean) => isValid);
return allValid;
},
message: "Please ensure all previous steps are valid to proceed",
},
],
},
],
[],
);
} }
export { SCREEN_KEYS, useCreateCopyJobScreensList }; export { SCREEN_KEYS, useCreateCopyJobScreensList };
+24 -24
View File
@@ -1,40 +1,40 @@
export enum CopyJobMigrationType { export enum CopyJobMigrationType {
Offline = "offline", Offline = "offline",
Online = "online", Online = "online",
} }
// all checks will happen // all checks will happen
export enum IdentityType { export enum IdentityType {
SystemAssigned = "systemassigned", // "SystemAssigned" SystemAssigned = "systemassigned", // "SystemAssigned"
UserAssigned = "userassigned", // "UserAssigned" UserAssigned = "userassigned", // "UserAssigned"
None = "none", // "None" None = "none", // "None"
} }
export enum DefaultIdentityType { export enum DefaultIdentityType {
SystemAssignedIdentity = "systemassignedidentity", // "SystemAssignedIdentity" SystemAssignedIdentity = "systemassignedidentity", // "SystemAssignedIdentity"
} }
export enum BackupPolicyType { export enum BackupPolicyType {
Continuous = "Continuous", Continuous = "Continuous",
Periodic = "Periodic", Periodic = "Periodic",
} }
export enum CopyJobStatusType { export enum CopyJobStatusType {
Pending = "Pending", Pending = "Pending",
InProgress = "InProgress", InProgress = "InProgress",
Running = "Running", Running = "Running",
Partitioning = "Partitioning", Partitioning = "Partitioning",
Paused = "Paused", Paused = "Paused",
Skipped = "Skipped", Skipped = "Skipped",
Completed = "Completed", Completed = "Completed",
Cancelled = "Cancelled", Cancelled = "Cancelled",
Failed = "Failed", Failed = "Failed",
Faulted = "Faulted", Faulted = "Faulted",
} }
export enum CopyJobActions { export enum CopyJobActions {
pause = "pause", pause = "pause",
resume = "resume", resume = "resume",
cancel = "cancel", cancel = "cancel",
complete = "complete", complete = "complete",
} }
@@ -5,79 +5,72 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
import { CopyJobType } from "../../Types"; import { CopyJobType } from "../../Types";
interface CopyJobActionMenuProps { interface CopyJobActionMenuProps {
job: CopyJobType; job: CopyJobType;
handleClick: (job: CopyJobType, action: string) => void; handleClick: (job: CopyJobType, action: string) => void;
} }
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => { const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
if ([ if ([CopyJobStatusType.Completed, CopyJobStatusType.Cancelled].includes(job.Status)) {
CopyJobStatusType.Completed, return null;
CopyJobStatusType.Cancelled }
].includes(job.Status)) return null;
const getMenuItems = (): IContextualMenuProps["items"] => { const getMenuItems = (): IContextualMenuProps["items"] => {
const baseItems = [ const baseItems = [
{ {
key: CopyJobActions.pause, key: CopyJobActions.pause,
text: ContainerCopyMessages.MonitorJobs.Actions.pause, text: ContainerCopyMessages.MonitorJobs.Actions.pause,
onClick: () => handleClick(job, CopyJobActions.pause) onClick: () => handleClick(job, CopyJobActions.pause),
}, },
{ {
key: CopyJobActions.cancel, key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel, text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
onClick: () => handleClick(job, CopyJobActions.cancel) onClick: () => handleClick(job, CopyJobActions.cancel),
}, },
{ {
key: CopyJobActions.resume, key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume, text: ContainerCopyMessages.MonitorJobs.Actions.resume,
onClick: () => handleClick(job, CopyJobActions.resume) onClick: () => handleClick(job, CopyJobActions.resume),
} },
]; ];
if (CopyJobStatusType.Paused === job.Status) { if (CopyJobStatusType.Paused === job.Status) {
return baseItems.filter(item => item.key !== CopyJobActions.pause); return baseItems.filter((item) => item.key !== CopyJobActions.pause);
} }
if (CopyJobStatusType.Pending === job.Status) { if (CopyJobStatusType.Pending === job.Status) {
return baseItems.filter(item => item.key !== CopyJobActions.resume); return baseItems.filter((item) => item.key !== CopyJobActions.resume);
} }
if ([ if (
CopyJobStatusType.InProgress, [CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status)
CopyJobStatusType.Running, ) {
CopyJobStatusType.Partitioning const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume);
].includes(job.Status)) { if (job.Mode === CopyJobMigrationType.Online) {
const filteredItems = baseItems.filter(item => item.key !== CopyJobActions.resume); filteredItems.push({
if (job.Mode === CopyJobMigrationType.Online) { key: CopyJobActions.complete,
filteredItems.push({ text: ContainerCopyMessages.MonitorJobs.Actions.complete,
key: CopyJobActions.complete, onClick: () => handleClick(job, CopyJobActions.complete),
text: ContainerCopyMessages.MonitorJobs.Actions.complete, });
onClick: () => handleClick(job, CopyJobActions.complete) }
}); return filteredItems;
} }
return filteredItems;
}
if ([ if ([CopyJobStatusType.Failed, CopyJobStatusType.Faulted, CopyJobStatusType.Skipped].includes(job.Status)) {
CopyJobStatusType.Failed, return baseItems.filter((item) => item.key === CopyJobActions.resume);
CopyJobStatusType.Faulted, }
CopyJobStatusType.Skipped,
].includes(job.Status)) {
return baseItems.filter(item => item.key === CopyJobActions.resume);
}
return baseItems; return baseItems;
}; };
return ( return (
<IconButton <IconButton
role="button" role="button"
iconProps={{ iconName: "more" }} iconProps={{ iconName: "more" }}
menuProps={{ items: getMenuItems() }} menuProps={{ items: getMenuItems() }}
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions} ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
title={ContainerCopyMessages.MonitorJobs.Columns.actions} title={ContainerCopyMessages.MonitorJobs.Columns.actions}
/> />
); );
}; };
export default CopyJobActionMenu; export default CopyJobActionMenu;
@@ -6,73 +6,73 @@ import CopyJobActionMenu from "./CopyJobActionMenu";
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon"; import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
export const getColumns = ( export const getColumns = (
handleSort: (columnKey: string) => void, handleSort: (columnKey: string) => void,
handleActionClick: (job: CopyJobType, action: string) => void, handleActionClick: (job: CopyJobType, action: string) => void,
sortedColumnKey: string | undefined, sortedColumnKey: string | undefined,
isSortedDescending: boolean isSortedDescending: boolean,
): IColumn[] => [ ): IColumn[] => [
{ {
key: "LastUpdatedTime", key: "LastUpdatedTime",
name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime, name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime,
fieldName: "LastUpdatedTime", fieldName: "LastUpdatedTime",
minWidth: 100, minWidth: 100,
maxWidth: 150, maxWidth: 150,
isResizable: true, isResizable: true,
isSorted: sortedColumnKey === "timestamp", isSorted: sortedColumnKey === "timestamp",
isSortedDescending: isSortedDescending, isSortedDescending: isSortedDescending,
onColumnClick: () => handleSort("timestamp"), onColumnClick: () => handleSort("timestamp"),
}, },
{ {
key: "Name", key: "Name",
name: ContainerCopyMessages.MonitorJobs.Columns.name, name: ContainerCopyMessages.MonitorJobs.Columns.name,
fieldName: "Name", fieldName: "Name",
minWidth: 90, minWidth: 90,
maxWidth: 130, maxWidth: 130,
isResizable: true, isResizable: true,
isSorted: sortedColumnKey === "Name", isSorted: sortedColumnKey === "Name",
isSortedDescending: isSortedDescending, isSortedDescending: isSortedDescending,
onColumnClick: () => handleSort("Name"), onColumnClick: () => handleSort("Name"),
}, },
{ {
key: "Mode", key: "Mode",
name: ContainerCopyMessages.MonitorJobs.Columns.mode, name: ContainerCopyMessages.MonitorJobs.Columns.mode,
fieldName: "Mode", fieldName: "Mode",
minWidth: 70, minWidth: 70,
maxWidth: 90, maxWidth: 90,
isResizable: true, isResizable: true,
isSorted: sortedColumnKey === "Mode", isSorted: sortedColumnKey === "Mode",
isSortedDescending: isSortedDescending, isSortedDescending: isSortedDescending,
onColumnClick: () => handleSort("Mode"), onColumnClick: () => handleSort("Mode"),
}, },
{ {
key: "CompletionPercentage", key: "CompletionPercentage",
name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage, name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage,
fieldName: "CompletionPercentage", fieldName: "CompletionPercentage",
minWidth: 120, minWidth: 120,
maxWidth: 130, maxWidth: 130,
isResizable: true, isResizable: true,
isSorted: sortedColumnKey === "CompletionPercentage", isSorted: sortedColumnKey === "CompletionPercentage",
isSortedDescending: isSortedDescending, isSortedDescending: isSortedDescending,
onRender: (job: CopyJobType) => `${job.CompletionPercentage}%`, onRender: (job: CopyJobType) => `${job.CompletionPercentage}%`,
onColumnClick: () => handleSort("CompletionPercentage"), onColumnClick: () => handleSort("CompletionPercentage"),
}, },
{ {
key: "CopyJobStatus", key: "CopyJobStatus",
name: ContainerCopyMessages.MonitorJobs.Columns.status, name: ContainerCopyMessages.MonitorJobs.Columns.status,
fieldName: "Status", fieldName: "Status",
minWidth: 80, minWidth: 80,
maxWidth: 100, maxWidth: 100,
isResizable: true, isResizable: true,
isSorted: sortedColumnKey === "Status", isSorted: sortedColumnKey === "Status",
isSortedDescending: isSortedDescending, isSortedDescending: isSortedDescending,
onRender: (job: CopyJobType) => <CopyJobStatusWithIcon status={job.Status} />, onRender: (job: CopyJobType) => <CopyJobStatusWithIcon status={job.Status} />,
onColumnClick: () => handleSort("Status"), onColumnClick: () => handleSort("Status"),
}, },
{ {
key: "Actions", key: "Actions",
name: ContainerCopyMessages.MonitorJobs.Columns.actions, name: ContainerCopyMessages.MonitorJobs.Columns.actions,
minWidth: 200, minWidth: 200,
isResizable: true, isResizable: true,
onRender: (job: CopyJobType) => <CopyJobActionMenu job={job} handleClick={handleActionClick} />, onRender: (job: CopyJobType) => <CopyJobActionMenu job={job} handleClick={handleActionClick} />,
}, },
]; ];
@@ -5,46 +5,49 @@ import { CopyJobStatusType } from "../../Enums";
// Styles // Styles
const iconClass = mergeStyles({ const iconClass = mergeStyles({
fontSize: '1em', fontSize: "1em",
marginRight: '0.3em', marginRight: "0.3em",
}); });
const classNames = mergeStyleSets({ const classNames = mergeStyleSets({
[CopyJobStatusType.Pending]: [{ color: '#fe7f2d' }, iconClass], [CopyJobStatusType.Pending]: [{ color: "#fe7f2d" }, iconClass],
[CopyJobStatusType.InProgress]: [{ color: '#ee9b00' }, iconClass], [CopyJobStatusType.InProgress]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Running]: [{ color: '#ee9b00' }, iconClass], [CopyJobStatusType.Running]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Partitioning]: [{ color: '#ee9b00' }, iconClass], [CopyJobStatusType.Partitioning]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Paused]: [{ color: '#bb3e03' }, iconClass], [CopyJobStatusType.Paused]: [{ color: "#bb3e03" }, iconClass],
[CopyJobStatusType.Skipped]: [{ color: '#00bbf9' }, iconClass], [CopyJobStatusType.Skipped]: [{ color: "#00bbf9" }, iconClass],
[CopyJobStatusType.Cancelled]: [{ color: '#00bbf9' }, iconClass], [CopyJobStatusType.Cancelled]: [{ color: "#00bbf9" }, iconClass],
[CopyJobStatusType.Failed]: [{ color: '#d90429' }, iconClass], [CopyJobStatusType.Failed]: [{ color: "#d90429" }, iconClass],
[CopyJobStatusType.Faulted]: [{ color: '#d90429' }, iconClass], [CopyJobStatusType.Faulted]: [{ color: "#d90429" }, iconClass],
[CopyJobStatusType.Completed]: [{ color: '#386641' }, iconClass], [CopyJobStatusType.Completed]: [{ color: "#386641" }, iconClass],
unknown: [{ color: '#000814' }, iconClass], unknown: [{ color: "#000814" }, iconClass],
}); });
// Icon Mapping // Icon Mapping
const iconMap: Record<CopyJobStatusType, string> = { const iconMap: Record<CopyJobStatusType, string> = {
[CopyJobStatusType.Pending]: "MSNVideosSolid", [CopyJobStatusType.Pending]: "MSNVideosSolid",
[CopyJobStatusType.InProgress]: "SyncStatusSolid", [CopyJobStatusType.InProgress]: "SyncStatusSolid",
[CopyJobStatusType.Running]: "SyncStatusSolid", [CopyJobStatusType.Running]: "SyncStatusSolid",
[CopyJobStatusType.Partitioning]: "SyncStatusSolid", [CopyJobStatusType.Partitioning]: "SyncStatusSolid",
[CopyJobStatusType.Paused]: "CirclePauseSolid", [CopyJobStatusType.Paused]: "CirclePauseSolid",
[CopyJobStatusType.Skipped]: "Blocked2Solid", [CopyJobStatusType.Skipped]: "Blocked2Solid",
[CopyJobStatusType.Cancelled]: "Blocked2Solid", [CopyJobStatusType.Cancelled]: "Blocked2Solid",
[CopyJobStatusType.Failed]: "AlertSolid", [CopyJobStatusType.Failed]: "AlertSolid",
[CopyJobStatusType.Faulted]: "AlertSolid", [CopyJobStatusType.Faulted]: "AlertSolid",
[CopyJobStatusType.Completed]: "CompletedSolid" [CopyJobStatusType.Completed]: "CompletedSolid",
}; };
const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status }) => ( const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status }) => {
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown";
return (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<FontIcon <FontIcon
aria-label={status} aria-label={status}
iconName={iconMap[status] || "UnknownSolid"} iconName={iconMap[status] || "UnknownSolid"}
className={classNames[status] || classNames.unknown} className={classNames[status] || classNames.unknown}
/> />
<Text>{(ContainerCopyMessages.MonitorJobs.Status as any)[status]}</Text> <Text>{statusText}</Text>
</Stack> </Stack>
); );
};
export default CopyJobStatusWithIcon; export default CopyJobStatusWithIcon;
@@ -1,23 +1,22 @@
import { ActionButton, Image } from '@fluentui/react'; import { ActionButton, Image } from "@fluentui/react";
import React, { useCallback } from 'react'; import React, { useCallback } from "react";
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg"; import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
import * as Actions from "../../Actions/CopyJobActions"; import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from '../../ContainerCopyMessages'; import ContainerCopyMessages from "../../ContainerCopyMessages";
interface CopyJobsNotFoundProps { } interface CopyJobsNotFoundProps {}
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = () => { const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = () => {
const handleCreateCopyJob = useCallback(Actions.openCreateCopyJobPanel, []);
const handleCreateCopyJob = useCallback(Actions.openCreateCopyJobPanel, []); return (
return ( <div className="notFoundContainer flexContainer centerContent">
<div className='notFoundContainer flexContainer centerContent'> <Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} />
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} /> <h4 className="noCopyJobsMessage">{ContainerCopyMessages.noCopyJobsTitle}</h4>
<h4 className='noCopyJobsMessage'>{ContainerCopyMessages.noCopyJobsTitle}</h4> <ActionButton allowDisabledFocus className="createCopyJobButton" onClick={handleCreateCopyJob}>
<ActionButton allowDisabledFocus className='createCopyJobButton' onClick={handleCreateCopyJob}> {ContainerCopyMessages.createCopyJobButtonText}
{ContainerCopyMessages.createCopyJobButtonText} </ActionButton>
</ActionButton> </div>
</div> );
); };
}
export default CopyJobsNotFound; export default CopyJobsNotFound;
@@ -1,102 +1,103 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
ConstrainMode, ConstrainMode,
DetailsListLayoutMode, DetailsListLayoutMode,
DetailsRow, DetailsRow,
IColumn, IColumn,
ScrollablePane, ScrollablePane,
ScrollbarVisibility, ScrollbarVisibility,
ShimmeredDetailsList, ShimmeredDetailsList,
Stack, Stack,
Sticky, Sticky,
StickyPositionType StickyPositionType,
} from "@fluentui/react"; } from "@fluentui/react";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { CopyJobType } from "../../Types"; import { CopyJobType } from "../../Types";
import { getColumns } from "./CopyJobColumns"; import { getColumns } from "./CopyJobColumns";
interface CopyJobsListProps { interface CopyJobsListProps {
jobs: CopyJobType[]; jobs: CopyJobType[];
handleActionClick: (job: CopyJobType, action: string) => void, handleActionClick: (job: CopyJobType, action: string) => void;
pageSize?: number pageSize?: number;
} }
const styles = { const styles = {
container: { height: 'calc(100vh - 15em)' } as React.CSSProperties, container: { height: "calc(100vh - 15em)" } as React.CSSProperties,
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties, stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
}; };
const PAGE_SIZE = 100; // Number of items per page const PAGE_SIZE = 100; // Number of items per page
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => { const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const [startIndex, setStartIndex] = React.useState(0); const [startIndex] = React.useState(0);
const [sortedJobs, setSortedJobs] = React.useState(jobs); const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined); const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false); const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
useEffect(() => { useEffect(() => {
setSortedJobs(jobs); setSortedJobs(jobs);
}, [jobs]); }, [jobs]);
const handleSort = (columnKey: string) => { const handleSort = (columnKey: string) => {
const isDescending = sortedColumnKey === columnKey ? !isSortedDescending : false; const isDescending = sortedColumnKey === columnKey ? !isSortedDescending : false;
const sorted = [...sortedJobs].sort((current: any, next: any) => { const sorted = [...sortedJobs].sort((current: any, next: any) => {
if (current[columnKey] < next[columnKey]) return isDescending ? 1 : -1; if (current[columnKey] < next[columnKey]) {
if (current[columnKey] > next[columnKey]) return isDescending ? -1 : 1; return isDescending ? 1 : -1;
return 0; }
}); if (current[columnKey] > next[columnKey]) {
setSortedJobs(sorted); return isDescending ? -1 : 1;
setSortedColumnKey(columnKey); }
setIsSortedDescending(isDescending); return 0;
} });
setSortedJobs(sorted);
setSortedColumnKey(columnKey);
setIsSortedDescending(isDescending);
};
const columns: IColumn[] = React.useMemo( const columns: IColumn[] = React.useMemo(
() => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending), () => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending),
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending] [handleSort, handleActionClick, sortedColumnKey, isSortedDescending],
); );
const _handleRowClick = React.useCallback((job: CopyJobType) => { const _handleRowClick = React.useCallback((job: CopyJobType) => {
console.log("Row clicked:", job); // eslint-disable-next-line no-console
}, []); console.log("Row clicked:", job);
}, []);
const _onRenderRow = React.useCallback((props: any) => {
return (
<div onClick={_handleRowClick.bind(null, props.item)}>
<DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
</div>
);
}, []);
// const totalCount = jobs.length;
const _onRenderRow = React.useCallback((props: any) => {
return ( return (
<div style={styles.container}> <div onClick={_handleRowClick.bind(null, props.item)}>
<Stack verticalFill={true}> <DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
<Stack.Item </div>
verticalFill={true}
grow={1}
shrink={1}
style={styles.stackItem}
>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList
onRenderRow={_onRenderRow}
checkboxVisibility={2}
columns={columns}
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
enableShimmer={false}
constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified}
onRenderDetailsHeader={(props, defaultRender) => (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
{defaultRender({ ...props })}
</Sticky>
)}
/>
</ScrollablePane>
</Stack.Item>
</Stack>
</div>
); );
} }, []);
export default CopyJobsList; // const totalCount = jobs.length;
return (
<div style={styles.container}>
<Stack verticalFill={true}>
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList
onRenderRow={_onRenderRow}
checkboxVisibility={2}
columns={columns}
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
enableShimmer={false}
constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified}
onRenderDetailsHeader={(props, defaultRender) => (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
{defaultRender({ ...props })}
</Sticky>
)}
/>
</ScrollablePane>
</Stack.Item>
</Stack>
</div>
);
};
export default CopyJobsList;
@@ -2,11 +2,11 @@ import create from "zustand";
import { MonitorCopyJobsRef } from "./MonitorCopyJobs"; import { MonitorCopyJobsRef } from "./MonitorCopyJobs";
type MonitorCopyJobsRefStateType = { type MonitorCopyJobsRefStateType = {
ref: MonitorCopyJobsRef; ref: MonitorCopyJobsRef;
setRef: (ref: MonitorCopyJobsRef) => void; setRef: (ref: MonitorCopyJobsRef) => void;
}; };
export const MonitorCopyJobsRefState = create<MonitorCopyJobsRefStateType>((set) => ({ export const MonitorCopyJobsRefState = create<MonitorCopyJobsRefStateType>((set) => ({
ref: null, ref: null,
setRef: (ref) => set({ ref: ref }), setRef: (ref) => set({ ref: ref }),
})); }));
@@ -1,115 +1,118 @@
import { MessageBar, MessageBarType, Stack } from '@fluentui/react'; /* eslint-disable react/display-name */
import ShimmerTree, { IndentLevel } from 'Common/ShimmerTree'; import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; import ShimmerTree, { IndentLevel } from "Common/ShimmerTree";
import { getCopyJobs, updateCopyJobStatus } from '../Actions/CopyJobActions'; import React, { forwardRef, useEffect, useImperativeHandle } from "react";
import { convertToCamelCase } from '../CopyJobUtils'; import { getCopyJobs, updateCopyJobStatus } from "../Actions/CopyJobActions";
import { CopyJobStatusType } from '../Enums'; import { convertToCamelCase } from "../CopyJobUtils";
import CopyJobsNotFound from '../MonitorCopyJobs/Components/CopyJobs.NotFound'; import { CopyJobStatusType } from "../Enums";
import { CopyJobType } from '../Types'; import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
import CopyJobsList from './Components/CopyJobsList'; import { CopyJobType } from "../Types";
import CopyJobsList from "./Components/CopyJobsList";
const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 seconds) const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 seconds)
interface MonitorCopyJobsProps { } interface MonitorCopyJobsProps {}
export interface MonitorCopyJobsRef { export interface MonitorCopyJobsRef {
refreshJobList: () => void; refreshJobList: () => void;
} }
const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_props, ref) => { const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_props, ref) => {
const [loading, setLoading] = React.useState(true); // Start with loading as true const [loading, setLoading] = React.useState(true); // Start with loading as true
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const [jobs, setJobs] = React.useState<CopyJobType[]>([]); const [jobs, setJobs] = React.useState<CopyJobType[]>([]);
const isUpdatingRef = React.useRef(false); // Use ref to track updating state const isUpdatingRef = React.useRef(false); // Use ref to track updating state
const isFirstFetchRef = React.useRef(true); // Use ref to track the first fetch const isFirstFetchRef = React.useRef(true); // Use ref to track the first fetch
const indentLevels = React.useMemo<IndentLevel[]>( const indentLevels = React.useMemo<IndentLevel[]>(() => Array(7).fill({ level: 0, width: "100%" }), []);
() => Array(7).fill({ level: 0, width: "100%" }),
[]
);
const fetchJobs = React.useCallback(async () => { const fetchJobs = React.useCallback(async () => {
if (isUpdatingRef.current) return; // Skip if an update is in progress if (isUpdatingRef.current) {
try { return;
if (isFirstFetchRef.current) setLoading(true); // Show loading spinner only for the first fetch } // Skip if an update is in progress
setError(null); try {
if (isFirstFetchRef.current) {
setLoading(true);
} // Show loading spinner only for the first fetch
setError(null);
const response = await getCopyJobs(); const response = await getCopyJobs();
setJobs((prevJobs) => { setJobs((prevJobs) => {
// Only update jobs if they are different // Only update jobs if they are different
const isSame = JSON.stringify(prevJobs) === JSON.stringify(response); const isSame = JSON.stringify(prevJobs) === JSON.stringify(response);
return isSame ? 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.");
} finally { } finally {
if (isFirstFetchRef.current) { if (isFirstFetchRef.current) {
setLoading(false); // Hide loading spinner after the first fetch setLoading(false); // Hide loading spinner after the first fetch
isFirstFetchRef.current = false; // Mark the first fetch as complete isFirstFetchRef.current = false; // Mark the first fetch as complete
} }
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchJobs(); fetchJobs();
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS); const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [fetchJobs]); }, [fetchJobs]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
refreshJobList: () => { refreshJobList: () => {
if (isUpdatingRef.current) { if (isUpdatingRef.current) {
setError("Please wait for the current update to complete before refreshing."); setError("Please wait for the current update to complete before refreshing.");
return; return;
} }
fetchJobs(); fetchJobs();
} },
})); }));
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => { const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => {
try { try {
isUpdatingRef.current = true; // Mark as updating isUpdatingRef.current = true; // Mark as updating
const updatedCopyJob = await updateCopyJobStatus(job, action); const updatedCopyJob = await updateCopyJobStatus(job, action);
if (updatedCopyJob) { if (updatedCopyJob) {
setJobs((prevJobs) => setJobs((prevJobs) =>
prevJobs.map((prevJob) => prevJobs.map((prevJob) =>
prevJob.Name === updatedCopyJob.properties.jobName prevJob.Name === updatedCopyJob.properties.jobName
? { ? {
...prevJob, ...prevJob,
Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType,
} : prevJob }
) : prevJob,
); ),
} );
} catch (error) { }
setError(error.message || "Failed to update copy job status. Please try again later."); } catch (error) {
} finally { setError(error.message || "Failed to update copy job status. Please try again later.");
isUpdatingRef.current = false; // Mark as not updating } finally {
} isUpdatingRef.current = false; // Mark as not updating
}, []); }
}, []);
const memoizedJobsList = React.useMemo(() => { const memoizedJobsList = React.useMemo(() => {
if (loading) return null; if (loading) {
if (jobs.length > 0) return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />; return null;
return <CopyJobsNotFound />; }
}, [jobs, loading, handleActionClick]); if (jobs.length > 0) {
return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
}
return <CopyJobsNotFound />;
}, [jobs, loading, handleActionClick]);
return ( return (
<Stack className='monitorCopyJobs flexContainer'> <Stack className="monitorCopyJobs flexContainer">
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: '100%', padding: '1rem 2.5rem' }} />} {loading && <ShimmerTree indentLevels={indentLevels} style={{ width: "100%", padding: "1rem 2.5rem" }} />}
{error && ( {error && (
<MessageBar <MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
messageBarType={MessageBarType.error} {error}
isMultiline={false} </MessageBar>
onDismiss={() => setError(null)} )}
> {memoizedJobsList}
{error} </Stack>
</MessageBar> );
)}
{memoizedJobsList}
</Stack>
);
}); });
export default MonitorCopyJobs; export default MonitorCopyJobs;
+90 -94
View File
@@ -5,132 +5,128 @@ import Explorer from "../../Explorer";
import { CopyJobMigrationType, CopyJobStatusType } from "../Enums"; import { CopyJobMigrationType, CopyJobStatusType } from "../Enums";
export interface ContainerCopyProps { export interface ContainerCopyProps {
container: Explorer; container: Explorer;
} }
export type CopyJobCommandBarBtnType = { export type CopyJobCommandBarBtnType = {
key: string; key: string;
iconSrc: string; iconSrc: string;
label: string; label: string;
ariaLabel: string; ariaLabel: string;
disabled?: boolean; disabled?: boolean;
onClick: () => void; onClick: () => void;
}; };
export type CopyJobTabForwardRefHandle = { export type CopyJobTabForwardRefHandle = {
validate: (state: CopyJobContextState) => boolean; validate: (state: CopyJobContextState) => boolean;
}; };
export type DropdownOptionType = { export type DropdownOptionType = {
key: string, key: string;
text: string, text: string;
data: any // eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
}; };
export type DatabaseParams = [ export type DatabaseParams = [string | undefined, string | undefined, string | undefined, ApiType];
string | undefined,
string | undefined,
string | undefined,
ApiType
];
export type DataContainerParams = [ export type DataContainerParams = [
string | undefined, string | undefined,
string | undefined, string | undefined,
string | undefined, string | undefined,
string | undefined, string | undefined,
ApiType ApiType,
]; ];
export interface DatabaseContainerSectionProps { export interface DatabaseContainerSectionProps {
heading: string, heading: string;
databaseOptions: DropdownOptionType[], databaseOptions: DropdownOptionType[];
selectedDatabase: string, selectedDatabase: string;
databaseDisabled?: boolean, databaseDisabled?: boolean;
databaseOnChange: (ev: any, option: DropdownOptionType) => void, databaseOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
containerOptions: DropdownOptionType[], containerOptions: DropdownOptionType[];
selectedContainer: string, selectedContainer: string;
containerDisabled?: boolean, containerDisabled?: boolean;
containerOnChange: (ev: any, option: DropdownOptionType) => void containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
} }
export interface CopyJobContextState { export interface CopyJobContextState {
jobName: string; jobName: string;
migrationType: CopyJobMigrationType; migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: boolean; sourceReadAccessFromTarget?: boolean;
// source details // source details
source: { source: {
subscription: Subscription; subscription: Subscription;
account: DatabaseAccount; account: DatabaseAccount;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
}, };
// target details // target details
target: { target: {
subscriptionId: string; subscriptionId: string;
account: DatabaseAccount; account: DatabaseAccount;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
}, };
} }
export interface CopyJobFlowType { export interface CopyJobFlowType {
currentScreen: string; currentScreen: string;
} }
export interface CopyJobContextProviderType { export interface CopyJobContextProviderType {
flow: CopyJobFlowType; flow: CopyJobFlowType;
setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>; setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>;
copyJobState: CopyJobContextState | null; copyJobState: CopyJobContextState | null;
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>; setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>;
resetCopyJobState: () => void; resetCopyJobState: () => void;
} }
export type CopyJobType = { export type CopyJobType = {
ID: string; ID: string;
Mode: string; Mode: string;
Name: string; Name: string;
Status: CopyJobStatusType; Status: CopyJobStatusType;
CompletionPercentage: number; CompletionPercentage: number;
Duration: string; Duration: string;
LastUpdatedTime: string; LastUpdatedTime: string;
timestamp: number; timestamp: number;
Error?: CopyJobErrorType Error?: CopyJobErrorType;
} };
export interface CopyJobErrorType { export interface CopyJobErrorType {
message: string; message: string;
code: string; code: string;
} }
export interface CopyJobError { export interface CopyJobError {
message: string; message: string;
navigateToStep?: number; navigateToStep?: number;
} }
export type DataTransferJobType = { export type DataTransferJobType = {
id: string; id: string;
type: string; type: string;
properties: { properties: {
jobName: string; jobName: string;
status: string; status: string;
lastUpdatedUtcTime: string; lastUpdatedUtcTime: string;
processedCount: number; processedCount: number;
totalCount: number; totalCount: number;
mode: string; mode: string;
duration: string; duration: string;
source: { source: {
databaseName: string; databaseName: string;
collectionName: string; collectionName: string;
component: string; component: string;
}; };
destination: { destination: {
databaseName: string; databaseName: string;
collectionName: string; collectionName: string;
component: string; component: string;
}; };
error: { error: {
message: string; message: string;
code: string; code: string;
}; };
}; };
}; };
+19 -19
View File
@@ -1,23 +1,23 @@
import { MonitorCopyJobsRefState } from 'Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobRefState'; import { MonitorCopyJobsRefState } from "Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobRefState";
import React, { useEffect } from 'react'; import React, { useEffect } from "react";
import CopyJobCommandBar from './CommandBar/CopyJobCommandBar'; import CopyJobCommandBar from "./CommandBar/CopyJobCommandBar";
import './containerCopyStyles.less'; import "./containerCopyStyles.less";
import MonitorCopyJobs, { MonitorCopyJobsRef } from './MonitorCopyJobs/MonitorCopyJobs'; import MonitorCopyJobs, { MonitorCopyJobsRef } from "./MonitorCopyJobs/MonitorCopyJobs";
import { ContainerCopyProps } from './Types'; import { ContainerCopyProps } from "./Types";
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => { const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => {
const monitorCopyJobsRef = React.useRef<MonitorCopyJobsRef>(); const monitorCopyJobsRef = React.useRef<MonitorCopyJobsRef>();
useEffect(() => { useEffect(() => {
if (monitorCopyJobsRef.current) { if (monitorCopyJobsRef.current) {
MonitorCopyJobsRefState.getState().setRef(monitorCopyJobsRef.current); MonitorCopyJobsRefState.getState().setRef(monitorCopyJobsRef.current);
} }
}, [monitorCopyJobsRef.current]); }, [monitorCopyJobsRef.current]);
return ( return (
<div id="containerCopyWrapper" className="flexContainer hideOverflows"> <div id="containerCopyWrapper" className="flexContainer hideOverflows">
<CopyJobCommandBar container={container} /> <CopyJobCommandBar container={container} />
<MonitorCopyJobs ref={monitorCopyJobsRef} /> <MonitorCopyJobs ref={monitorCopyJobsRef} />
</div> </div>
); );
}; };
export default ContainerCopyPanel; export default ContainerCopyPanel;
@@ -8,7 +8,7 @@ export interface PanelContainerProps {
panelContent?: JSX.Element; panelContent?: JSX.Element;
isConsoleExpanded: boolean; isConsoleExpanded: boolean;
isOpen: boolean; isOpen: boolean;
hasConsole: boolean hasConsole?: boolean;
isConsoleAnimationFinished?: boolean; isConsoleAnimationFinished?: boolean;
panelWidth?: string; panelWidth?: string;
onRenderNavigationContent?: IRenderFunction<IPanelProps>; onRenderNavigationContent?: IRenderFunction<IPanelProps>;
+8 -10
View File
@@ -78,24 +78,22 @@ const App: React.FunctionComponent = () => {
} }
StyleConstants.updateStyles(); StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
console.log("Using config: ", config); // console.log("Using config: ", config);
if (!explorer) { if (!explorer) {
return <LoadingExplorer />; return <LoadingExplorer />;
} }
console.log("Using explorer: ", explorer); // console.log("Using explorer: ", explorer);
console.log("Using userContext: ", userContext); // console.log("Using userContext: ", userContext);
return ( return (
<KeyboardShortcutRoot> <KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot"> <div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
{ {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( <ContainerCopyPanel container={explorer} />
<ContainerCopyPanel container={explorer} /> ) : (
) : ( <DivExplorer explorer={explorer} />
<DivExplorer explorer={explorer} /> )}
)
}
<SidePanel /> <SidePanel />
<Dialog /> <Dialog />
+1
View File
@@ -8,6 +8,7 @@ describe("AuthorizationUtils", () => {
const setAadDataPlane = (enabled: boolean) => { const setAadDataPlane = (enabled: boolean) => {
updateUserContext({ updateUserContext({
features: { features: {
enableContainerCopy: false,
enableAadDataPlane: enabled, enableAadDataPlane: enabled,
canExceedMaximumValue: false, canExceedMaximumValue: false,
cosmosdb: false, cosmosdb: false,
+9 -6
View File
@@ -1,9 +1,12 @@
import { userContext } from "UserContext"; import { userContext } from "UserContext";
export function getCopyJobAuthorizationHeader(token: string = ""): Headers { export function getCopyJobAuthorizationHeader(token: string = ""): Headers {
const headers = new Headers(); if (!token && !userContext.authorizationToken) {
const authToken = token ? `Bearer ${token}` : userContext.authorizationToken; throw new Error("Authorization token is missing");
headers.append("Authorization", authToken); }
headers.append("Content-Type", "application/json"); const headers = new Headers();
return headers; const authToken = token ? `Bearer ${token}` : userContext.authorizationToken ?? "";
} headers.append("Authorization", authToken);
headers.append("Content-Type", "application/json");
return headers;
}
+77 -76
View File
@@ -3,117 +3,118 @@ import { armRequest } from "Utils/arm/request";
import { getCopyJobAuthorizationHeader } from "../CopyJobAuthUtils"; import { getCopyJobAuthorizationHeader } from "../CopyJobAuthUtils";
export type FetchAccountDetailsParams = { export type FetchAccountDetailsParams = {
subscriptionId: string; subscriptionId: string;
resourceGroupName: string; resourceGroupName: string;
accountName: string; accountName: string;
}; };
export type RoleAssignmentPropertiesType = { export type RoleAssignmentPropertiesType = {
roleDefinitionId: string; roleDefinitionId: string;
principalId: string; principalId: string;
scope: string; scope: string;
}; };
export type RoleAssignmentType = { export type RoleAssignmentType = {
id: string; id: string;
name: string; name: string;
properties: RoleAssignmentPropertiesType; properties: RoleAssignmentPropertiesType;
type: string; type: string;
}; };
type RoleDefinitionDataActions = { type RoleDefinitionDataActions = {
dataActions: string[]; dataActions: string[];
}; };
export type RoleDefinitionType = { export type RoleDefinitionType = {
assignableScopes: string[]; assignableScopes: string[];
id: string; id: string;
name: string; name: string;
permissions: RoleDefinitionDataActions[]; permissions: RoleDefinitionDataActions[];
resourceGroup: string; resourceGroup: string;
roleName: string; roleName: string;
type: string; type: string;
typePropertiesType: string; typePropertiesType: string;
}; };
const apiVersion = "2025-04-15"; const apiVersion = "2025-04-15";
const getArmBaseUrl = (): string => { const getArmBaseUrl = (): string => {
const base = configContext.ARM_ENDPOINT; const base = configContext.ARM_ENDPOINT;
return base.endsWith("/") ? base.slice(0, -1) : base; return base.endsWith("/") ? base.slice(0, -1) : base;
}; };
const buildArmUrl = (path: string): string => const buildArmUrl = (path: string): string => `${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
`${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
const handleResponse = async (response: Response, context: string) => { const handleResponse = async (response: Response, context: string) => {
if (!response.ok) { if (!response.ok) {
const body = await response.text().catch(() => ""); const body = await response.text().catch(() => "");
throw new Error( throw new Error(`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}`);
`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}` }
); return response.json();
}
return response.json();
}; };
export const fetchRoleAssignments = async ( export const fetchRoleAssignments = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
principalId: string principalId: string,
): Promise<RoleAssignmentType[]> => { ): Promise<RoleAssignmentType[]> => {
const uri = buildArmUrl( const uri = buildArmUrl(
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlRoleAssignments` `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlRoleAssignments`,
); );
const response = await fetch(uri, { method: "GET", headers: getCopyJobAuthorizationHeader() }); const response = await fetch(uri, { method: "GET", headers: getCopyJobAuthorizationHeader() });
const data = await handleResponse(response, "role assignments"); const data = await handleResponse(response, "role assignments");
return (data.value || []).filter( return (data.value || []).filter(
(assignment: RoleAssignmentType) => (assignment: RoleAssignmentType) => assignment?.properties?.principalId === principalId,
assignment?.properties?.principalId === principalId );
);
}; };
export const fetchRoleDefinitions = async ( export const fetchRoleDefinitions = async (roleAssignments: RoleAssignmentType[]): Promise<RoleDefinitionType[]> => {
roleAssignments: RoleAssignmentType[] const roleDefinitionIds = roleAssignments.map((assignment) => assignment.properties.roleDefinitionId);
): Promise<RoleDefinitionType[]> => { const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
const roleDefinitionIds = roleAssignments.map(assignment => assignment.properties.roleDefinitionId);
const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
const headers = getCopyJobAuthorizationHeader(); const headers = getCopyJobAuthorizationHeader();
const roleDefinitionUris = uniqueRoleDefinitionIds.map((id) => buildArmUrl(id)); const roleDefinitionUris = uniqueRoleDefinitionIds.map((id) => buildArmUrl(id));
const promises = roleDefinitionUris.map((url) => fetch(url, { method: "GET", headers })); const promises = roleDefinitionUris.map((url) => fetch(url, { method: "GET", headers }));
const responses = await Promise.all(promises); const responses = await Promise.all(promises);
const roleDefinitions = await Promise.all( const roleDefinitions = await Promise.all(
responses.map((res, i) => handleResponse(res, `role definition ${uniqueRoleDefinitionIds[i]}`)) responses.map((res, i) => handleResponse(res, `role definition ${uniqueRoleDefinitionIds[i]}`)),
); );
return roleDefinitions; return roleDefinitions;
}; };
export const assignRole = async ( export const assignRole = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
principalId: string principalId: string,
): Promise<RoleAssignmentType> => { ): Promise<RoleAssignmentType | null> => {
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; if (!principalId) {
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`; return null;
const roleAssignmentName = crypto.randomUUID(); }
const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`; const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
const roleAssignmentName = crypto.randomUUID();
const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;
const body = { const body = {
properties: { properties: {
roleDefinitionId, roleDefinitionId,
scope: `${accountScope}/`, scope: `${accountScope}/`,
principalId principalId,
} },
}; };
const response: RoleAssignmentType = await armRequest({ const response: RoleAssignmentType = await armRequest({
host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body host: configContext.ARM_ENDPOINT,
}); path,
return response; method: "PUT",
apiVersion,
body,
});
return response;
}; };
+26 -27
View File
@@ -4,35 +4,34 @@ import { configContext } from "../../ConfigContext";
const apiVersion = "2025-04-15"; const apiVersion = "2025-04-15";
export type FetchAccountDetailsParams = { export type FetchAccountDetailsParams = {
subscriptionId: string; subscriptionId: string;
resourceGroupName: string; resourceGroupName: string;
accountName: string; accountName: string;
}; };
const buildUrl = (params: FetchAccountDetailsParams): string => { const buildUrl = (params: FetchAccountDetailsParams): string => {
const { subscriptionId, resourceGroupName, accountName } = params; const { subscriptionId, resourceGroupName, accountName } = params;
let armEndpoint = configContext.ARM_ENDPOINT; let armEndpoint = configContext.ARM_ENDPOINT;
if (armEndpoint.endsWith("/")) { if (armEndpoint.endsWith("/")) {
armEndpoint = armEndpoint.slice(0, -1); armEndpoint = armEndpoint.slice(0, -1);
} }
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}?api-version=${apiVersion}`; return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}?api-version=${apiVersion}`;
} };
export async function fetchDatabaseAccount( export async function fetchDatabaseAccount(subscriptionId: string, resourceGroupName: string, accountName: string) {
subscriptionId: string, if (!userContext.authorizationToken) {
resourceGroupName: string, return Promise.reject("Authorization token is missing");
accountName: string }
) { const headers = new Headers();
const headers = new Headers(); headers.append("Authorization", userContext.authorizationToken);
headers.append("Authorization", userContext.authorizationToken); headers.append("Content-Type", "application/json");
headers.append("Content-Type", "application/json"); const uri = buildUrl({ subscriptionId, resourceGroupName, accountName });
const uri = buildUrl({ subscriptionId, resourceGroupName, accountName }); const response = await fetch(uri, { method: "GET", headers: headers });
const response = await fetch(uri, { method: "GET", headers: headers });
if (!response.ok) {
if (!response.ok) { throw new Error(`Error fetching database account: ${response.statusText}`);
throw new Error(`Error fetching database account: ${response.statusText}`); }
} const account: DatabaseAccount = await response.json();
const account: DatabaseAccount = await response.json(); return account;
return account;
} }
@@ -83,7 +83,7 @@ export async function listByDatabaseAccount(
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
signal?: AbortSignal signal?: AbortSignal,
): Promise<Types.DataTransferJobFeedResults> { ): Promise<Types.DataTransferJobFeedResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion, signal }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion, signal });
@@ -8,107 +8,106 @@
/* Base class for all DataTransfer source/sink */ /* Base class for all DataTransfer source/sink */
export interface DataTransferDataSourceSink { export interface DataTransferDataSourceSink {
/* undocumented */ /* undocumented */
component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBMongoVCore" | "CosmosDBSql" | "AzureBlobStorage"; component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBMongoVCore" | "CosmosDBSql" | "AzureBlobStorage";
} }
/* A base CosmosDB data source/sink */ /* A base CosmosDB data source/sink */
export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & { export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
remoteAccountName?: string; remoteAccountName?: string;
}; };
/* A CosmosDB Cassandra API data source/sink */ /* A CosmosDB Cassandra API data source/sink */
export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
keyspaceName: string; keyspaceName: string;
/* undocumented */ /* undocumented */
tableName: string; tableName: string;
}; };
/* A CosmosDB Mongo API data source/sink */ /* A CosmosDB Mongo API data source/sink */
export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
databaseName: string; databaseName: string;
/* undocumented */ /* undocumented */
collectionName: string; collectionName: string;
}; };
/* A CosmosDB Mongo vCore API data source/sink */ /* A CosmosDB Mongo vCore API data source/sink */
export type CosmosMongoVCoreDataTransferDataSourceSink = DataTransferDataSourceSink & { export type CosmosMongoVCoreDataTransferDataSourceSink = DataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
databaseName: string; databaseName: string;
/* undocumented */ /* undocumented */
collectionName: string; collectionName: string;
/* undocumented */ /* undocumented */
hostName?: string; hostName?: string;
/* undocumented */ /* undocumented */
connectionStringKeyVaultUri?: string; connectionStringKeyVaultUri?: string;
}; };
/* A CosmosDB No Sql API data source/sink */ /* A CosmosDB No Sql API data source/sink */
export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & { export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
databaseName: string; databaseName: string;
/* undocumented */ /* undocumented */
containerName: string; containerName: string;
}; };
/* An Azure Blob Storage data source/sink */ /* An Azure Blob Storage data source/sink */
export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & { export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & {
/* undocumented */ /* undocumented */
containerName: string; containerName: string;
/* undocumented */ /* undocumented */
endpointUrl?: string; endpointUrl?: string;
}; };
/* The properties of a DataTransfer Job */ /* The properties of a DataTransfer Job */
export interface DataTransferJobProperties { export interface DataTransferJobProperties {
/* Job Name */ /* Job Name */
readonly jobName?: string; readonly jobName?: string;
/* Source DataStore details */ /* Source DataStore details */
source: DataTransferDataSourceSink; source: DataTransferDataSourceSink;
/* Destination DataStore details */ /* Destination DataStore details */
destination: DataTransferDataSourceSink; destination: DataTransferDataSourceSink;
/* Job Status */ /* Job Status */
readonly status?: string; readonly status?: string;
/* Processed Count. */ /* Processed Count. */
readonly processedCount?: number readonly processedCount?: number;
/* Total Count. */ /* Total Count. */
readonly totalCount?: number; readonly totalCount?: number;
/* Last Updated Time (ISO-8601 format). */ /* Last Updated Time (ISO-8601 format). */
readonly lastUpdatedUtcTime?: string; readonly lastUpdatedUtcTime?: string;
/* Worker count */ /* Worker count */
workerCount?: number; workerCount?: number;
/* Error response for Faulted job */ /* Error response for Faulted job */
readonly error?: unknown; readonly error?: unknown;
/* Total Duration of Job */ /* Total Duration of Job */
readonly duration?: string readonly duration?: string;
/* Mode of job execution */ /* Mode of job execution */
mode?: "Offline" | "Online"; mode?: "Offline" | "Online";
} }
/* Parameters to create Data Transfer Job */ /* Parameters to create Data Transfer Job */
export type CreateJobRequest = unknown & { export type CreateJobRequest = unknown & {
/* Data Transfer Create Job Properties */ /* Data Transfer Create Job Properties */
properties: DataTransferJobProperties; properties: DataTransferJobProperties;
}; };
/* A Cosmos DB Data Transfer Job */ /* A Cosmos DB Data Transfer Job */
export type DataTransferJobGetResults = unknown & { export type DataTransferJobGetResults = unknown & {
/* undocumented */ /* undocumented */
properties?: DataTransferJobProperties; properties?: DataTransferJobProperties;
}; };
/* The List operation response, that contains the Data Transfer jobs and their properties. */ /* The List operation response, that contains the Data Transfer jobs and their properties. */
export interface DataTransferJobFeedResults { export interface DataTransferJobFeedResults {
/* List of Data Transfer jobs and their properties. */ /* List of Data Transfer jobs and their properties. */
readonly value?: DataTransferJobGetResults[]; readonly value?: DataTransferJobGetResults[];
/* URL to get the next set of Data Transfer job list results if there are any. */ /* URL to get the next set of Data Transfer job list results if there are any. */
readonly nextLink?: string; readonly nextLink?: string;
} }
+40 -39
View File
@@ -6,51 +6,52 @@ import { armRequest } from "./request";
const apiVersion = "2025-04-15"; const apiVersion = "2025-04-15";
const updateIdentity = async ( const updateIdentity = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
body: object body: object,
): Promise<DatabaseAccount> => { ): Promise<DatabaseAccount | null> => {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const response: { status: string } = await armRequest({ const response: { status: string } = await armRequest({
host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body host: configContext.ARM_ENDPOINT,
}); path,
if (response.status === "Succeeded") { method: "PATCH",
const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName); apiVersion,
return account; body,
} });
return null; if (response.status === "Succeeded") {
const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName);
return account;
}
return null;
}; };
const updateSystemIdentity = async ( const updateSystemIdentity = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string accountName: string,
): Promise<DatabaseAccount> => { ): Promise<DatabaseAccount | null> => {
const body = { const body = {
identity: { identity: {
type: "SystemAssigned" type: "SystemAssigned",
} },
}; };
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body); const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
return updatedAccount; return updatedAccount;
}; };
const updateDefaultIdentity = async ( const updateDefaultIdentity = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string accountName: string,
): Promise<DatabaseAccount> => { ): Promise<DatabaseAccount | null> => {
const body = { const body = {
properties: { properties: {
defaultIdentity: "SystemAssignedIdentity" defaultIdentity: "SystemAssignedIdentity",
} },
}; };
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body); const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
return updatedAccount; return updatedAccount;
}; };
export { updateDefaultIdentity, updateSystemIdentity }; export { updateDefaultIdentity, updateSystemIdentity };
+3 -3
View File
@@ -83,7 +83,7 @@ export async function armRequestWithoutPolling<T>({
method, method,
headers, headers,
body: requestBody ? JSON.stringify(requestBody) : undefined, body: requestBody ? JSON.stringify(requestBody) : undefined,
signal signal,
}); });
if (!response.ok) { if (!response.ok) {
@@ -119,7 +119,7 @@ export async function armRequest<T>({
queryParams, queryParams,
contentType, contentType,
customHeaders, customHeaders,
signal signal,
}: Options): Promise<T> { }: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({ const armRequestResult = await armRequestWithoutPolling<T>({
host, host,
@@ -130,7 +130,7 @@ export async function armRequest<T>({
queryParams, queryParams,
contentType, contentType,
customHeaders, customHeaders,
signal signal,
}); });
const operationStatusUrl = armRequestResult.operationStatusUrl; const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {
+51 -58
View File
@@ -7,76 +7,69 @@ import { getCopyJobAuthorizationHeader } from "../Utils/CopyJobAuthUtils";
const apiVersion = "2023-09-15"; const apiVersion = "2023-09-15";
export interface FetchDataContainersListParams { export interface FetchDataContainersListParams {
subscriptionId: string; subscriptionId: string;
resourceGroupName: string; resourceGroupName: string;
databaseName: string; databaseName: string;
accountName: string; accountName: string;
apiType?: ApiType; apiType?: ApiType;
} }
const buildReadDataContainersListUrl = (params: FetchDataContainersListParams): string => { const buildReadDataContainersListUrl = (params: FetchDataContainersListParams): string => {
const { subscriptionId, resourceGroupName, accountName, databaseName, apiType } = params; const { subscriptionId, resourceGroupName, accountName, databaseName, apiType } = params;
const databaseEndpoint = getDatabaseEndpoint(apiType); const databaseEndpoint = getDatabaseEndpoint(apiType);
const collectionEndpoint = getCollectionEndpoint(apiType); const collectionEndpoint = getCollectionEndpoint(apiType);
let armEndpoint = configContext.ARM_ENDPOINT; let armEndpoint = configContext.ARM_ENDPOINT;
if (armEndpoint.endsWith("/")) { if (armEndpoint.endsWith("/")) {
armEndpoint = armEndpoint.slice(0, -1); armEndpoint = armEndpoint.slice(0, -1);
} }
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}/${databaseName}/${collectionEndpoint}?api-version=${apiVersion}`; return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}/${databaseName}/${collectionEndpoint}?api-version=${apiVersion}`;
} };
const fetchDataContainersList = async ( const fetchDataContainersList = async (
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
databaseName: string, databaseName: string,
apiType: ApiType apiType: ApiType,
): Promise<DatabaseModel[]> => { ): Promise<DatabaseModel[]> => {
const uri = buildReadDataContainersListUrl({ const uri = buildReadDataContainersListUrl({
subscriptionId, subscriptionId,
resourceGroupName, resourceGroupName,
accountName, accountName,
databaseName, databaseName,
apiType apiType,
}); });
const headers = getCopyJobAuthorizationHeader(); const headers = getCopyJobAuthorizationHeader();
const response = await fetch(uri, { const response = await fetch(uri, {
method: "GET", method: "GET",
headers: headers headers: headers,
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch containers"); throw new Error("Failed to fetch containers");
} }
const data = await response.json(); const data = await response.json();
return data.value; return data.value;
}; };
export function useDataContainers( export function useDataContainers(
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
databaseName: string, databaseName: string,
apiType: ApiType apiType: ApiType,
): DatabaseModel[] | undefined { ): DatabaseModel[] | undefined {
const { data } = useSWR( const { data } = useSWR(
() => ( () =>
subscriptionId && resourceGroupName && accountName && databaseName && apiType ? [ subscriptionId && resourceGroupName && accountName && databaseName && apiType
"fetchContainersLinkedToDatabases", ? ["fetchContainersLinkedToDatabases", subscriptionId, resourceGroupName, accountName, databaseName, apiType]
subscriptionId, resourceGroupName, accountName, databaseName, apiType : undefined,
] : undefined (_, subscriptionId, resourceGroupName, accountName, databaseName, apiType) =>
), fetchDataContainersList(subscriptionId, resourceGroupName, accountName, databaseName, apiType),
(_, subscriptionId, resourceGroupName, accountName, databaseName, apiType) => fetchDataContainersList( );
subscriptionId,
resourceGroupName,
accountName,
databaseName,
apiType
),
);
return data; return data;
} }
+12 -9
View File
@@ -11,7 +11,10 @@ interface AccountListResult {
value: DatabaseAccount[]; value: DatabaseAccount[];
} }
export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: string = ""): Promise<DatabaseAccount[]> { export async function fetchDatabaseAccounts(
subscriptionId: string,
accessToken: string = "",
): Promise<DatabaseAccount[]> {
if (!accessToken && !userContext.authorizationToken) { if (!accessToken && !userContext.authorizationToken) {
return []; return [];
} }
@@ -61,15 +64,15 @@ export async function fetchDatabaseAccountsFromGraph(
subscriptions: [subscriptionId], subscriptions: [subscriptionId],
...(skipToken ...(skipToken
? { ? {
options: { options: {
$skipToken: skipToken, $skipToken: skipToken,
} as QueryRequestOptions, } as QueryRequestOptions,
} }
: { : {
options: { options: {
$top: 150, $top: 150,
} as QueryRequestOptions, } as QueryRequestOptions,
}), }),
}; };
const response = await fetch(managementResourceGraphAPIURL, { const response = await fetch(managementResourceGraphAPIURL, {
+43 -44
View File
@@ -7,60 +7,59 @@ import { getCopyJobAuthorizationHeader } from "../Utils/CopyJobAuthUtils";
const apiVersion = "2023-09-15"; const apiVersion = "2023-09-15";
export interface FetchDatabasesListParams { export interface FetchDatabasesListParams {
subscriptionId: string; subscriptionId: string;
resourceGroupName: string; resourceGroupName: string;
accountName: string; accountName: string;
apiType?: ApiType; apiType?: ApiType;
} }
const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string => { const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string => {
const { subscriptionId, resourceGroupName, accountName, apiType } = params; const { subscriptionId, resourceGroupName, accountName, apiType } = params;
const databaseEndpoint = getDatabaseEndpoint(apiType); const databaseEndpoint = getDatabaseEndpoint(apiType);
let armEndpoint = configContext.ARM_ENDPOINT; let armEndpoint = configContext.ARM_ENDPOINT;
if (armEndpoint.endsWith("/")) { if (armEndpoint.endsWith("/")) {
armEndpoint = armEndpoint.slice(0, -1); armEndpoint = armEndpoint.slice(0, -1);
} }
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}?api-version=${apiVersion}`; return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}?api-version=${apiVersion}`;
} };
const fetchDatabasesList = async (subscriptionId: string, resourceGroupName: string, accountName: string, apiType: ApiType): Promise<DatabaseModel[]> => { const fetchDatabasesList = async (
const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, apiType }); subscriptionId: string,
const headers = getCopyJobAuthorizationHeader(); resourceGroupName: string,
accountName: string,
apiType: ApiType,
): Promise<DatabaseModel[]> => {
const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, apiType });
const headers = getCopyJobAuthorizationHeader();
const response = await fetch(uri, { const response = await fetch(uri, {
method: "GET", method: "GET",
headers: headers headers: headers,
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch databases"); throw new Error("Failed to fetch databases");
} }
const data = await response.json(); const data = await response.json();
return data.value; return data.value;
}; };
export function useDatabases( export function useDatabases(
subscriptionId: string, subscriptionId: string,
resourceGroupName: string, resourceGroupName: string,
accountName: string, accountName: string,
apiType: ApiType apiType: ApiType,
): DatabaseModel[] | undefined { ): DatabaseModel[] | undefined {
const { data } = useSWR( const { data } = useSWR(
() => ( () =>
subscriptionId && resourceGroupName && accountName && apiType ? [ subscriptionId && resourceGroupName && accountName && apiType
"fetchDatabasesLinkedToResource", ? ["fetchDatabasesLinkedToResource", subscriptionId, resourceGroupName, accountName, apiType]
subscriptionId, resourceGroupName, accountName, apiType : undefined,
] : undefined (_, subscriptionId, resourceGroupName, accountName, apiType) =>
), fetchDatabasesList(subscriptionId, resourceGroupName, accountName, apiType),
(_, subscriptionId, resourceGroupName, accountName, apiType) => fetchDatabasesList( );
subscriptionId,
resourceGroupName,
accountName,
apiType
),
);
return data; return data;
} }