mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-13 07:57:26 +01:00
Fix lint & typescript checks
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
+4
-3
@@ -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} <InfoTooltip content={managedIdentityTooltip} />
|
{ContainerCopyMessages.addManagedIdentity.toggleLabel}
|
||||||
|
<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} <InfoTooltip content={userAssignedTooltip} />
|
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}
|
||||||
|
<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>
|
||||||
|
|||||||
+6
-6
@@ -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} <InfoTooltip content={TooltipContent} />
|
{ContainerCopyMessages.readPermissionAssigned.description}
|
||||||
|
<InfoTooltip content={TooltipContent} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={readPermissionAssigned}
|
checked={readPermissionAssigned}
|
||||||
|
|||||||
+2
-1
@@ -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} <InfoTooltip content={managedIdentityTooltip} />
|
{ContainerCopyMessages.defaultManagedIdentity.description}
|
||||||
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={defaultSystemAssigned}
|
checked={defaultSystemAssigned}
|
||||||
|
|||||||
+4
-8
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-11
@@ -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" } } : {})}
|
||||||
|
|||||||
+5
-9
@@ -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) {
|
||||||
|
|||||||
+25
-35
@@ -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 ?? [];
|
||||||
|
|||||||
+1
-2
@@ -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 (
|
||||||
|
|||||||
+19
-19
@@ -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>
|
||||||
|
|||||||
+3
-1
@@ -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>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
+5
-9
@@ -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>
|
||||||
)
|
));
|
||||||
);
|
|
||||||
|
|||||||
+3
-1
@@ -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>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
+8
-8
@@ -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;
|
||||||
|
|||||||
+8
-9
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+12
-20
@@ -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;
|
||||||
+11
-33
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user