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"; 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[];
@@ -32,9 +32,7 @@ const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => {
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>
); );
}; };
+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;
@@ -7,9 +7,12 @@ import {
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,
@@ -17,7 +20,7 @@ import {
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,14 +28,14 @@ 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;
@@ -43,12 +46,14 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
} }
copyJobsAbortController = new AbortController(); copyJobsAbortController = new AbortController();
try { try {
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(userContext.databaseAccount?.id || ""); const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await listByDatabaseAccount( const response = await listByDatabaseAccount(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
copyJobsAbortController.signal copyJobsAbortController.signal,
); );
const jobs = response.value || []; 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" */ /* added a lower bound to "0" and upper bound to "100" */
const calculateCompletionPercentage = (processed: number, total: number): number => { const calculateCompletionPercentage = (processed: number, total: number): number => {
if ( if (
typeof processed !== 'number' || typeof processed !== "number" ||
typeof total !== 'number' || typeof total !== "number" ||
!isFinite(processed) || !isFinite(processed) ||
!isFinite(total) || !isFinite(total) ||
total <= 0 total <= 0
@@ -74,12 +79,15 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
}; };
const formattedJobs: CopyJobType[] = jobs const formattedJobs: CopyJobType[] = jobs
.filter((job: DataTransferJobGetResults) => .filter(
(job: DataTransferJobGetResults) =>
job.properties?.source?.component === COSMOS_SQL_COMPONENT && 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) => .sort(
new Date(next.properties.lastUpdatedUtcTime).getTime() - new Date(current.properties.lastUpdatedUtcTime).getTime() (current: DataTransferJobGetResults, next: DataTransferJobGetResults) =>
new Date(next.properties.lastUpdatedUtcTime).getTime() -
new Date(current.properties.lastUpdatedUtcTime).getTime(),
) )
.map((job: DataTransferJobGetResults, index: number) => { .map((job: DataTransferJobGetResults, index: number) => {
const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime); const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime);
@@ -102,36 +110,32 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
console.error(`Error fetching copy jobs: ${errorContent}`); console.error(`Error fetching copy jobs: ${errorContent}`);
throw error; 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(
userContext.databaseAccount?.id || "",
);
const body = { const body = {
properties: { properties: {
source: { source: {
component: "CosmosDBSql", component: "CosmosDBSql",
remoteAccountName: source?.account?.name, remoteAccountName: source?.account?.name,
databaseName: source?.databaseId, databaseName: source?.databaseId,
containerName: source?.containerId containerName: source?.containerId,
}, },
destination: { destination: {
component: "CosmosDBSql", component: "CosmosDBSql",
databaseName: target?.databaseId, databaseName: target?.databaseId,
containerName: target?.containerId containerName: target?.containerId,
},
mode: migrationType,
}, },
mode: migrationType
}
} as unknown as CreateJobRequest; } as unknown as CreateJobRequest;
const response = await create( const response = await create(subscriptionId, resourceGroup, accountName, jobName, body);
subscriptionId,
resourceGroup,
accountName,
jobName,
body,
);
MonitorCopyJobsRefState.getState().ref?.refreshJobList(); MonitorCopyJobsRefState.getState().ref?.refreshJobList();
onSuccess(); onSuccess();
return response; return response;
@@ -139,11 +143,10 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
console.error("Error submitting create copy job:", error); console.error("Error submitting create copy job:", error);
throw 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:
@@ -161,7 +164,9 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
default: default:
throw new Error(`Unsupported action: ${action}`); 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); const response = await updateFn?.(subscriptionId, resourceGroup, accountName, job.Name);
return response; 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 errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning]; const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
const pattern = new RegExp(`'(${statusList.join('|')})'`, 'g'); const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
const normalizedErrorMessage = errorMessage.replace(pattern, `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`); const normalizedErrorMessage = errorMessage.replace(
pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
);
console.error(`Error updating copy job status: ${normalizedErrorMessage}`); console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
throw error; throw error;
} }
} };
@@ -26,6 +26,6 @@ const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ container }) => {
/> />
</div> </div>
); );
} };
export default CopyJobCommandBar; export default CopyJobCommandBar;
@@ -10,7 +10,7 @@ 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",
@@ -33,7 +33,7 @@ function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel, label: ContainerCopyMessages.feedbackButtonLabel,
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel, ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel,
onClick: () => { }, onClick: () => {},
}); });
} }
return buttons; return buttons;
@@ -52,7 +52,7 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
}; };
} }
// 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);
} }
@@ -23,7 +23,8 @@ export default {
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:
"Please select a source container and a destination container to copy to.",
sourceContainerSubHeading: "Source container", sourceContainerSubHeading: "Source container",
targetContainerSubHeading: "Destination container", targetContainerSubHeading: "Destination container",
databaseDropdownLabel: "Database", databaseDropdownLabel: "Database",
@@ -42,45 +43,59 @@ export default {
// 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: { toggleBtn: {
onText: "On", onText: "On",
offText: "Off" offText: "Off",
}, },
addManagedIdentity: { addManagedIdentity: {
title: "System assigned managed identity enabled", 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", 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.", userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.", userAssignedIdentityLabel: "You may also select a user assigned managed identity.",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity", createUserAssignedIdentityLink: "Create User Assigned Managed Identity",
enablementTitle: "Enable system 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: { defaultManagedIdentity: {
title: "System assigned managed identity enabled as default", 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.", description:
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.", "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", 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: { readPermissionAssigned: {
title: "Read permission assigned to default identity", 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.", description:
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.", "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", 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: { pointInTimeRestore: {
title: "Point In Time Restore enabled", 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", buttonText: "Enable Point In Time Restore",
}, },
onlineCopyEnabled: { onlineCopyEnabled: {
title: "Online copy enabled", 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", buttonText: "Enable Online Copy",
}, },
MonitorJobs: { MonitorJobs: {
@@ -112,6 +127,6 @@ export default {
Faulted: "Failed", Faulted: "Failed",
Skipped: "Cancelled", Skipped: "Cancelled",
Cancelled: "Cancelled", Cancelled: "Cancelled",
} },
} },
} };
@@ -10,7 +10,7 @@ export const useCopyJobContext = (): CopyJobContextProviderType => {
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;
@@ -32,9 +32,9 @@ const getInitialCopyJobState = (): CopyJobContextState => {
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());
@@ -42,13 +42,13 @@ const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) =>
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;
+27 -12
View File
@@ -5,7 +5,7 @@ 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";
@@ -25,8 +25,12 @@ export function buildDataTransferJobPath({
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}`;
}
if (action) {
path += `/${action}`;
}
return path; return path;
} }
@@ -44,16 +48,22 @@ export function convertTime(timeStr: string): string | null {
}; };
const [hours, minutes, seconds] = timeParts; 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) .filter(Boolean)
.join(", "); .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", {
@@ -66,25 +76,30 @@ export function formatUTCDateTime(utcStr: string): { formattedDateTime: string,
} }
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())
.join("");
return formattedStr; 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(
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB/databaseAccounts/([^/]+)",
"i",
);
const matches = accountId.match(pattern); const matches = accountId.match(pattern);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, subscriptionId, resourceGroup, accountName] = matches || []; const [_, subscriptionId, resourceGroup, accountName] = matches || [];
return { subscriptionId, resourceGroup, accountName }; return { subscriptionId, resourceGroup, accountName };
} }
@@ -33,7 +33,8 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<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;
<InfoTooltip content={managedIdentityTooltip} />
</Text> </Text>
} }
checked={systemAssigned} checked={systemAssigned}
@@ -42,7 +43,8 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
onChange={onToggle} onChange={onToggle}
/> />
<Text className="user-assigned-label" style={textStyle}> <Text className="user-assigned-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;<InfoTooltip content={userAssignedTooltip} /> {ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;
<InfoTooltip content={userAssignedTooltip} />
</Text> </Text>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer"> <Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer">
@@ -55,7 +57,6 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
title={ContainerCopyMessages.addManagedIdentity.enablementTitle} title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity} onPrimary={handleAddSystemIdentity}
> >
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)} {ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
</PopoverMessage> </PopoverMessage>
@@ -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,7 +10,6 @@ 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> = () => {
@@ -25,7 +24,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
accountName: sourceAccountName accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
setLoading(true); setLoading(true);
@@ -33,7 +32,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
sourceSubscriptionId, sourceSubscriptionId,
sourceResourceGroup, sourceResourceGroup,
sourceAccountName, sourceAccountName,
target?.account?.identity?.principalId!, target?.account?.identity?.principalId ?? "",
); );
if (assignedRole) { if (assignedRole) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
@@ -46,12 +45,13 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddManagedIdentityProps> = ()
} 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;
<InfoTooltip content={TooltipContent} />
</div> </div>
<Toggle <Toggle
checked={readPermissionAssigned} checked={readPermissionAssigned}
@@ -18,7 +18,8 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
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;
<InfoTooltip content={managedIdentityTooltip} />
</div> </div>
<Toggle <Toggle
checked={defaultSystemAssigned} checked={defaultSystemAssigned}
@@ -12,19 +12,15 @@ const OnlineCopyEnabled: React.FC<AddManagedIdentityProps> = () => {
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>
<PrimaryButton
text={ContainerCopyMessages.onlineCopyEnabled.buttonText}
onClick={openWindowAndMonitor}
/>
</Stack> </Stack>
); );
}; };
@@ -20,19 +20,15 @@ const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
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,
sourceResourceGroup,
sourceAccountName
);
if (account) { if (account) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
source: { ...prevState.source, account: account } source: { ...prevState.source, account: account },
})); }));
} }
} catch (error) { } catch (error) {
@@ -40,14 +36,12 @@ const PointInTimeRestore: React.FC<AddManagedIdentityProps> = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []) }, []);
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed); 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}
</div>
<PrimaryButton <PrimaryButton
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText} text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
@@ -7,7 +7,7 @@ interface UseManagedIdentityUpdaterParams {
updateIdentityFn: ( updateIdentityFn: (
subscriptionId: string, subscriptionId: string,
resourceGroup?: string, resourceGroup?: string,
accountName?: string accountName?: string,
) => Promise<DatabaseAccount | undefined>; ) => Promise<DatabaseAccount | undefined>;
} }
@@ -17,7 +17,7 @@ interface UseManagedIdentityUpdaterReturn {
} }
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);
@@ -29,18 +29,14 @@ const useManagedIdentity = (
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,
targetResourceGroup,
targetAccountName
);
if (updatedAccount) { if (updatedAccount) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
target: { ...prevState.target, account: updatedAccount } target: { ...prevState.target, account: updatedAccount },
})); }));
} }
} catch (error) { } catch (error) {
@@ -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";
@@ -23,7 +14,7 @@ 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>;
@@ -35,7 +26,7 @@ export const SECTION_IDS = {
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[] = [
@@ -50,7 +41,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
targetAccountIdentityType === IdentityType.SystemAssigned || targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned targetAccountIdentityType === IdentityType.UserAssigned
); );
} },
}, },
{ {
id: SECTION_IDS.defaultManagedIdentity, id: SECTION_IDS.defaultManagedIdentity,
@@ -60,7 +51,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase(); const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity; return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
} },
}, },
{ {
id: SECTION_IDS.readPermissionAssigned, id: SECTION_IDS.readPermissionAssigned,
@@ -73,22 +64,20 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: sourceResourceGroup,
accountName: sourceAccountName accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = 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[] = [
@@ -100,32 +89,32 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
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, id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title, title: ContainerCopyMessages.onlineCopyEnabled.title,
Component: OnlineCopyEnabled, Component: OnlineCopyEnabled,
disabled: true, disabled: true,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate: (_state: CopyJobContextState) => { validate: (_state: CopyJobContextState) => {
return false; 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"),
) ),
); );
} }
@@ -156,7 +145,9 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
useEffect(() => { useEffect(() => {
const validateSections = async () => { const validateSections = async () => {
if (isValidatingRef.current) return; if (isValidatingRef.current) {
return;
}
isValidatingRef.current = true; isValidatingRef.current = true;
const result: PermissionSectionConfig[] = []; const result: PermissionSectionConfig[] = [];
@@ -182,8 +173,7 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
} }
break; break;
} }
} } else {
else {
// Section has no validate method // Section has no validate method
newValidationCache.set(section.id, false); newValidationCache.set(section.id, false);
result.push({ ...section, completed: false }); result.push({ ...section, completed: false });
@@ -193,13 +183,13 @@ const usePermissionSections = (state: CopyJobContextState): PermissionSectionCon
setValidationCache(newValidationCache); setValidationCache(newValidationCache);
setPermissionSections(result); setPermissionSections(result);
isValidatingRef.current = false; isValidatingRef.current = false;
} };
validateSections(); validateSections();
return () => { return () => {
isValidatingRef.current = false; isValidatingRef.current = false;
} };
}, [state, sectionToValidate]); }, [state, sectionToValidate]);
return permissionSections ?? []; return permissionSections ?? [];
@@ -4,12 +4,11 @@ const useWindowOpenMonitor = (url: string, onClose?: () => void, intervalMs = 50
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();
} }
@@ -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,16 +9,12 @@ 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,
title,
Component,
completed,
disabled
}) => (
<AccordionItem key={id} value={id} disabled={disabled}> <AccordionItem key={id} value={id} disabled={disabled}>
<AccordionHeader className="accordionHeader"> <AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">{title}</Text> <Text className="accordionHeaderText" variant="medium">
{title}
</Text>
<Image <Image
className="statusIcon" className="statusIcon"
src={completed ? CheckmarkIcon : WarningIcon} src={completed ? CheckmarkIcon : WarningIcon}
@@ -32,7 +23,7 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({
height={completed ? 20 : 24} height={completed ? 20 : 24}
/> />
</AccordionHeader> </AccordionHeader>
<AccordionPanel aria-disabled={disabled} className="accordionPanel" > <AccordionPanel aria-disabled={disabled} className="accordionPanel">
<Component /> <Component />
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
@@ -45,11 +36,11 @@ const AssignPermissions = () => {
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);
@@ -58,26 +49,16 @@ const AssignPermissions = () => {
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 ? (
<ShimmerTree indentLevels={indentLevels} style={{ width: '100%' }} />
) : ( ) : (
<Accordion <Accordion className="permissionsAccordion" collapsible openItems={openItems}>
className="permissionsAccordion" {permissionSections.map((section) => (
collapsible
openItems={openItems}
>
{
permissionSections.map(section => (
<PermissionSection key={section.id} {...section} /> <PermissionSection key={section.id} {...section} />
)) ))}
}
</Accordion> </Accordion>
) )}
}
</Stack> </Stack>
); );
}; };
@@ -10,13 +10,11 @@ interface FieldRowProps {
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"> <Stack.Item align="center" className="flex-grow-col">
{children} {children}
</Stack.Item> </Stack.Item>
@@ -3,8 +3,10 @@ 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;
}
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
return ( return (
<TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}> <TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}>
<Image src={InfoIcon} alt="Information" width={14} height={14} /> <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 { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react"; import React from "react";
@@ -9,10 +11,17 @@ interface PopoverContainerProps {
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" : ""}`}
tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }}
>
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
{title}
</Text>
<Text>{children}</Text> <Text>{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton <PrimaryButton
@@ -25,7 +34,8 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ isLoadin
</Stack> </Stack>
</Stack> </Stack>
); );
}); },
);
interface PopoverMessageProps { interface PopoverMessageProps {
isLoading?: boolean; isLoading?: boolean;
@@ -36,8 +46,17 @@ interface PopoverMessageProps {
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,
visible,
title,
onCancel,
onPrimary,
children,
}) => {
if (!visible) {
return null;
}
return ( return (
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}> <PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary} isLoading={isLoading}>
{children} {children}
@@ -11,7 +11,7 @@ const CreateCopyJobScreens: React.FC = () => {
handlePrimary, handlePrimary,
handlePrevious, handlePrevious,
handleCancel, handleCancel,
primaryBtnText primaryBtnText,
} = useCopyJobNavigation(); } = useCopyJobNavigation();
return ( return (
@@ -6,38 +6,38 @@ const commonProps = {
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,41 +1,40 @@
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, sourceDatabaseName: copyJobState.source?.databaseId,
sourceContainerName: copyJobState.source?.containerId, sourceContainerName: copyJobState.source?.containerId,
targetDatabaseName: copyJobState.target?.databaseId, targetDatabaseName: copyJobState.target?.databaseId,
targetContainerName: copyJobState.target?.containerId, 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}
onChange={onJobNameChange}
/>
</FieldRow> </FieldRow>
<Stack> <Stack>
<Text className='bold'>{ContainerCopyMessages.sourceSubscriptionLabel}</Text> <Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text> <Text>{copyJobState.source?.subscription?.displayName}</Text>
</Stack> </Stack>
<Stack> <Stack>
<Text className='bold'>{ContainerCopyMessages.sourceAccountLabel}</Text> <Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{copyJobState.source?.account?.name}</Text> <Text>{copyJobState.source?.account?.name}</Text>
</Stack> </Stack>
<Stack> <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";
@@ -24,5 +26,5 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = React.memo(
onChange={onChange} onChange={onChange}
/> />
</FieldRow> </FieldRow>
) ),
); );
@@ -1,3 +1,5 @@
/* 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";
@@ -7,14 +9,8 @@ interface MigrationTypeCheckboxProps {
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 <Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
checked={checked}
onChange={onChange}
/>
</Stack> </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";
@@ -22,5 +24,5 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
onChange={onChange} onChange={onChange}
/> />
</FieldRow> </FieldRow>
) ),
); );
@@ -5,10 +5,10 @@ import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } f
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(
() => () =>
@@ -17,7 +17,7 @@ export function useDropdownOptions(
text: sub.displayName, text: sub.displayName,
data: sub, data: sub,
})) || [], })) || [],
[subscriptions] [subscriptions],
); );
const accountOptions = React.useMemo( const accountOptions = React.useMemo(
@@ -27,7 +27,7 @@ export function useDropdownOptions(
text: account.name, text: account.name,
data: account, data: account,
})) || [], })) || [],
[accounts] [accounts],
); );
return { subscriptionOptions, accountOptions }; return { subscriptionOptions, accountOptions };
@@ -37,7 +37,7 @@ 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 {
@@ -61,7 +61,7 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
return prevState; return prevState;
}); });
}, },
[setCopyJobState] [setCopyJobState],
); );
const handleMigrationTypeChange = React.useCallback( const handleMigrationTypeChange = React.useCallback(
@@ -71,7 +71,7 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
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,16 +12,15 @@ 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 SelectAccount = React.memo(
(_props: SelectAccountProps) => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const subscriptions: Subscription[] = useSubscriptions(); const subscriptions: Subscription[] = useSubscriptions();
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); 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 { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts);
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState); const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
@@ -44,13 +44,9 @@ const SelectAccount = React.memo(
onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)} onChange={(_ev, option) => handleSelectSourceAccount("account", option?.data)}
/> />
<MigrationTypeCheckbox <MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
checked={migrationTypeChecked}
onChange={handleMigrationTypeChange}
/>
</Stack> </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") => return (type: "sourceDatabase" | "sourceContainer" | "targetDatabase" | "targetContainer") =>
(_evnt: any, option: DropdownOptionType) => { (_evnt: React.FormEvent, option: DropdownOptionType) => {
const value = option.key; const value = option.key;
setCopyJobState((prevState) => { setCopyJobState((prevState) => {
switch (type) { switch (type) {
case "sourceDatabase": case "sourceDatabase":
return { return {
...prevState, ...prevState,
source: { ...prevState.source, databaseId: value, containerId: undefined } source: { ...prevState.source, databaseId: value, containerId: undefined },
}; };
case "sourceContainer": case "sourceContainer":
return { return {
...prevState, ...prevState,
source: { ...prevState.source, containerId: value } source: { ...prevState.source, containerId: value },
}; };
case "targetDatabase": case "targetDatabase":
return { return {
...prevState, ...prevState,
target: { ...prevState.target, databaseId: value, containerId: undefined } target: { ...prevState.target, databaseId: value, containerId: undefined },
}; };
case "targetContainer": case "targetContainer":
return { return {
...prevState, ...prevState,
target: { ...prevState.target, containerId: value } target: { ...prevState.target, containerId: value },
}; };
default: default:
return prevState; return prevState;
} }
}); });
} };
} }
@@ -13,7 +13,7 @@ export const DatabaseContainerSection = ({
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>
@@ -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,18 +9,10 @@ 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 SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainersProps) => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const { const { source, target, sourceDbParams, sourceContainerParams, targetDbParams, targetContainerParams } =
source, useMemoizedSourceAndTargetData(copyJobState);
target,
sourceDbParams,
sourceContainerParams,
targetDbParams,
targetContainerParams
} = useMemoizedSourceAndTargetData(copyJobState);
// Custom hooks // Custom hooks
const sourceDatabases = useDatabases(...sourceDbParams) || []; const sourceDatabases = useDatabases(...sourceDbParams) || [];
@@ -29,20 +22,20 @@ const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainers
// Memoize option objects for dropdowns // Memoize option objects for dropdowns
const sourceDatabaseOptions = React.useMemo( const sourceDatabaseOptions = React.useMemo(
() => sourceDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })), () => sourceDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
[sourceDatabases] [sourceDatabases],
); );
const sourceContainerOptions = React.useMemo( const sourceContainerOptions = React.useMemo(
() => sourceContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })), () => sourceContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
[sourceContainers] [sourceContainers],
); );
const targetDatabaseOptions = React.useMemo( const targetDatabaseOptions = React.useMemo(
() => targetDatabases.map((db: any) => ({ key: db.name, text: db.name, data: db })), () => targetDatabases.map((db: DatabaseModel) => ({ key: db.name, text: db.name, data: db })),
[targetDatabases] [targetDatabases],
); );
const targetContainerOptions = React.useMemo( const targetContainerOptions = React.useMemo(
() => targetContainers.map((c: any) => ({ key: c.name, text: c.name, data: c })), () => targetContainers.map((c: DatabaseModel) => ({ key: c.name, text: c.name, data: c })),
[targetContainers] [targetContainers],
); );
const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]); const onDropdownChange = React.useCallback(dropDownChangeHandler(setCopyJobState), [setCopyJobState]);
@@ -76,5 +69,4 @@ const SelectSourceAndTargetContainers = (_props: SelectSourceAndTargetContainers
); );
}; };
export default SelectSourceAndTargetContainers; export default SelectSourceAndTargetContainers;
@@ -9,56 +9,34 @@ export function useMemoizedSourceAndTargetData(copyJobState: CopyJobContextState
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 };
@@ -9,10 +9,7 @@ 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) {
@@ -42,13 +39,10 @@ export function useCopyJobNavigation() {
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]);
[currentScreen.key, copyJobState, cache]
);
const primaryBtnText = useMemo(() => { const primaryBtnText = useMemo(() => {
if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) { if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
return "Copy"; return "Copy";
@@ -7,5 +7,5 @@ interface CopyJobPrerequisitesCacheState {
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 }),
})); }));
@@ -41,9 +41,11 @@ function useCreateCopyJobScreensList() {
component: <SelectSourceAndTargetContainers />, component: <SelectSourceAndTargetContainers />,
validations: [ validations: [
{ {
validate: (state: CopyJobContextState) => ( validate: (state: CopyJobContextState) =>
!!state?.source?.databaseId && !!state?.source?.containerId && !!state?.target?.databaseId && !!state?.target?.containerId !!state?.source?.databaseId &&
), !!state?.source?.containerId &&
!!state?.target?.databaseId &&
!!state?.target?.containerId,
message: "Please select source and target containers to proceed", message: "Please select source and target containers to proceed",
}, },
], ],
@@ -53,11 +55,8 @@ function useCreateCopyJobScreensList() {
component: <PreviewCopyJob />, component: <PreviewCopyJob />,
validations: [ validations: [
{ {
validate: (state: CopyJobContextState) => !!( validate: (state: CopyJobContextState) =>
typeof state?.jobName === "string" !!(typeof state?.jobName === "string" && state?.jobName && /^[a-zA-Z0-9-.]+$/.test(state?.jobName)),
&& state?.jobName
&& /^[a-zA-Z0-9-.]+$/.test(state?.jobName)
),
message: "Please enter a job name to proceed", message: "Please enter a job name to proceed",
}, },
], ],
@@ -69,19 +68,20 @@ function useCreateCopyJobScreensList() {
{ {
validate: (cache: Map<string, boolean>) => { validate: (cache: Map<string, boolean>) => {
const cacheValuesIterator = Array.from(cache.values()); 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); const allValid = cacheValuesIterator.every((isValid: boolean) => isValid);
return allValid; return allValid;
}, },
message: "Please ensure all previous steps are valid to proceed", message: "Please ensure all previous steps are valid to proceed",
} },
], ],
}, },
], ],
[] [],
); );
} }
export { SCREEN_KEYS, useCreateCopyJobScreensList }; export { SCREEN_KEYS, useCreateCopyJobScreensList };
@@ -10,60 +10,53 @@ interface CopyJobActionMenuProps {
} }
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)) {
const filteredItems = baseItems.filter(item => item.key !== CopyJobActions.resume);
if (job.Mode === CopyJobMigrationType.Online) { if (job.Mode === CopyJobMigrationType.Online) {
filteredItems.push({ filteredItems.push({
key: CopyJobActions.complete, key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete, text: ContainerCopyMessages.MonitorJobs.Actions.complete,
onClick: () => handleClick(job, CopyJobActions.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;
@@ -9,7 +9,7 @@ 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",
@@ -75,4 +75,4 @@ export const getColumns = (
isResizable: true, isResizable: true,
onRender: (job: CopyJobType) => <CopyJobActionMenu job={job} handleClick={handleActionClick} />, onRender: (job: CopyJobType) => <CopyJobActionMenu job={job} handleClick={handleActionClick} />,
}, },
]; ];
@@ -5,21 +5,21 @@ 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
@@ -33,18 +33,21 @@ const iconMap: Record<CopyJobStatusType, string> = {
[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,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
ConstrainMode, ConstrainMode,
DetailsListLayoutMode, DetailsListLayoutMode,
@@ -8,7 +9,7 @@ import {
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";
@@ -16,20 +17,20 @@ 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);
@@ -40,21 +41,26 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
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;
}
if (current[columnKey] > next[columnKey]) {
return isDescending ? -1 : 1;
}
return 0; return 0;
}); });
setSortedJobs(sorted); setSortedJobs(sorted);
setSortedColumnKey(columnKey); setSortedColumnKey(columnKey);
setIsSortedDescending(isDescending); 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) => {
// eslint-disable-next-line no-console
console.log("Row clicked:", job); console.log("Row clicked:", job);
}, []); }, []);
@@ -71,12 +77,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
return ( return (
<div style={styles.container}> <div style={styles.container}>
<Stack verticalFill={true}> <Stack verticalFill={true}>
<Stack.Item <Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
verticalFill={true}
grow={1}
shrink={1}
style={styles.stackItem}
>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}> <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList <ShimmeredDetailsList
onRenderRow={_onRenderRow} onRenderRow={_onRenderRow}
@@ -97,6 +98,6 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
</Stack> </Stack>
</div> </div>
); );
} };
export default CopyJobsList; export default CopyJobsList;
@@ -1,16 +1,17 @@
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;
@@ -23,15 +24,16 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
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) {
return;
} // Skip if an update is in progress
try { 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); setError(null);
const response = await getCopyJobs(); const response = await getCopyJobs();
@@ -64,7 +66,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
return; return;
} }
fetchJobs(); fetchJobs();
} },
})); }));
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => { 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.Name === updatedCopyJob.properties.jobName
? { ? {
...prevJob, ...prevJob,
Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType,
} : prevJob }
) : prevJob,
),
); );
} }
} catch (error) { } catch (error) {
@@ -90,20 +93,20 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_p
}, []); }, []);
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;
}
if (jobs.length > 0) {
return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
}
return <CopyJobsNotFound />; return <CopyJobsNotFound />;
}, [jobs, loading, handleActionClick]); }, [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}
isMultiline={false}
onDismiss={() => setError(null)}
>
{error} {error}
</MessageBar> </MessageBar>
)} )}
+19 -23
View File
@@ -22,35 +22,31 @@ export type CopyJobTabForwardRefHandle = {
}; };
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 {
@@ -63,14 +59,14 @@ export interface CopyJobContextState {
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 {
@@ -94,8 +90,8 @@ export type CopyJobType = {
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;
+6 -6
View File
@@ -1,9 +1,9 @@
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>();
@@ -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>;
+5 -7
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,
+4 -1
View File
@@ -1,8 +1,11 @@
import { userContext } from "UserContext"; import { userContext } from "UserContext";
export function getCopyJobAuthorizationHeader(token: string = ""): Headers { export function getCopyJobAuthorizationHeader(token: string = ""): Headers {
if (!token && !userContext.authorizationToken) {
throw new Error("Authorization token is missing");
}
const headers = new Headers(); const headers = new Headers();
const authToken = token ? `Bearer ${token}` : userContext.authorizationToken; const authToken = token ? `Bearer ${token}` : userContext.authorizationToken ?? "";
headers.append("Authorization", authToken); headers.append("Authorization", authToken);
headers.append("Content-Type", "application/json"); headers.append("Content-Type", "application/json");
return headers; return headers;
+20 -19
View File
@@ -43,15 +43,12 @@ const getArmBaseUrl = (): string => {
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();
}; };
@@ -60,25 +57,22 @@ 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 roleDefinitionIds = roleAssignments.map(assignment => assignment.properties.roleDefinitionId);
const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds)); const uniqueRoleDefinitionIds = Array.from(new Set(roleDefinitionIds));
const headers = getCopyJobAuthorizationHeader(); const headers = getCopyJobAuthorizationHeader();
@@ -88,7 +82,7 @@ export const fetchRoleDefinitions = async (
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;
@@ -98,8 +92,11 @@ 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> => {
if (!principalId) {
return null;
}
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`; const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
const roleAssignmentName = crypto.randomUUID(); const roleAssignmentName = crypto.randomUUID();
@@ -109,11 +106,15 @@ export const assignRole = async (
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,
method: "PUT",
apiVersion,
body,
}); });
return response; return response;
}; };
+5 -6
View File
@@ -17,13 +17,12 @@ const buildUrl = (params: FetchAccountDetailsParams): string => {
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");
@@ -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 });
@@ -46,7 +46,6 @@ export type CosmosMongoVCoreDataTransferDataSourceSink = DataTransferDataSourceS
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 */
@@ -76,7 +75,7 @@ export interface DataTransferJobProperties {
/* 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). */
@@ -87,7 +86,7 @@ export interface DataTransferJobProperties {
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";
} }
+15 -14
View File
@@ -9,11 +9,15 @@ 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,
method: "PATCH",
apiVersion,
body,
}); });
if (response.status === "Succeeded") { if (response.status === "Succeeded") {
const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName); const account = await fetchDatabaseAccount(subscriptionId, resourceGroupName, accountName);
@@ -25,12 +29,12 @@ const updateIdentity = async (
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;
@@ -39,18 +43,15 @@ const updateSystemIdentity = async (
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) {
+11 -18
View File
@@ -24,27 +24,27 @@ const buildReadDataContainersListUrl = (params: FetchDataContainersListParams):
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) {
@@ -60,22 +60,15 @@ export function useDataContainers(
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;
+4 -1
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 [];
} }
+15 -16
View File
@@ -22,15 +22,20 @@ const buildReadDatabasesListUrl = (params: FetchDatabasesListParams): string =>
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 (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
apiType: ApiType,
): Promise<DatabaseModel[]> => {
const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, apiType }); const uri = buildReadDatabasesListUrl({ subscriptionId, resourceGroupName, accountName, 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) {
@@ -45,21 +50,15 @@ 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;