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
+3 -5
View File
@@ -2,8 +2,8 @@ import { Shimmer, ShimmerElementType, Stack } from "@fluentui/react";
import * as React from "react";
export interface IndentLevel {
level: number,
width?: string
level: number;
width?: string;
}
interface ShimmerTreeProps {
indentLevels: IndentLevel[];
@@ -32,9 +32,7 @@ const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => {
return (
<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>
);
};
+1 -1
View File
@@ -14,7 +14,7 @@ export interface DatabaseAccountUserAssignedIdentity {
[key: string]: {
principalId: string;
clientId: string;
}
};
}
export interface DatabaseAccountIdentity {
@@ -7,9 +7,12 @@ import {
create,
listByDatabaseAccount,
pause,
resume
resume,
} 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 {
convertTime,
@@ -17,7 +20,7 @@ import {
COSMOS_SQL_COMPONENT,
extractErrorMessage,
formatUTCDateTime,
getAccountDetailsFromResourceId
getAccountDetailsFromResourceId,
} from "../CopyJobUtils";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobActions, CopyJobStatusType } from "../Enums";
@@ -25,14 +28,14 @@ import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefSta
import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from "../Types";
export const openCreateCopyJobPanel = () => {
const sidePanelState = useSidePanel.getState()
const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle,
<CreateCopyJobScreensProvider />,
"650px"
"650px",
);
}
};
let copyJobsAbortController: AbortController | null = null;
@@ -43,12 +46,14 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
}
copyJobsAbortController = new AbortController();
try {
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || "");
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
copyJobsAbortController.signal
copyJobsAbortController.signal,
);
const jobs = response.value || [];
@@ -60,8 +65,8 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
/* added a lower bound to "0" and upper bound to "100" */
const calculateCompletionPercentage = (processed: number, total: number): number => {
if (
typeof processed !== 'number' ||
typeof total !== 'number' ||
typeof processed !== "number" ||
typeof total !== "number" ||
!isFinite(processed) ||
!isFinite(total) ||
total <= 0
@@ -74,12 +79,15 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
};
const formattedJobs: CopyJobType[] = jobs
.filter((job: DataTransferJobGetResults) =>
.filter(
(job: DataTransferJobGetResults) =>
job.properties?.source?.component === COSMOS_SQL_COMPONENT &&
job.properties?.destination?.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()
.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);
@@ -102,36 +110,32 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
console.error(`Error fetching copy jobs: ${errorContent}`);
throw error;
}
}
};
export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: () => void) => {
try {
const { source, target, migrationType, jobName } = state;
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || "");
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const body = {
properties: {
source: {
component: "CosmosDBSql",
remoteAccountName: source?.account?.name,
databaseName: source?.databaseId,
containerName: source?.containerId
containerName: source?.containerId,
},
destination: {
component: "CosmosDBSql",
databaseName: target?.databaseId,
containerName: target?.containerId
containerName: target?.containerId,
},
mode: migrationType,
},
mode: migrationType
}
} as unknown as CreateJobRequest;
const response = await create(
subscriptionId,
resourceGroup,
accountName,
jobName,
body,
);
const response = await create(subscriptionId, resourceGroup, accountName, jobName, body);
MonitorCopyJobsRefState.getState().ref?.refreshJobList();
onSuccess();
return response;
@@ -139,11 +143,10 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
console.error("Error submitting create copy job:", error);
throw error;
}
}
};
export const updateCopyJobStatus = async (job: CopyJobType, action: string): Promise<DataTransferJobGetResults> => {
try {
let updateFn = null;
switch (action.toLowerCase()) {
case CopyJobActions.pause:
@@ -161,7 +164,9 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
default:
throw new Error(`Unsupported action: ${action}`);
}
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || "");
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await updateFn?.(subscriptionId, resourceGroup, accountName, job.Name);
return response;
@@ -169,10 +174,13 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
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}'`);
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;
}
}
};
@@ -26,6 +26,6 @@ const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ container }) => {
/>
</div>
);
}
};
export default CopyJobCommandBar;
@@ -10,7 +10,7 @@ import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefSta
import { CopyJobCommandBarBtnType } from "../Types";
function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState(state => state.ref);
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const buttons: CopyJobCommandBarBtnType[] = [
{
key: "createCopyJob",
@@ -52,7 +52,7 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function getCommandBarButtons(_container: Explorer): CommandButtonComponentProps[] {
return getCopyJobBtns().map(btnMapper);
}
@@ -23,7 +23,8 @@ export default {
migrationTypeCheckboxLabel: "Copy container in offline mode",
// Select Source and Target Containers Screen
selectSourceAndTargetContainersDescription: "Please select a source container and a destination container to copy to.",
selectSourceAndTargetContainersDescription:
"Please select a source container and a destination container to copy to.",
sourceContainerSubHeading: "Source container",
targetContainerSubHeading: "Destination container",
databaseDropdownLabel: "Database",
@@ -42,45 +43,59 @@ export default {
// Assign Permissions Screen
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"
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.",
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.",
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}'?` : "",
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.",
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.",
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.",
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.",
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.",
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.",
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: {
@@ -112,6 +127,6 @@ export default {
Faulted: "Failed",
Skipped: "Cancelled",
Cancelled: "Cancelled",
}
}
}
},
},
};
@@ -10,7 +10,7 @@ export const useCopyJobContext = (): CopyJobContextProviderType => {
throw new Error("useCopyJobContext must be used within a CopyJobContextProvider");
}
return context;
}
};
interface CopyJobContextProviderProps {
children: React.ReactNode;
@@ -32,9 +32,9 @@ const getInitialCopyJobState = (): CopyJobContextState => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false
}
}
sourceReadAccessFromTarget: false,
};
};
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
@@ -42,13 +42,13 @@ const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) =>
const resetCopyJobState = () => {
setCopyJobState(getInitialCopyJobState());
}
};
return (
<CopyJobContext.Provider value={{ copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
{props.children}
</CopyJobContext.Provider>
);
}
};
export default CopyJobContextProvider;
+27 -12
View File
@@ -5,7 +5,7 @@ export const buildResourceLink = (resource: DatabaseAccount): string => {
const resourceId = resource.id;
// TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax)
return `https://ms.portal.azure.com/#resource${resourceId}`;
}
};
export const COSMOS_SQL_COMPONENT = "CosmosDBSql";
@@ -25,8 +25,12 @@ export function buildDataTransferJobPath({
action?: string;
}) {
let path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
if (jobName) path += `/${jobName}`;
if (action) path += `/${action}`;
if (jobName) {
path += `/${jobName}`;
}
if (action) {
path += `/${action}`;
}
return path;
}
@@ -44,16 +48,22 @@ export function convertTime(timeStr: string): string | null {
};
const [hours, minutes, seconds] = timeParts;
const formattedTimeParts = [formatPart(hours, "hours"), formatPart(minutes, "minutes"), formatPart(seconds, "seconds")]
const formattedTimeParts = [
formatPart(hours, "hours"),
formatPart(minutes, "minutes"),
formatPart(seconds, "seconds"),
]
.filter(Boolean)
.join(", ");
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);
if (isNaN(date.getTime())) return null;
if (isNaN(date.getTime())) {
return null;
}
return {
formattedDateTime: new Intl.DateTimeFormat("en-US", {
@@ -66,25 +76,30 @@ export function formatUTCDateTime(utcStr: string): { formattedDateTime: string,
}
export function convertToCamelCase(str: string): string {
const formattedStr = str.split(/\s+/).map(
word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join('');
const formattedStr = str
.split(/\s+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("");
return formattedStr;
}
export function extractErrorMessage(error: CopyJobErrorType): CopyJobErrorType {
return {
...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) {
if (!accountId) {
return null;
}
const pattern = new RegExp('/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)', 'i');
const pattern = new RegExp(
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)",
"i",
);
const matches = accountId.match(pattern);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, subscriptionId, resourceGroup, accountName] = matches || [];
return { subscriptionId, resourceGroup, accountName };
}
@@ -33,7 +33,8 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<Toggle
label={
<Text className="toggle-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.toggleLabel}&nbsp;<InfoTooltip content={managedIdentityTooltip} />
{ContainerCopyMessages.addManagedIdentity.toggleLabel}&nbsp;
<InfoTooltip content={managedIdentityTooltip} />
</Text>
}
checked={systemAssigned}
@@ -42,7 +43,8 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
onChange={onToggle}
/>
<Text className="user-assigned-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;<InfoTooltip content={userAssignedTooltip} />
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;
<InfoTooltip content={userAssignedTooltip} />
</Text>
<div style={{ marginTop: 8 }}>
<Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer">
@@ -55,7 +57,6 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity}
>
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
</PopoverMessage>
@@ -1,4 +1,4 @@
import { ITooltipHostStyles, Stack, Toggle } from "@fluentui/react";
import { Stack, Toggle } from "@fluentui/react";
import React, { useCallback } from "react";
import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
@@ -10,7 +10,6 @@ import { PermissionSectionConfig } from "./hooks/usePermissionsSection";
import useToggle from "./hooks/useToggle";
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
type AddManagedIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = () => {
@@ -25,7 +24,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
const {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
setLoading(true);
@@ -33,7 +32,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName,
target?.account?.identity?.principalId!,
target?.account?.identity?.principalId ?? "",
);
if (assignedRole) {
setCopyJobState((prevState) => ({
@@ -46,12 +45,13 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
} finally {
setLoading(false);
}
}, [copyJobState]);
}, [copyJobState, setCopyJobState]);
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description} &nbsp;<InfoTooltip content={TooltipContent} />
{ContainerCopyMessages.readPermissionAssigned.description} &nbsp;
<InfoTooltip content={TooltipContent} />
</div>
<Toggle
checked={readPermissionAssigned}
@@ -18,7 +18,8 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description} &nbsp;<InfoTooltip content={managedIdentityTooltip} />
{ContainerCopyMessages.defaultManagedIdentity.description} &nbsp;
<InfoTooltip content={managedIdentityTooltip} />
</div>
<Toggle
checked={defaultSystemAssigned}
@@ -12,19 +12,15 @@ const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => {
const sourceAccountLink = buildResourceLink(source?.account);
const onlineCopyUrl = `${sourceAccountLink}/Features`;
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);
return (
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.onlineCopyEnabled.description}
</div>
<PrimaryButton
text={ContainerCopyMessages.onlineCopyEnabled.buttonText}
onClick={openWindowAndMonitor}
/>
<div className="toggle-label">{ContainerCopyMessages.onlineCopyEnabled.description}</div>
<PrimaryButton text={ContainerCopyMessages.onlineCopyEnabled.buttonText} onClick={openWindowAndMonitor} />
</Stack>
);
};
@@ -20,19 +20,15 @@ const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
const {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
setLoading(true);
const account = await fetchDatabaseAccount(
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName
);
const account = await fetchDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName);
if (account) {
setCopyJobState((prevState) => ({
...prevState,
source: { ...prevState.source, account: account }
source: { ...prevState.source, account: account },
}));
}
} catch (error) {
@@ -40,14 +36,12 @@ const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
} finally {
setLoading(false);
}
}, [])
}, []);
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.pointInTimeRestore.description}
</div>
<div className="toggle-label">{ContainerCopyMessages.pointInTimeRestore.description}</div>
<PrimaryButton
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
@@ -7,7 +7,7 @@ interface UseManagedIdentityUpdaterParams {
updateIdentityFn: (
subscriptionId: string,
resourceGroup?: string,
accountName?: string
accountName?: string,
) => Promise<DatabaseAccount | undefined>;
}
@@ -17,7 +17,7 @@ interface UseManagedIdentityUpdaterReturn {
}
const useManagedIdentity = (
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"]
updateIdentityFn: UseManagedIdentityUpdaterParams["updateIdentityFn"],
): UseManagedIdentityUpdaterReturn => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const [loading, setLoading] = useState<boolean>(false);
@@ -29,18 +29,14 @@ const useManagedIdentity = (
const {
subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup,
accountName: targetAccountName
accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const updatedAccount = await updateIdentityFn(
targetSubscriptionId,
targetResourceGroup,
targetAccountName
);
const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName);
if (updatedAccount) {
setCopyJobState((prevState) => ({
...prevState,
target: { ...prevState.target, account: updatedAccount }
target: { ...prevState.target, account: updatedAccount },
}));
}
} catch (error) {
@@ -1,17 +1,8 @@
import { useEffect, useMemo, useRef, useState } from "react";
import {
fetchRoleAssignments,
fetchRoleDefinitions,
RoleDefinitionType
} from "../../../../../../Utils/arm/RbacUtils";
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId } from "../../../../CopyJobUtils";
import {
BackupPolicyType,
CopyJobMigrationType,
DefaultIdentityType,
IdentityType
} from "../../../../Enums";
import { BackupPolicyType, CopyJobMigrationType, DefaultIdentityType, IdentityType } from "../../../../Enums";
import { CopyJobContextState } from "../../../../Types";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity";
@@ -23,7 +14,7 @@ import PointInTimeRestore from "../PointInTimeRestore";
export interface PermissionSectionConfig {
id: string;
title: string;
Component: React.ComponentType
Component: React.ComponentType;
disabled: boolean;
completed?: boolean;
validate?: (state: CopyJobContextState) => boolean | Promise<boolean>;
@@ -35,7 +26,7 @@ export const SECTION_IDS = {
defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned",
pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled"
onlineCopyEnabled: "onlineCopyEnabled",
} as const;
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
@@ -50,7 +41,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned
);
}
},
},
{
id: SECTION_IDS.defaultManagedIdentity,
@@ -60,7 +51,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
validate: (state: CopyJobContextState) => {
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
}
},
},
{
id: SECTION_IDS.readPermissionAssigned,
@@ -73,22 +64,20 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
const {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
const rolesAssigned = await fetchRoleAssignments(
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName,
principalId
principalId,
);
const roleDefinitions = await fetchRoleDefinitions(
rolesAssigned ?? []
);
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
}
}
},
},
];
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
@@ -100,32 +89,32 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
validate: (state: CopyJobContextState) => {
const sourceAccountBackupPolicy = state?.source?.account?.properties?.backupPolicy?.type ?? "";
return sourceAccountBackupPolicy === BackupPolicyType.Continuous;
}
},
},
{
id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title,
Component: OnlineCopyEnabled,
disabled: true,
// 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.
*/
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some(
role =>
(role) =>
role.name === "00000000-0000-0000-0000-000000000001" ||
role.permissions.some(
permission =>
(permission) =>
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"),
),
);
}
@@ -156,7 +145,9 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
useEffect(() => {
const validateSections = async () => {
if (isValidatingRef.current) return;
if (isValidatingRef.current) {
return;
}
isValidatingRef.current = true;
const result: PermissionSectionConfig[] = [];
@@ -182,8 +173,7 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
}
break;
}
}
else {
} else {
// Section has no validate method
newValidationCache.set(section.id, false);
result.push({ ...section, completed: false });
@@ -193,13 +183,13 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
setValidationCache(newValidationCache);
setPermissionSections(result);
isValidatingRef.current = false;
}
};
validateSections();
return () => {
isValidatingRef.current = false;
}
};
}, [state, sectionToValidate]);
return permissionSections ?? [];
@@ -4,12 +4,11 @@ const useWindowOpenMonitor = (url: string, onClose?: () => void, intervalMs = 50
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const openWindowAndMonitor = () => {
const newWindow = window.open(url, '_blank');
const newWindow = window.open(url, "_blank");
intervalRef.current = setInterval(() => {
if (newWindow?.closed) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
console.log('New window has been closed!');
if (onClose) {
onClose();
}
@@ -1,10 +1,5 @@
import { Image, Stack, Text } from "@fluentui/react";
import {
Accordion,
AccordionHeader,
AccordionItem,
AccordionPanel
} from "@fluentui/react-components";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import React, { useEffect } from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg";
@@ -14,16 +9,12 @@ import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({
id,
title,
Component,
completed,
disabled
}) => (
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
<AccordionItem key={id} value={id} disabled={disabled}>
<AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">{title}</Text>
<Text className="accordionHeaderText" variant="medium">
{title}
</Text>
<Image
className="statusIcon"
src={completed ? CheckmarkIcon : WarningIcon}
@@ -45,11 +36,11 @@ const AssignPermissions = () => {
const indentLevels = React.useMemo<IndentLevel[]>(
() => Array(copyJobState.migrationType === CopyJobMigrationType.Online ? 5 : 3).fill({ level: 0, width: "100%" }),
[]
[],
);
useEffect(() => {
const firstIncompleteSection = permissionSections.find(section => !section.completed);
const firstIncompleteSection = permissionSections.find((section) => !section.completed);
const nextOpenItems = firstIncompleteSection ? [firstIncompleteSection.id] : [];
if (JSON.stringify(openItems) !== JSON.stringify(nextOpenItems)) {
setOpenItems(nextOpenItems);
@@ -58,26 +49,16 @@ const AssignPermissions = () => {
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span>
{ContainerCopyMessages.assignPermissions.description}
</span>
{
permissionSections?.length === 0 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: '100%' }} />
<span>{ContainerCopyMessages.assignPermissions.description}</span>
{permissionSections?.length === 0 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: "100%" }} />
) : (
<Accordion
className="permissionsAccordion"
collapsible
openItems={openItems}
>
{
permissionSections.map(section => (
<Accordion className="permissionsAccordion" collapsible openItems={openItems}>
{permissionSections.map((section) => (
<PermissionSection key={section.id} {...section} />
))
}
))}
</Accordion>
)
}
)}
</Stack>
);
};
@@ -10,13 +10,11 @@ interface FieldRowProps {
const FieldRow: React.FC<FieldRowProps> = ({ label = "", children, labelClassName = "" }) => {
return (
<Stack horizontal horizontalAlign="space-between" className="flex-row">
{
label && (
{label && (
<Stack.Item align="center" className="flex-fixed-width">
<label className={`field-label ${labelClassName}`}>{label}: </label>
</Stack.Item>
)
}
)}
<Stack.Item align="center" className="flex-grow-col">
{children}
</Stack.Item>
@@ -3,8 +3,10 @@ import React from "react";
import InfoIcon from "../../../../../../images/Info.svg";
const InfoTooltip: React.FC<{ content?: string }> = ({ content }) => {
if (!content) return null;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
if (!content) {
return null;
}
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
return (
<TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}>
<Image src={InfoIcon} alt="Information" width={14} height={14} />
@@ -1,3 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react";
@@ -9,10 +11,17 @@ interface PopoverContainerProps {
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 (
<Stack className={`popover-container foreground ${isLoading ? "loading" : ""}`} tokens={{ childrenGap: 20 }} style={{ maxWidth: 450 }}>
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>{title}</Text>
<Stack
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }}
>
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
{title}
</Text>
<Text>{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton
@@ -25,7 +34,8 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ isLoadin
</Stack>
</Stack>
);
});
},
);
interface PopoverMessageProps {
isLoading?: boolean;
@@ -36,8 +46,17 @@ interface PopoverMessageProps {
children: React.ReactNode;
}
const PopoverMessage: React.FC<PopoverMessageProps> = ({ isLoading = false, visible, title, onCancel, onPrimary, children }) => {
if (!visible) return null;
const PopoverMessage: React.FC<PopoverMessageProps> = ({
isLoading = false,
visible,
title,
onCancel,
onPrimary,
children,
}) => {
if (!visible) {
return null;
}
return (
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}>
{children}
@@ -11,7 +11,7 @@ const CreateCopyJobScreens: React.FC = () => {
handlePrimary,
handlePrevious,
handleCancel,
primaryBtnText
primaryBtnText,
} = useCopyJobNavigation();
return (
@@ -6,38 +6,38 @@ const commonProps = {
maxWidth: 140,
styles: {
root: {
whiteSpace: 'normal',
lineHeight: '1.2',
wordBreak: 'break-word'
}
}
whiteSpace: "normal",
lineHeight: "1.2",
wordBreak: "break-word",
},
},
};
export const getPreviewCopyJobDetailsListColumns = function (): IColumn[] {
export const getPreviewCopyJobDetailsListColumns = (): IColumn[] => {
return [
{
key: 'sourcedbname',
key: "sourcedbname",
name: ContainerCopyMessages.sourceDatabaseLabel,
fieldName: 'sourceDatabaseName',
...commonProps
fieldName: "sourceDatabaseName",
...commonProps,
},
{
key: 'sourcecolname',
key: "sourcecolname",
name: ContainerCopyMessages.sourceContainerLabel,
fieldName: 'sourceContainerName',
...commonProps
fieldName: "sourceContainerName",
...commonProps,
},
{
key: 'targetdbname',
key: "targetdbname",
name: ContainerCopyMessages.targetDatabaseLabel,
fieldName: 'targetDatabaseName',
...commonProps
fieldName: "targetDatabaseName",
...commonProps,
},
{
key: 'targetcolname',
key: "targetcolname",
name: ContainerCopyMessages.targetContainerLabel,
fieldName: 'targetContainerName',
...commonProps
}
fieldName: "targetContainerName",
...commonProps,
},
];
};
@@ -1,41 +1,40 @@
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from '@fluentui/react';
import FieldRow from 'Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow';
import React from 'react';
import ContainerCopyMessages from '../../../ContainerCopyMessages';
import { useCopyJobContext } from '../../../Context/CopyJobContext';
import { getPreviewCopyJobDetailsListColumns } from './Utils/PreviewCopyJobUtils';
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react";
import FieldRow from "Explorer/ContainerCopy/CreateCopyJob/Screens/Components/FieldRow";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getPreviewCopyJobDetailsListColumns } from "./Utils/PreviewCopyJobUtils";
const PreviewCopyJob: React.FC = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedDatabaseAndContainers = [{
const selectedDatabaseAndContainers = [
{
sourceDatabaseName: copyJobState.source?.databaseId,
sourceContainerName: copyJobState.source?.containerId,
targetDatabaseName: copyJobState.target?.databaseId,
targetContainerName: copyJobState.target?.containerId,
}];
},
];
const jobName = copyJobState.jobName;
const onJobNameChange = (_ev?: React.FormEvent, newValue?: string) => {
setCopyJobState((prevState) => ({
...prevState,
jobName: newValue || '',
jobName: newValue || "",
}));
};
return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField
value={jobName}
onChange={onJobNameChange}
/>
<TextField value={jobName} onChange={onJobNameChange} />
</FieldRow>
<Stack>
<Text className='bold'>{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text>
</Stack>
<Stack>
<Text className='bold'>{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{copyJobState.source?.account?.name}</Text>
</Stack>
<Stack>
@@ -1,3 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -24,5 +26,5 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo(
onChange={onChange}
/>
</FieldRow>
)
),
);
@@ -1,3 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Checkbox, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -7,14 +9,8 @@ interface MigrationTypeCheckboxProps {
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
}
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(
({ checked, onChange }) => (
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Checkbox
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
checked={checked}
onChange={onChange}
/>
<Checkbox 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 React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -22,5 +24,5 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
onChange={onChange}
/>
</FieldRow>
)
),
);
@@ -5,10 +5,10 @@ import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } f
export function useDropdownOptions(
subscriptions: Subscription[],
accounts: DatabaseAccount[]
accounts: DatabaseAccount[],
): {
subscriptionOptions: DropdownOptionType[],
accountOptions: DropdownOptionType[]
subscriptionOptions: DropdownOptionType[];
accountOptions: DropdownOptionType[];
} {
const subscriptionOptions = React.useMemo(
() =>
@@ -17,7 +17,7 @@ export function useDropdownOptions(
text: sub.displayName,
data: sub,
})) || [],
[subscriptions]
[subscriptions],
);
const accountOptions = React.useMemo(
@@ -27,7 +27,7 @@ export function useDropdownOptions(
text: account.name,
data: account,
})) || [],
[accounts]
[accounts],
);
return { subscriptionOptions, accountOptions };
@@ -37,7 +37,7 @@ type setCopyJobStateType = CopyJobContextProviderType["setCopyJobState"];
export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
const handleSelectSourceAccount = React.useCallback(
(type: "subscription" | "account", data: Subscription & DatabaseAccount | undefined) => {
(type: "subscription" | "account", data: (Subscription & DatabaseAccount) | undefined) => {
setCopyJobState((prevState: CopyJobContextState) => {
if (type === "subscription") {
return {
@@ -61,7 +61,7 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
return prevState;
});
},
[setCopyJobState]
[setCopyJobState],
);
const handleMigrationTypeChange = React.useCallback(
@@ -71,7 +71,7 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
}));
},
[setCopyJobState]
[setCopyJobState],
);
return { handleSelectSourceAccount, handleMigrationTypeChange };
@@ -1,3 +1,4 @@
/* eslint-disable react/display-name */
import { Stack } from "@fluentui/react";
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../Contracts/DataModels";
@@ -11,16 +12,15 @@ import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
import { useDropdownOptions, useEventHandlers } from "./Utils/selectAccountUtils";
interface SelectAccountProps { }
const SelectAccount = React.memo(
(_props: SelectAccountProps) => {
const SelectAccount = React.memo(() => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const subscriptions: Subscription[] = useSubscriptions();
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter(account => account.type === "SQL" || account.kind === "GlobalDocumentDB");
const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter(
(account) => account.type === "SQL" || account.kind === "GlobalDocumentDB",
);
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts);
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
@@ -44,13 +44,9 @@ const SelectAccount = React.memo(
onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/>
<MigrationTypeCheckbox
checked={migrationTypeChecked}
onChange={handleMigrationTypeChange}
/>
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
</Stack>
);
}
);
});
export default SelectAccount;
@@ -1,36 +1,35 @@
import React from "react";
import { CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function dropDownChangeHandler(
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>
) {
export function dropDownChangeHandler(setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>) {
return (type: "sourceDatabase" | "sourceContainer" | "targetDatabase" | "targetContainer") =>
(_evnt: any, option: DropdownOptionType) => {
(_evnt: React.FormEvent, option: DropdownOptionType) => {
const value = option.key;
setCopyJobState((prevState) => {
switch (type) {
case "sourceDatabase":
return {
...prevState,
source: { ...prevState.source, databaseId: value, containerId: undefined }
source: { ...prevState.source, databaseId: value, containerId: undefined },
};
case "sourceContainer":
return {
...prevState,
source: { ...prevState.source, containerId: value }
source: { ...prevState.source, containerId: value },
};
case "targetDatabase":
return {
...prevState,
target: { ...prevState.target, databaseId: value, containerId: undefined }
target: { ...prevState.target, databaseId: value, containerId: undefined },
};
case "targetContainer":
return {
...prevState,
target: { ...prevState.target, containerId: value }
target: { ...prevState.target, containerId: value },
};
default:
return prevState;
}
});
}
};
}
@@ -13,7 +13,7 @@ export const DatabaseContainerSection = ({
containerOptions,
selectedContainer,
containerDisabled,
containerOnChange
containerOnChange,
}: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label>
@@ -1,4 +1,5 @@
import { Stack } from "@fluentui/react";
import { DatabaseModel } from "Contracts/DataModels";
import React from "react";
import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers";
@@ -8,18 +9,10 @@ import { DatabaseContainerSection } from "./components/DatabaseContainerSection"
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
import { useMemoizedSourceAndTargetData } from "./memoizedData";
interface SelectSourceAndTargetContainersProps { }
const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainersProps) => {
const SelectSourceAndTargetContainers = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const {
source,
target,
sourceDbParams,
sourceContainerParams,
targetDbParams,
targetContainerParams
} = useMemoizedSourceAndTargetData(copyJobState);
const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
useMemoizedSourceAndTargetData(copyJobState);
// Custom hooks
const sourceDatabases = useDatabases(...sourceDbParams) || [];
@@ -29,20 +22,20 @@ const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainers
// Memoize option objects for dropdowns
const sourceDatabaseOptions = React.useMemo(
() => sourceDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })),
[sourceDatabases]
() => sourceDatabases.map((db: DatabaseModel) => ({ 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]
() => sourceContainers.map((c: DatabaseModel) => ({ 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]
() => targetDatabases.map((db: DatabaseModel) => ({ 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]
() => targetContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
[targetContainers],
);
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
@@ -76,5 +69,4 @@ const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainers
);
};
export default SelectSourceAndTargetContainers;
@@ -9,56 +9,34 @@ export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState
const {
subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup,
accountName: sourceAccountName
accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
const {
subscriptionId: targetSubscriptionId,
resourceGroup: targetResourceGroup,
accountName: targetAccountName
accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
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(
() =>
[
sourceSubscriptionId,
sourceResourceGroup,
sourceAccountName,
source?.databaseId,
'SQL',
] as DataContainerParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId]
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId, "SQL"] as DataContainerParams,
[sourceSubscriptionId, sourceResourceGroup, sourceAccountName, source?.databaseId],
);
const targetDbParams = React.useMemo(
() => [
targetSubscriptionId,
targetResourceGroup,
targetAccountName,
'SQL',
] as DatabaseParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName]
() => [targetSubscriptionId, targetResourceGroup, targetAccountName, "SQL"] as DatabaseParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName],
);
const targetContainerParams = React.useMemo(
() => [
targetSubscriptionId,
targetResourceGroup,
targetAccountName,
target?.databaseId,
'SQL',
] as DataContainerParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId]
() =>
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId, "SQL"] as DataContainerParams,
[targetSubscriptionId, targetResourceGroup, targetAccountName, target?.databaseId],
);
return { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams };
@@ -9,10 +9,7 @@ type NavigationState = {
screenHistory: string[];
};
type Action =
| { type: "NEXT"; nextScreen: string }
| { type: "PREVIOUS" }
| { type: "RESET" };
type Action = { type: "NEXT"; nextScreen: string } | { type: "PREVIOUS" } | { type: "RESET" };
function navigationReducer(state: NavigationState, action: Action): NavigationState {
switch (action.type) {
@@ -42,13 +39,10 @@ export function useCopyJobNavigation() {
const currentScreenKey = state.screenHistory[state.screenHistory.length - 1];
const currentScreen = screens.find((screen) => screen.key === currentScreenKey);
const isPrimaryDisabled = useMemo(
() => {
const isPrimaryDisabled = useMemo(() => {
const context = currentScreenKey === SCREEN_KEYS.AssignPermissions ? cache : copyJobState;
return !currentScreen?.validations.every((v) => v.validate(context));
},
[currentScreen.key, copyJobState, cache]
);
}, [currentScreen.key, copyJobState, cache]);
const primaryBtnText = useMemo(() => {
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
return "Copy";
@@ -7,5 +7,5 @@ interface CopyJobPrerequisitesCacheState {
export const useCopyJobPrerequisitesCache = create<CopyJobPrerequisitesCacheState>((set) => ({
validationCache: new Map<string, boolean>(),
setValidationCache: (cache) => set({ validationCache: cache })
setValidationCache: (cache) => set({ validationCache: cache }),
}));
@@ -41,9 +41,11 @@ function useCreateCopyJobScreensList() {
component: <SelectSourceAndTargetContainers />,
validations: [
{
validate: (state: CopyJobContextState) => (
!!state?.source?.databaseId && !!state?.source?.containerId && !!state?.target?.databaseId && !!state?.target?.containerId
),
validate: (state: CopyJobContextState) =>
!!state?.source?.databaseId &&
!!state?.source?.containerId &&
!!state?.target?.databaseId &&
!!state?.target?.containerId,
message: "Please select source and target containers to proceed",
},
],
@@ -53,11 +55,8 @@ function useCreateCopyJobScreensList() {
component: <PreviewCopyJob />,
validations: [
{
validate: (state: CopyJobContextState) => !!(
typeof state?.jobName === "string"
&& state?.jobName
&& /^[a-zA-Z0-9-.]+$/.test(state?.jobName)
),
validate: (state: CopyJobContextState) =>
!!(typeof state?.jobName === "string" && state?.jobName && /^[a-zA-Z0-9-.]+$/.test(state?.jobName)),
message: "Please enter a job name to proceed",
},
],
@@ -69,19 +68,20 @@ function useCreateCopyJobScreensList() {
{
validate: (cache: Map<string, boolean>) => {
const cacheValuesIterator = Array.from(cache.values());
if (cacheValuesIterator.length === 0) return false;
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 };
@@ -10,60 +10,53 @@ interface CopyJobActionMenuProps {
}
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
if ([
CopyJobStatusType.Completed,
CopyJobStatusType.Cancelled
].includes(job.Status)) return null;
if ([CopyJobStatusType.Completed, CopyJobStatusType.Cancelled].includes(job.Status)) {
return null;
}
const getMenuItems = (): IContextualMenuProps["items"] => {
const baseItems = [
{
key: CopyJobActions.pause,
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
onClick: () => handleClick(job, CopyJobActions.pause)
onClick: () => handleClick(job, CopyJobActions.pause),
},
{
key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
onClick: () => handleClick(job, CopyJobActions.cancel)
onClick: () => handleClick(job, CopyJobActions.cancel),
},
{
key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
onClick: () => handleClick(job, CopyJobActions.resume)
}
onClick: () => handleClick(job, CopyJobActions.resume),
},
];
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) {
return baseItems.filter(item => item.key !== CopyJobActions.resume);
return baseItems.filter((item) => item.key !== CopyJobActions.resume);
}
if ([
CopyJobStatusType.InProgress,
CopyJobStatusType.Running,
CopyJobStatusType.Partitioning
].includes(job.Status)) {
const filteredItems = baseItems.filter(item => item.key !== CopyJobActions.resume);
if (
[CopyJobStatusType.InProgress, CopyJobStatusType.Running, CopyJobStatusType.Partitioning].includes(job.Status)
) {
const filteredItems = baseItems.filter((item) => item.key !== CopyJobActions.resume);
if (job.Mode === CopyJobMigrationType.Online) {
filteredItems.push({
key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
onClick: () => handleClick(job, CopyJobActions.complete)
onClick: () => handleClick(job, CopyJobActions.complete),
});
}
return filteredItems;
}
if ([
CopyJobStatusType.Failed,
CopyJobStatusType.Faulted,
CopyJobStatusType.Skipped,
].includes(job.Status)) {
return baseItems.filter(item => item.key === CopyJobActions.resume);
if ([CopyJobStatusType.Failed, CopyJobStatusType.Faulted, CopyJobStatusType.Skipped].includes(job.Status)) {
return baseItems.filter((item) => item.key === CopyJobActions.resume);
}
return baseItems;
@@ -9,7 +9,7 @@ export const getColumns = (
handleSort: (columnKey: string) => void,
handleActionClick: (job: CopyJobType, action: string) => void,
sortedColumnKey: string | undefined,
isSortedDescending: boolean
isSortedDescending: boolean,
): IColumn[] => [
{
key: "LastUpdatedTime",
@@ -5,21 +5,21 @@ import { CopyJobStatusType } from "../../Enums";
// Styles
const iconClass = mergeStyles({
fontSize: '1em',
marginRight: '0.3em',
fontSize: "1em",
marginRight: "0.3em",
});
const classNames = mergeStyleSets({
[CopyJobStatusType.Pending]: [{ color: '#fe7f2d' }, iconClass],
[CopyJobStatusType.InProgress]: [{ color: '#ee9b00' }, iconClass],
[CopyJobStatusType.Running]: [{ color: '#ee9b00' }, iconClass],
[CopyJobStatusType.Partitioning]: [{ color: '#ee9b00' }, iconClass],
[CopyJobStatusType.Paused]: [{ color: '#bb3e03' }, iconClass],
[CopyJobStatusType.Skipped]: [{ color: '#00bbf9' }, iconClass],
[CopyJobStatusType.Cancelled]: [{ color: '#00bbf9' }, iconClass],
[CopyJobStatusType.Failed]: [{ color: '#d90429' }, iconClass],
[CopyJobStatusType.Faulted]: [{ color: '#d90429' }, iconClass],
[CopyJobStatusType.Completed]: [{ color: '#386641' }, iconClass],
unknown: [{ color: '#000814' }, iconClass],
[CopyJobStatusType.Pending]: [{ color: "#fe7f2d" }, iconClass],
[CopyJobStatusType.InProgress]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Running]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Partitioning]: [{ color: "#ee9b00" }, iconClass],
[CopyJobStatusType.Paused]: [{ color: "#bb3e03" }, iconClass],
[CopyJobStatusType.Skipped]: [{ color: "#00bbf9" }, iconClass],
[CopyJobStatusType.Cancelled]: [{ color: "#00bbf9" }, iconClass],
[CopyJobStatusType.Failed]: [{ color: "#d90429" }, iconClass],
[CopyJobStatusType.Faulted]: [{ color: "#d90429" }, iconClass],
[CopyJobStatusType.Completed]: [{ color: "#386641" }, iconClass],
unknown: [{ color: "#000814" }, iconClass],
});
// Icon Mapping
@@ -33,18 +33,21 @@ const iconMap: Record<CopyJobStatusType, string> = {
[CopyJobStatusType.Cancelled]: "Blocked2Solid",
[CopyJobStatusType.Failed]: "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">
<FontIcon
aria-label={status}
iconName={iconMap[status] || "UnknownSolid"}
className={classNames[status] || classNames.unknown}
/>
<Text>{(ContainerCopyMessages.MonitorJobs.Status as any)[status]}</Text>
<Text>{statusText}</Text>
</Stack>
);
};
export default CopyJobStatusWithIcon;
@@ -1,23 +1,22 @@
import { ActionButton, Image } from '@fluentui/react';
import React, { useCallback } from 'react';
import { ActionButton, Image } from "@fluentui/react";
import React, { useCallback } from "react";
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from '../../ContainerCopyMessages';
import ContainerCopyMessages from "../../ContainerCopyMessages";
interface CopyJobsNotFoundProps {}
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = () => {
const handleCreateCopyJob = useCallback(Actions.openCreateCopyJobPanel, []);
return (
<div className='notFoundContainer flexContainer centerContent'>
<div className="notFoundContainer flexContainer centerContent">
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} />
<h4 className='noCopyJobsMessage'>{ContainerCopyMessages.noCopyJobsTitle}</h4>
<ActionButton allowDisabledFocus className='createCopyJobButton' onClick={handleCreateCopyJob}>
<h4 className="noCopyJobsMessage">{ContainerCopyMessages.noCopyJobsTitle}</h4>
<ActionButton allowDisabledFocus className="createCopyJobButton" onClick={handleCreateCopyJob}>
{ContainerCopyMessages.createCopyJobButtonText}
</ActionButton>
</div>
);
}
};
export default CopyJobsNotFound;
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
ConstrainMode,
DetailsListLayoutMode,
@@ -8,7 +9,7 @@ import {
ShimmeredDetailsList,
Stack,
Sticky,
StickyPositionType
StickyPositionType,
} from "@fluentui/react";
import React, { useEffect } from "react";
import { CopyJobType } from "../../Types";
@@ -16,20 +17,20 @@ import { getColumns } from "./CopyJobColumns";
interface CopyJobsListProps {
jobs: CopyJobType[];
handleActionClick: (job: CopyJobType, action: string) => void,
pageSize?: number
handleActionClick: (job: CopyJobType, action: string) => void;
pageSize?: number;
}
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,
};
const PAGE_SIZE = 100; // Number of items per page
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const [startIndex, setStartIndex] = React.useState(0);
const [sortedJobs, setSortedJobs] = React.useState(jobs);
const [startIndex] = React.useState(0);
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
@@ -40,21 +41,26 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
const handleSort = (columnKey: string) => {
const isDescending = sortedColumnKey === columnKey ? !isSortedDescending : false;
const sorted = [...sortedJobs].sort((current: any, next: any) => {
if (current[columnKey] < next[columnKey]) return isDescending ? 1 : -1;
if (current[columnKey] > next[columnKey]) return isDescending ? -1 : 1;
if (current[columnKey] < next[columnKey]) {
return isDescending ? 1 : -1;
}
if (current[columnKey] > next[columnKey]) {
return isDescending ? -1 : 1;
}
return 0;
});
setSortedJobs(sorted);
setSortedColumnKey(columnKey);
setIsSortedDescending(isDescending);
}
};
const columns: IColumn[] = React.useMemo(
() => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending),
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending]
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending],
);
const _handleRowClick = React.useCallback((job: CopyJobType) => {
// eslint-disable-next-line no-console
console.log("Row clicked:", job);
}, []);
@@ -71,12 +77,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
return (
<div style={styles.container}>
<Stack verticalFill={true}>
<Stack.Item
verticalFill={true}
grow={1}
shrink={1}
style={styles.stackItem}
>
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList
onRenderRow={_onRenderRow}
@@ -97,6 +98,6 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
</Stack>
</div>
);
}
};
export default CopyJobsList;
@@ -1,12 +1,13 @@
import { MessageBar, MessageBarType, Stack } from '@fluentui/react';
import ShimmerTree, { IndentLevel } from 'Common/ShimmerTree';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import { getCopyJobs, updateCopyJobStatus } from '../Actions/CopyJobActions';
import { convertToCamelCase } from '../CopyJobUtils';
import { CopyJobStatusType } from '../Enums';
import CopyJobsNotFound from '../MonitorCopyJobs/Components/CopyJobs.NotFound';
import { CopyJobType } from '../Types';
import CopyJobsList from './Components/CopyJobsList';
/* eslint-disable react/display-name */
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import ShimmerTree, { IndentLevel } from "Common/ShimmerTree";
import React, { forwardRef, useEffect, useImperativeHandle } from "react";
import { getCopyJobs, updateCopyJobStatus } from "../Actions/CopyJobActions";
import { convertToCamelCase } from "../CopyJobUtils";
import { CopyJobStatusType } from "../Enums";
import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
import { CopyJobType } from "../Types";
import CopyJobsList from "./Components/CopyJobsList";
const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 seconds)
@@ -23,15 +24,16 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
const isUpdatingRef = React.useRef(false); // Use ref to track updating state
const isFirstFetchRef = React.useRef(true); // Use ref to track the first fetch
const indentLevels = React.useMemo<IndentLevel[]>(
() => Array(7).fill({ level: 0, width: "100%" }),
[]
);
const indentLevels = React.useMemo<IndentLevel[]>(() => Array(7).fill({ level: 0, width: "100%" }), []);
const fetchJobs = React.useCallback(async () => {
if (isUpdatingRef.current) return; // Skip if an update is in progress
if (isUpdatingRef.current) {
return;
} // Skip if an update is in progress
try {
if (isFirstFetchRef.current) setLoading(true); // Show loading spinner only for the first fetch
if (isFirstFetchRef.current) {
setLoading(true);
} // Show loading spinner only for the first fetch
setError(null);
const response = await getCopyJobs();
@@ -64,7 +66,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
return;
}
fetchJobs();
}
},
}));
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => {
@@ -77,9 +79,10 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
prevJob.Name === updatedCopyJob.properties.jobName
? {
...prevJob,
Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType
} : prevJob
)
Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType,
}
: prevJob,
),
);
}
} catch (error) {
@@ -90,20 +93,20 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
}, []);
const memoizedJobsList = React.useMemo(() => {
if (loading) return null;
if (jobs.length > 0) return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
if (loading) {
return null;
}
if (jobs.length > 0) {
return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
}
return <CopyJobsNotFound />;
}, [jobs, loading, handleActionClick]);
return (
<Stack className='monitorCopyJobs flexContainer'>
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: '100%', padding: '1rem 2.5rem' }} />}
<Stack className="monitorCopyJobs flexContainer">
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: "100%", padding: "1rem 2.5rem" }} />}
{error && (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={() => setError(null)}
>
<MessageBar messageBarType={MessageBarType.error} isMultiline={false} onDismiss={() => setError(null)}>
{error}
</MessageBar>
)}
+19 -23
View File
@@ -22,35 +22,31 @@ export type CopyJobTabForwardRefHandle = {
};
export type DropdownOptionType = {
key: string,
text: string,
data: any
key: string;
text: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
};
export type DatabaseParams = [
string | undefined,
string | undefined,
string | undefined,
ApiType
];
export type DatabaseParams = [string | undefined, string | undefined, string | undefined, ApiType];
export type DataContainerParams = [
string | undefined,
string | undefined,
string | undefined,
string | undefined,
ApiType
ApiType,
];
export interface DatabaseContainerSectionProps {
heading: string,
databaseOptions: DropdownOptionType[],
selectedDatabase: string,
databaseDisabled?: boolean,
databaseOnChange: (ev: any, option: DropdownOptionType) => void,
containerOptions: DropdownOptionType[],
selectedContainer: string,
containerDisabled?: boolean,
containerOnChange: (ev: any, option: DropdownOptionType) => void
heading: string;
databaseOptions: DropdownOptionType[];
selectedDatabase: string;
databaseDisabled?: boolean;
databaseOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
containerOptions: DropdownOptionType[];
selectedContainer: string;
containerDisabled?: boolean;
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
}
export interface CopyJobContextState {
@@ -63,14 +59,14 @@ export interface CopyJobContextState {
account: DatabaseAccount;
databaseId: string;
containerId: string;
},
};
// target details
target: {
subscriptionId: string;
account: DatabaseAccount;
databaseId: string;
containerId: string;
},
};
}
export interface CopyJobFlowType {
@@ -94,8 +90,8 @@ export type CopyJobType = {
Duration: string;
LastUpdatedTime: string;
timestamp: number;
Error?: CopyJobErrorType
}
Error?: CopyJobErrorType;
};
export interface CopyJobErrorType {
message: string;
+6 -6
View File
@@ -1,9 +1,9 @@
import { MonitorCopyJobsRefState } from 'Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobRefState';
import React, { useEffect } from 'react';
import CopyJobCommandBar from './CommandBar/CopyJobCommandBar';
import './containerCopyStyles.less';
import MonitorCopyJobs, { MonitorCopyJobsRef } from './MonitorCopyJobs/MonitorCopyJobs';
import { ContainerCopyProps } from './Types';
import { MonitorCopyJobsRefState } from "Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobRefState";
import React, { useEffect } from "react";
import CopyJobCommandBar from "./CommandBar/CopyJobCommandBar";
import "./containerCopyStyles.less";
import MonitorCopyJobs, { MonitorCopyJobsRef } from "./MonitorCopyJobs/MonitorCopyJobs";
import { ContainerCopyProps } from "./Types";
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => {
const monitorCopyJobsRef = React.useRef<MonitorCopyJobsRef>();
@@ -8,7 +8,7 @@ export interface PanelContainerProps {
panelContent?: JSX.Element;
isConsoleExpanded: boolean;
isOpen: boolean;
hasConsole: boolean
hasConsole?: boolean;
isConsoleAnimationFinished?: boolean;
panelWidth?: string;
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
+5 -7
View File
@@ -78,24 +78,22 @@ const App: React.FunctionComponent = () => {
}
StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform);
console.log("Using config: ", config);
// console.log("Using config: ", config);
if (!explorer) {
return <LoadingExplorer />;
}
console.log("Using explorer: ", explorer);
console.log("Using userContext: ", userContext);
// console.log("Using explorer: ", explorer);
// console.log("Using userContext: ", userContext);
return (
<KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot">
{
userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<ContainerCopyPanel container={explorer} />
) : (
<DivExplorer explorer={explorer} />
)
}
)}
<SidePanel />
<Dialog />
+1
View File
@@ -8,6 +8,7 @@ describe("AuthorizationUtils", () => {
const setAadDataPlane = (enabled: boolean) => {
updateUserContext({
features: {
enableContainerCopy: false,
enableAadDataPlane: enabled,
canExceedMaximumValue: false,
cosmosdb: false,
+4 -1
View File
@@ -1,8 +1,11 @@
import { userContext } from "UserContext";
export function getCopyJobAuthorizationHeader(token: string = ""): Headers {
if (!token && !userContext.authorizationToken) {
throw new Error("Authorization token is missing");
}
const headers = new Headers();
const authToken = token ? `Bearer ${token}` : userContext.authorizationToken;
const authToken = token ? `Bearer ${token}` : userContext.authorizationToken ?? "";
headers.append("Authorization", authToken);
headers.append("Content-Type", "application/json");
return headers;
+20 -19
View File
@@ -43,15 +43,12 @@ const getArmBaseUrl = (): string => {
return base.endsWith("/") ? base.slice(0, -1) : base;
};
const buildArmUrl = (path: string): string =>
`${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
const buildArmUrl = (path: string): string => `${getArmBaseUrl()}${path}?api-version=${apiVersion}`;
const handleResponse = async (response: Response, context: string) => {
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(
`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}`
);
throw new Error(`Failed to fetch ${context}. Status: ${response.status}. ${body || ""}`);
}
return response.json();
};
@@ -60,25 +57,22 @@ export const fetchRoleAssignments = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
principalId: string
principalId: string,
): Promise<RoleAssignmentType[]> => {
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 data = await handleResponse(response, "role assignments");
return (data.value || []).filter(
(assignment: RoleAssignmentType) =>
assignment?.properties?.principalId === principalId
(assignment: RoleAssignmentType) => assignment?.properties?.principalId === principalId,
);
};
export const fetchRoleDefinitions = async (
roleAssignments: RoleAssignmentType[]
): Promise<RoleDefinitionType[]> => {
const roleDefinitionIds = roleAssignments.map(assignment => assignment.properties.roleDefinitionId);
export const fetchRoleDefinitions = async (roleAssignments: RoleAssignmentType[]): Promise<RoleDefinitionType[]> => {
const roleDefinitionIds = roleAssignments.map((assignment) => assignment.properties.roleDefinitionId);
const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
const headers = getCopyJobAuthorizationHeader();
@@ -88,7 +82,7 @@ export const fetchRoleDefinitions = async (
const responses = await Promise.all(promises);
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;
@@ -98,8 +92,11 @@ export const assignRole = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
principalId: string
): Promise<RoleAssignmentType> => {
principalId: string,
): Promise<RoleAssignmentType | null> => {
if (!principalId) {
return null;
}
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
const roleAssignmentName = crypto.randomUUID();
@@ -109,11 +106,15 @@ export const assignRole = async (
properties: {
roleDefinitionId,
scope: `${accountScope}/`,
principalId
}
principalId,
},
};
const response: RoleAssignmentType = await armRequest({
host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body
host: configContext.ARM_ENDPOINT,
path,
method: "PUT",
apiVersion,
body,
});
return response;
};
+5 -6
View File
@@ -17,13 +17,12 @@ const buildUrl = (params: FetchAccountDetailsParams): string => {
armEndpoint = armEndpoint.slice(0, -1);
}
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}?api-version=${apiVersion}`;
}
};
export async function fetchDatabaseAccount(
subscriptionId: string,
resourceGroupName: string,
accountName: string
) {
export async function fetchDatabaseAccount(subscriptionId: string, resourceGroupName: string, accountName: string) {
if (!userContext.authorizationToken) {
return Promise.reject("Authorization token is missing");
}
const headers = new Headers();
headers.append("Authorization", userContext.authorizationToken);
headers.append("Content-Type", "application/json");
@@ -83,7 +83,7 @@ export async function listByDatabaseAccount(
subscriptionId: string,
resourceGroupName: string,
accountName: string,
signal?: AbortSignal
signal?: AbortSignal,
): Promise<Types.DataTransferJobFeedResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion, signal });
@@ -46,7 +46,6 @@ export type CosmosMongoVCoreDataTransferDataSourceSink = DataTransferDataSourceS
connectionStringKeyVaultUri?: string;
};
/* A CosmosDB No Sql API data source/sink */
export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
/* undocumented */
@@ -76,7 +75,7 @@ export interface DataTransferJobProperties {
/* Job Status */
readonly status?: string;
/* Processed Count. */
readonly processedCount?: number
readonly processedCount?: number;
/* Total Count. */
readonly totalCount?: number;
/* Last Updated Time (ISO-8601 format). */
@@ -87,7 +86,7 @@ export interface DataTransferJobProperties {
readonly error?: unknown;
/* Total Duration of Job */
readonly duration?: string
readonly duration?: string;
/* Mode of job execution */
mode?: "Offline" | "Online";
}
+15 -14
View File
@@ -9,11 +9,15 @@ const updateIdentity = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
body: object
): Promise<DatabaseAccount> => {
body: object,
): Promise<DatabaseAccount | null> => {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const response: { status: string } = await armRequest({
host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body
host: configContext.ARM_ENDPOINT,
path,
method: "PATCH",
apiVersion,
body,
});
if (response.status === "Succeeded") {
const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName);
@@ -25,12 +29,12 @@ const updateIdentity = async (
const updateSystemIdentity = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string
): Promise<DatabaseAccount> => {
accountName: string,
): Promise<DatabaseAccount | null> => {
const body = {
identity: {
type: "SystemAssigned"
}
type: "SystemAssigned",
},
};
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
return updatedAccount;
@@ -39,18 +43,15 @@ const updateSystemIdentity = async (
const updateDefaultIdentity = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string
): Promise<DatabaseAccount> => {
accountName: string,
): Promise<DatabaseAccount | null> => {
const body = {
properties: {
defaultIdentity: "SystemAssignedIdentity"
}
defaultIdentity: "SystemAssignedIdentity",
},
};
const updatedAccount = await updateIdentity(subscriptionId, resourceGroupName, accountName, body);
return updatedAccount;
};
export { updateDefaultIdentity, updateSystemIdentity };
+3 -3
View File
@@ -83,7 +83,7 @@ export async function armRequestWithoutPolling<T>({
method,
headers,
body: requestBody ? JSON.stringify(requestBody) : undefined,
signal
signal,
});
if (!response.ok) {
@@ -119,7 +119,7 @@ export async function armRequest<T>({
queryParams,
contentType,
customHeaders,
signal
signal,
}: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({
host,
@@ -130,7 +130,7 @@ export async function armRequest<T>({
queryParams,
contentType,
customHeaders,
signal
signal,
});
const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) {
+11 -18
View File
@@ -24,27 +24,27 @@ const buildReadDataContainersListUrl = (params: FetchDataContainersListParams):
armEndpoint = armEndpoint.slice(0, -1);
}
return `${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/${databaseEndpoint}/${databaseName}/${collectionEndpoint}?api-version=${apiVersion}`;
}
};
const fetchDataContainersList = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
databaseName: string,
apiType: ApiType
apiType: ApiType,
): Promise<DatabaseModel[]> => {
const uri = buildReadDataContainersListUrl({
subscriptionId,
resourceGroupName,
accountName,
databaseName,
apiType
apiType,
});
const headers = getCopyJobAuthorizationHeader();
const response = await fetch(uri, {
method: "GET",
headers: headers
headers: headers,
});
if (!response.ok) {
@@ -60,22 +60,15 @@ export function useDataContainers(
resourceGroupName: string,
accountName: string,
databaseName: string,
apiType: ApiType
apiType: ApiType,
): DatabaseModel[] | undefined {
const { data } = useSWR(
() => (
subscriptionId && resourceGroupName && accountName && databaseName && apiType ? [
"fetchContainersLinkedToDatabases",
subscriptionId, resourceGroupName, accountName, databaseName, apiType
] : undefined
),
(_, subscriptionId, resourceGroupName, accountName, databaseName, apiType) => fetchDataContainersList(
subscriptionId,
resourceGroupName,
accountName,
databaseName,
apiType
),
() =>
subscriptionId && resourceGroupName && accountName && databaseName && apiType
? ["fetchContainersLinkedToDatabases", subscriptionId, resourceGroupName, accountName, databaseName, apiType]
: undefined,
(_, subscriptionId, resourceGroupName, accountName, databaseName, apiType) =>
fetchDataContainersList(subscriptionId, resourceGroupName, accountName, databaseName, apiType),
);
return data;
+4 -1
View File
@@ -11,7 +11,10 @@ interface AccountListResult {
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) {
return [];
}
+15 -16
View File
@@ -22,15 +22,20 @@ const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string =>
armEndpoint = armEndpoint.slice(0, -1);
}
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 (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
apiType: ApiType,
): Promise<DatabaseModel[]> => {
const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, apiType });
const headers = getCopyJobAuthorizationHeader();
const response = await fetch(uri, {
method: "GET",
headers: headers
headers: headers,
});
if (!response.ok) {
@@ -45,21 +50,15 @@ export function useDatabases(
subscriptionId: string,
resourceGroupName: string,
accountName: string,
apiType: ApiType
apiType: ApiType,
): DatabaseModel[] | undefined {
const { data } = useSWR(
() => (
subscriptionId && resourceGroupName && accountName && apiType ? [
"fetchDatabasesLinkedToResource",
subscriptionId, resourceGroupName, accountName, apiType
] : undefined
),
(_, subscriptionId, resourceGroupName, accountName, apiType) => fetchDatabasesList(
subscriptionId,
resourceGroupName,
accountName,
apiType
),
() =>
subscriptionId && resourceGroupName && accountName && apiType
? ["fetchDatabasesLinkedToResource", subscriptionId, resourceGroupName, accountName, apiType]
: undefined,
(_, subscriptionId, resourceGroupName, accountName, apiType) =>
fetchDatabasesList(subscriptionId, resourceGroupName, accountName, apiType),
);
return data;