mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-10 04:56:56 +00:00
Added monitor copy job list screen
This commit is contained in:
@@ -1,8 +1,21 @@
|
|||||||
|
import { configContext } from "ConfigContext";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
import { armRequest } from "../../../Utils/arm/request";
|
||||||
import ContainerCopyMessages from "../ContainerCopyMessages";
|
import ContainerCopyMessages from "../ContainerCopyMessages";
|
||||||
|
import {
|
||||||
|
buildDataTransferJobPath,
|
||||||
|
convertTime,
|
||||||
|
convertToCamelCase,
|
||||||
|
COPY_JOB_API_VERSION,
|
||||||
|
COSMOS_SQL_COMPONENT,
|
||||||
|
extractErrorMessage,
|
||||||
|
formatUTCDateTime
|
||||||
|
} from "../CopyJobUtils";
|
||||||
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
||||||
import { CopyJobContextState } from "../Types";
|
import { CopyJobStatusType } from "../Enums";
|
||||||
|
import { CopyJobContextState, CopyJobError, CopyJobType, DataTransferJobType } from "../Types";
|
||||||
|
|
||||||
export const openCreateCopyJobPanel = () => {
|
export const openCreateCopyJobPanel = () => {
|
||||||
const sidePanelState = useSidePanel.getState()
|
const sidePanelState = useSidePanel.getState()
|
||||||
@@ -10,10 +23,132 @@ export const openCreateCopyJobPanel = () => {
|
|||||||
sidePanelState.openSidePanel(
|
sidePanelState.openSidePanel(
|
||||||
ContainerCopyMessages.createCopyJobPanelTitle,
|
ContainerCopyMessages.createCopyJobPanelTitle,
|
||||||
<CreateCopyJobScreensProvider />,
|
<CreateCopyJobScreensProvider />,
|
||||||
"600px"
|
"650px"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const submitCreateCopyJob = (state: CopyJobContextState) => {
|
export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
||||||
console.log("Submitting create copy job with state:", state);
|
try {
|
||||||
};
|
const path = buildDataTransferJobPath({
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.databaseAccount?.resourceGroup || "",
|
||||||
|
accountName: userContext.databaseAccount?.name || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: { value: DataTransferJobType[] } = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion: COPY_JOB_API_VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = response.value || [];
|
||||||
|
if (!Array.isArray(jobs)) {
|
||||||
|
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* added a lower bound to "0" and upper bound to "100" */
|
||||||
|
const calculateCompletionPercentage = (processed: number, total: number): number => {
|
||||||
|
if (
|
||||||
|
typeof processed !== 'number' ||
|
||||||
|
typeof total !== 'number' ||
|
||||||
|
!isFinite(processed) ||
|
||||||
|
!isFinite(total) ||
|
||||||
|
total <= 0
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage = Math.round((processed / total) * 100);
|
||||||
|
return Math.max(0, Math.min(100, percentage));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedJobs: CopyJobType[] = jobs
|
||||||
|
.filter((job: DataTransferJobType) =>
|
||||||
|
job.properties?.source?.component === COSMOS_SQL_COMPONENT &&
|
||||||
|
job.properties?.destination?.component === COSMOS_SQL_COMPONENT
|
||||||
|
)
|
||||||
|
.sort((current: DataTransferJobType, next: DataTransferJobType) =>
|
||||||
|
new Date(next.properties.lastUpdatedUtcTime).getTime() - new Date(current.properties.lastUpdatedUtcTime).getTime()
|
||||||
|
)
|
||||||
|
.map((job: DataTransferJobType, index: number) => {
|
||||||
|
const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ID: (index + 1).toString(),
|
||||||
|
Mode: job.properties.mode,
|
||||||
|
Name: job.properties.jobName,
|
||||||
|
Status: convertToCamelCase(job.properties.status) as CopyJobType["Status"],
|
||||||
|
CompletionPercentage: calculateCompletionPercentage(job.properties.processedCount, job.properties.totalCount),
|
||||||
|
Duration: convertTime(job.properties.duration),
|
||||||
|
LastUpdatedTime: dateTimeObj.formattedDateTime,
|
||||||
|
timestamp: dateTimeObj.timestamp,
|
||||||
|
Error: job.properties.error ? extractErrorMessage(job.properties.error) : null,
|
||||||
|
} as CopyJobType;
|
||||||
|
});
|
||||||
|
return formattedJobs;
|
||||||
|
} catch (error) {
|
||||||
|
const errorContent = JSON.stringify(error.content || error);
|
||||||
|
console.error(`Error fetching copy jobs: ${errorContent}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitCreateCopyJob = async (state: CopyJobContextState) => {
|
||||||
|
try {
|
||||||
|
const { source, target, migrationType, jobName } = state;
|
||||||
|
const path = buildDataTransferJobPath({
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.databaseAccount?.resourceGroup || "",
|
||||||
|
accountName: userContext.databaseAccount?.name || "",
|
||||||
|
jobName
|
||||||
|
});
|
||||||
|
const body = {
|
||||||
|
"properties": {
|
||||||
|
"source": {
|
||||||
|
"component": "CosmosDBSql",
|
||||||
|
"remoteAccountName": source?.account?.name,
|
||||||
|
"databaseName": source?.databaseId,
|
||||||
|
"containerName": source?.containerId
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"component": "CosmosDBSql",
|
||||||
|
"databaseName": target?.databaseId,
|
||||||
|
"containerName": target?.containerId
|
||||||
|
},
|
||||||
|
"mode": migrationType
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: { value: DataTransferJobType } = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT, path, method: "PUT", body, apiVersion: COPY_JOB_API_VERSION
|
||||||
|
});
|
||||||
|
return response.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting create copy job:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateCopyJobStatus = async (job: CopyJobType, action: string): Promise<DataTransferJobType> => {
|
||||||
|
try {
|
||||||
|
const path = buildDataTransferJobPath({
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.databaseAccount?.resourceGroup || "",
|
||||||
|
accountName: userContext.databaseAccount?.name || "",
|
||||||
|
jobName: job.Name,
|
||||||
|
action: action
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: { value: DataTransferJobType } = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion: COPY_JOB_API_VERSION
|
||||||
|
});
|
||||||
|
return response.value;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
|
||||||
|
|
||||||
|
const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
|
||||||
|
const pattern = new RegExp(`'(${statusList.join('|')})'`, 'g');
|
||||||
|
const normalizedErrorMessage = errorMessage.replace(pattern, `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`);
|
||||||
|
|
||||||
|
console.error(`Error updating copy job status: ${normalizedErrorMessage}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,5 +82,35 @@ export default {
|
|||||||
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: {
|
||||||
|
Columns: {
|
||||||
|
lastUpdatedTime: "Date & time",
|
||||||
|
name: "Job name",
|
||||||
|
status: "Status",
|
||||||
|
completionPercentage: "Completion %",
|
||||||
|
duration: "Duration",
|
||||||
|
error: "Error message",
|
||||||
|
mode: "Mode",
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
Actions: {
|
||||||
|
pause: "Pause",
|
||||||
|
resume: "Resume",
|
||||||
|
stop: "Stop",
|
||||||
|
cutover: "Cutover",
|
||||||
|
viewDetails: "View Details",
|
||||||
|
},
|
||||||
|
Status: {
|
||||||
|
Pending: "Pending",
|
||||||
|
InProgress: "In Progress",
|
||||||
|
Running: "In Progress",
|
||||||
|
Partitioning: "In Progress",
|
||||||
|
Paused: "Paused",
|
||||||
|
Completed: "Completed",
|
||||||
|
Failed: "Failed",
|
||||||
|
Faulted: "Failed",
|
||||||
|
Skipped: "Canceled",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,81 @@
|
|||||||
import { DatabaseAccount } from "Contracts/DataModels";
|
import { DatabaseAccount } from "Contracts/DataModels";
|
||||||
|
import { CopyJobErrorType } from "./Types";
|
||||||
|
|
||||||
export const buildResourceLink = (resource: DatabaseAccount): string => {
|
export const buildResourceLink = (resource: DatabaseAccount): string => {
|
||||||
const resourceId = resource.id;
|
const resourceId = resource.id;
|
||||||
// TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax)
|
// TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax)
|
||||||
return `https://ms.portal.azure.com/#resource${resourceId}`;
|
return `https://ms.portal.azure.com/#resource${resourceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COSMOS_SQL_COMPONENT = "CosmosDBSql";
|
||||||
|
|
||||||
|
export const COPY_JOB_API_VERSION = "2025-05-01-preview";
|
||||||
|
|
||||||
|
export function buildDataTransferJobPath({
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
jobName,
|
||||||
|
action,
|
||||||
|
}: {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
jobName?: string;
|
||||||
|
action?: string;
|
||||||
|
}) {
|
||||||
|
let path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
|
||||||
|
if (jobName) path += `/${jobName}`;
|
||||||
|
if (action) path += `/${action}`;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTime(timeStr: string): string | null {
|
||||||
|
const timeParts = timeStr.split(":").map(Number);
|
||||||
|
|
||||||
|
if (timeParts.length !== 3 || timeParts.some(isNaN)) {
|
||||||
|
return null; // Return null for invalid format
|
||||||
|
}
|
||||||
|
const formatPart = (value: number, unit: string) => {
|
||||||
|
if (unit === "seconds") {
|
||||||
|
value = Math.round(value);
|
||||||
|
}
|
||||||
|
return value > 0 ? `${value.toString().padStart(2, "0")} ${unit}` : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hours, minutes, seconds] = timeParts;
|
||||||
|
const formattedTimeParts = [formatPart(hours, "hours"), formatPart(minutes, "minutes"), formatPart(seconds, "seconds")]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return formattedTimeParts || "0 seconds"; // Return "0 seconds" if all parts are zero
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUTCDateTime(utcStr: string): { formattedDateTime: string, timestamp: number } | null {
|
||||||
|
const date = new Date(utcStr);
|
||||||
|
if (isNaN(date.getTime())) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
formattedDateTime: new Intl.DateTimeFormat("en-US", {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "medium",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(date),
|
||||||
|
timestamp: date.getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToCamelCase(str: string): string {
|
||||||
|
const formattedStr = str.split(/\s+/).map(
|
||||||
|
word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||||
|
).join('');
|
||||||
|
return formattedStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractErrorMessage(error: CopyJobErrorType): CopyJobErrorType {
|
||||||
|
return {
|
||||||
|
...error,
|
||||||
|
message: error.message.split("\r\n\r\n")[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCopyJobContext } from "../../Context/CopyJobContext";
|
|
||||||
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
|
import { useCopyJobNavigation } from "../Utils/useCopyJobNavigation";
|
||||||
import NavigationControls from "./Components/NavigationControls";
|
import NavigationControls from "./Components/NavigationControls";
|
||||||
|
|
||||||
const CreateCopyJobScreens: React.FC = () => {
|
const CreateCopyJobScreens: React.FC = () => {
|
||||||
const { copyJobState } = useCopyJobContext();
|
|
||||||
const {
|
const {
|
||||||
currentScreen,
|
currentScreen,
|
||||||
isPrimaryDisabled,
|
isPrimaryDisabled,
|
||||||
@@ -14,7 +12,7 @@ const CreateCopyJobScreens: React.FC = () => {
|
|||||||
handlePrevious,
|
handlePrevious,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
primaryBtnText
|
primaryBtnText
|
||||||
} = useCopyJobNavigation(copyJobState);
|
} = useCopyJobNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
|
<Stack verticalAlign="space-between" className="createCopyJobScreensContainer">
|
||||||
|
|||||||
@@ -1,31 +1,43 @@
|
|||||||
import { IColumn } from "@fluentui/react";
|
import { IColumn } from "@fluentui/react";
|
||||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
minWidth: 130,
|
||||||
|
maxWidth: 140,
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
lineHeight: '1.2',
|
||||||
|
wordBreak: 'break-word'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getPreviewCopyJobDetailsListColumns = function (): IColumn[] {
|
export const getPreviewCopyJobDetailsListColumns = function (): IColumn[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'sourcedbname',
|
key: 'sourcedbname',
|
||||||
name: ContainerCopyMessages.sourceDatabaseLabel,
|
name: ContainerCopyMessages.sourceDatabaseLabel,
|
||||||
fieldName: 'sourceDatabaseName',
|
fieldName: 'sourceDatabaseName',
|
||||||
minWidth: 100
|
...commonProps
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sourcecolname',
|
key: 'sourcecolname',
|
||||||
name: ContainerCopyMessages.sourceContainerLabel,
|
name: ContainerCopyMessages.sourceContainerLabel,
|
||||||
fieldName: 'sourceContainerName',
|
fieldName: 'sourceContainerName',
|
||||||
minWidth: 100
|
...commonProps
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'targetdbname',
|
key: 'targetdbname',
|
||||||
name: ContainerCopyMessages.targetDatabaseLabel,
|
name: ContainerCopyMessages.targetDatabaseLabel,
|
||||||
fieldName: 'targetDatabaseName',
|
fieldName: 'targetDatabaseName',
|
||||||
minWidth: 100
|
...commonProps
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'targetcolname',
|
key: 'targetcolname',
|
||||||
name: ContainerCopyMessages.targetContainerLabel,
|
name: ContainerCopyMessages.targetContainerLabel,
|
||||||
fieldName: 'targetContainerName',
|
fieldName: 'targetContainerName',
|
||||||
minWidth: 100
|
...commonProps
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useReducer } from "react";
|
import { useCallback, useMemo, useReducer } from "react";
|
||||||
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
||||||
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
|
import { submitCreateCopyJob } from "../../Actions/CopyJobActions";
|
||||||
import { CopyJobContextState } from "../../Types";
|
import { useCopyJobContext } from "../../Context/CopyJobContext";
|
||||||
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
|
import { useCopyJobPrerequisitesCache } from "./useCopyJobPrerequisitesCache";
|
||||||
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
|
import { SCREEN_KEYS, useCreateCopyJobScreensList } from "./useCreateCopyJobScreensList";
|
||||||
|
|
||||||
@@ -33,7 +33,8 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCopyJobNavigation(copyJobState: CopyJobContextState) {
|
export function useCopyJobNavigation() {
|
||||||
|
const { copyJobState } = useCopyJobContext();
|
||||||
const screens = useCreateCopyJobScreensList();
|
const screens = useCreateCopyJobScreensList();
|
||||||
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
||||||
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ function useCreateCopyJobScreensList() {
|
|||||||
component: <PreviewCopyJob />,
|
component: <PreviewCopyJob />,
|
||||||
validations: [
|
validations: [
|
||||||
{
|
{
|
||||||
validate: (state: CopyJobContextState) => !!state?.jobName,
|
validate: (state: CopyJobContextState) => !!(
|
||||||
|
typeof state?.jobName === "string"
|
||||||
|
&& state?.jobName
|
||||||
|
&& /^[a-zA-Z0-9-.]+$/.test(state?.jobName)
|
||||||
|
),
|
||||||
message: "Please enter a job name to proceed",
|
message: "Please enter a job name to proceed",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,8 +19,22 @@ export enum BackupPolicyType {
|
|||||||
Periodic = "Periodic",
|
Periodic = "Periodic",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CopyJobMigrationStatus {
|
export enum CopyJobStatusType {
|
||||||
Pause = "Pause",
|
Pending = "Pending",
|
||||||
Resume = "Resume",
|
InProgress = "InProgress",
|
||||||
Cancel = "Cancel",
|
Running = "Running",
|
||||||
|
Partitioning = "Partitioning",
|
||||||
|
Paused = "Paused",
|
||||||
|
Skipped = "Skipped",
|
||||||
|
Completed = "Completed",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
Failed = "Failed",
|
||||||
|
Faulted = "Faulted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CopyJobActions {
|
||||||
|
pause = "pause",
|
||||||
|
stop = "cancel",
|
||||||
|
resume = "resume",
|
||||||
|
cutover = "complete"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
|
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums";
|
||||||
|
import { CopyJobType } from "../../Types";
|
||||||
|
|
||||||
|
interface CopyJobActionMenuProps {
|
||||||
|
job: CopyJobType;
|
||||||
|
handleClick: (job: CopyJobType, action: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||||
|
if ([
|
||||||
|
CopyJobStatusType.Completed,
|
||||||
|
CopyJobStatusType.Cancelled
|
||||||
|
].includes(job.Status)) return null;
|
||||||
|
|
||||||
|
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||||
|
const baseItems = [
|
||||||
|
{
|
||||||
|
key: CopyJobActions.pause,
|
||||||
|
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||||
|
onClick: () => handleClick(job, CopyJobActions.pause)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CopyJobActions.stop,
|
||||||
|
text: ContainerCopyMessages.MonitorJobs.Actions.stop,
|
||||||
|
onClick: () => handleClick(job, CopyJobActions.stop)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CopyJobActions.resume,
|
||||||
|
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||||
|
onClick: () => handleClick(job, CopyJobActions.resume)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (CopyJobStatusType.Paused === job.Status) {
|
||||||
|
return baseItems.filter(item => item.key !== CopyJobActions.pause);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CopyJobStatusType.Pending === job.Status) {
|
||||||
|
return baseItems.filter(item => item.key !== CopyJobActions.resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([
|
||||||
|
CopyJobStatusType.InProgress,
|
||||||
|
CopyJobStatusType.Running,
|
||||||
|
CopyJobStatusType.Partitioning
|
||||||
|
].includes(job.Status)) {
|
||||||
|
const filteredItems = baseItems.filter(item => item.key !== CopyJobActions.resume);
|
||||||
|
if (job.Mode === CopyJobMigrationType.Online) {
|
||||||
|
filteredItems.push({
|
||||||
|
key: CopyJobActions.cutover,
|
||||||
|
text: ContainerCopyMessages.MonitorJobs.Actions.cutover,
|
||||||
|
onClick: () => handleClick(job, CopyJobActions.cutover)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([
|
||||||
|
CopyJobStatusType.Failed,
|
||||||
|
CopyJobStatusType.Faulted,
|
||||||
|
CopyJobStatusType.Skipped,
|
||||||
|
].includes(job.Status)) {
|
||||||
|
return baseItems.filter(item => item.key === CopyJobActions.resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
role="button"
|
||||||
|
iconProps={{ iconName: "more" }}
|
||||||
|
menuProps={{ items: getMenuItems() }}
|
||||||
|
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
|
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyJobActionMenu;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { IColumn } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
|
import { CopyJobType } from "../../Types";
|
||||||
|
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||||
|
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||||
|
|
||||||
|
export const getColumns = (
|
||||||
|
handleSort: (columnKey: string) => void,
|
||||||
|
handleActionClick: (job: CopyJobType, action: string) => void,
|
||||||
|
sortedColumnKey: string | undefined,
|
||||||
|
isSortedDescending: boolean
|
||||||
|
): IColumn[] => [
|
||||||
|
{
|
||||||
|
key: "LastUpdatedTime",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime,
|
||||||
|
fieldName: "LastUpdatedTime",
|
||||||
|
minWidth: 100,
|
||||||
|
maxWidth: 150,
|
||||||
|
isResizable: true,
|
||||||
|
isSorted: sortedColumnKey === "timestamp",
|
||||||
|
isSortedDescending: isSortedDescending,
|
||||||
|
onColumnClick: () => handleSort("timestamp"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Name",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.name,
|
||||||
|
fieldName: "Name",
|
||||||
|
minWidth: 90,
|
||||||
|
maxWidth: 130,
|
||||||
|
isResizable: true,
|
||||||
|
isSorted: sortedColumnKey === "Name",
|
||||||
|
isSortedDescending: isSortedDescending,
|
||||||
|
onColumnClick: () => handleSort("Name"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Mode",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.mode,
|
||||||
|
fieldName: "Mode",
|
||||||
|
minWidth: 70,
|
||||||
|
maxWidth: 90,
|
||||||
|
isResizable: true,
|
||||||
|
isSorted: sortedColumnKey === "Mode",
|
||||||
|
isSortedDescending: isSortedDescending,
|
||||||
|
onColumnClick: () => handleSort("Mode"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CompletionPercentage",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage,
|
||||||
|
fieldName: "CompletionPercentage",
|
||||||
|
minWidth: 120,
|
||||||
|
maxWidth: 130,
|
||||||
|
isResizable: true,
|
||||||
|
isSorted: sortedColumnKey === "CompletionPercentage",
|
||||||
|
isSortedDescending: isSortedDescending,
|
||||||
|
onRender: (job: CopyJobType) => `${job.CompletionPercentage}%`,
|
||||||
|
onColumnClick: () => handleSort("CompletionPercentage"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CopyJobStatus",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.status,
|
||||||
|
fieldName: "Status",
|
||||||
|
minWidth: 80,
|
||||||
|
maxWidth: 100,
|
||||||
|
isResizable: true,
|
||||||
|
isSorted: sortedColumnKey === "Status",
|
||||||
|
isSortedDescending: isSortedDescending,
|
||||||
|
onRender: (job: CopyJobType) => <CopyJobStatusWithIcon status={job.Status} />,
|
||||||
|
onColumnClick: () => handleSort("Status"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Actions",
|
||||||
|
name: ContainerCopyMessages.MonitorJobs.Columns.actions,
|
||||||
|
minWidth: 200,
|
||||||
|
isResizable: true,
|
||||||
|
onRender: (job: CopyJobType) => <CopyJobActionMenu job={job} handleClick={handleActionClick} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { FontIcon, mergeStyles, mergeStyleSets, Stack, Text } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
|
import { CopyJobStatusType } from "../../Enums";
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const iconClass = mergeStyles({
|
||||||
|
fontSize: '1em',
|
||||||
|
marginRight: '0.3em',
|
||||||
|
});
|
||||||
|
const classNames = mergeStyleSets({
|
||||||
|
[CopyJobStatusType.Pending]: [{ color: '#fe7f2d' }, iconClass],
|
||||||
|
[CopyJobStatusType.InProgress]: [{ color: '#ee9b00' }, iconClass],
|
||||||
|
[CopyJobStatusType.Running]: [{ color: '#ee9b00' }, iconClass],
|
||||||
|
[CopyJobStatusType.Partitioning]: [{ color: '#ee9b00' }, iconClass],
|
||||||
|
[CopyJobStatusType.Paused]: [{ color: '#bb3e03' }, iconClass],
|
||||||
|
[CopyJobStatusType.Skipped]: [{ color: '#00bbf9' }, iconClass],
|
||||||
|
[CopyJobStatusType.Cancelled]: [{ color: '#00bbf9' }, iconClass],
|
||||||
|
[CopyJobStatusType.Failed]: [{ color: '#d90429' }, iconClass],
|
||||||
|
[CopyJobStatusType.Faulted]: [{ color: '#d90429' }, iconClass],
|
||||||
|
[CopyJobStatusType.Completed]: [{ color: '#386641' }, iconClass],
|
||||||
|
unknown: [{ color: '#000814' }, iconClass],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon Mapping
|
||||||
|
const iconMap: Record<CopyJobStatusType, string> = {
|
||||||
|
[CopyJobStatusType.Pending]: "MSNVideosSolid",
|
||||||
|
[CopyJobStatusType.InProgress]: "SyncStatusSolid",
|
||||||
|
[CopyJobStatusType.Running]: "SyncStatusSolid",
|
||||||
|
[CopyJobStatusType.Partitioning]: "SyncStatusSolid",
|
||||||
|
[CopyJobStatusType.Paused]: "CirclePauseSolid",
|
||||||
|
[CopyJobStatusType.Skipped]: "Blocked2Solid",
|
||||||
|
[CopyJobStatusType.Cancelled]: "Blocked2Solid",
|
||||||
|
[CopyJobStatusType.Failed]: "AlertSolid",
|
||||||
|
[CopyJobStatusType.Faulted]: "AlertSolid",
|
||||||
|
[CopyJobStatusType.Completed]: "CompletedSolid"
|
||||||
|
};
|
||||||
|
|
||||||
|
const CopyJobStatusWithIcon: React.FC<{ status: CopyJobStatusType }> = ({ status }) => (
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<FontIcon
|
||||||
|
aria-label={status}
|
||||||
|
iconName={iconMap[status] || "UnknownSolid"}
|
||||||
|
className={classNames[status] || classNames.unknown}
|
||||||
|
/>
|
||||||
|
<Text>{(ContainerCopyMessages.MonitorJobs.Status as any)[status]}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CopyJobStatusWithIcon;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
ConstrainMode,
|
||||||
|
DetailsListLayoutMode,
|
||||||
|
DetailsRow,
|
||||||
|
IColumn,
|
||||||
|
ScrollablePane,
|
||||||
|
ScrollbarVisibility,
|
||||||
|
ShimmeredDetailsList,
|
||||||
|
Stack,
|
||||||
|
Sticky,
|
||||||
|
StickyPositionType
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { CopyJobType } from "../../Types";
|
||||||
|
import { getColumns } from "./CopyJobColumns";
|
||||||
|
|
||||||
|
interface CopyJobsListProps {
|
||||||
|
jobs: CopyJobType[];
|
||||||
|
handleActionClick: (job: CopyJobType, action: string) => void,
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
container: { height: 'calc(100vh - 15em)' } as React.CSSProperties,
|
||||||
|
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100; // Number of items per page
|
||||||
|
|
||||||
|
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
||||||
|
const [startIndex, setStartIndex] = React.useState(0);
|
||||||
|
const [sortedJobs, setSortedJobs] = React.useState(jobs);
|
||||||
|
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||||
|
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSortedJobs(jobs);
|
||||||
|
}, [jobs]);
|
||||||
|
|
||||||
|
const handleSort = (columnKey: string) => {
|
||||||
|
const isDescending = sortedColumnKey === columnKey ? !isSortedDescending : false;
|
||||||
|
const sorted = [...sortedJobs].sort((current: any, next: any) => {
|
||||||
|
if (current[columnKey] < next[columnKey]) return isDescending ? 1 : -1;
|
||||||
|
if (current[columnKey] > next[columnKey]) return isDescending ? -1 : 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
setSortedJobs(sorted);
|
||||||
|
setSortedColumnKey(columnKey);
|
||||||
|
setIsSortedDescending(isDescending);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: IColumn[] = React.useMemo(
|
||||||
|
() => getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending),
|
||||||
|
[handleSort, handleActionClick, sortedColumnKey, isSortedDescending]
|
||||||
|
);
|
||||||
|
|
||||||
|
const _handleRowClick = React.useCallback((job: CopyJobType) => {
|
||||||
|
console.log("Row clicked:", job);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const _onRenderRow = React.useCallback((props: any) => {
|
||||||
|
return (
|
||||||
|
<div onClick={_handleRowClick.bind(null, props.item)}>
|
||||||
|
<DetailsRow {...props} styles={{ root: { cursor: "pointer" } }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const totalCount = jobs.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<Stack verticalFill={true}>
|
||||||
|
<Stack.Item
|
||||||
|
verticalFill={true}
|
||||||
|
grow={1}
|
||||||
|
shrink={1}
|
||||||
|
style={styles.stackItem}
|
||||||
|
>
|
||||||
|
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||||
|
<ShimmeredDetailsList
|
||||||
|
onRenderRow={_onRenderRow}
|
||||||
|
checkboxVisibility={2}
|
||||||
|
columns={columns}
|
||||||
|
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
|
||||||
|
enableShimmer={false}
|
||||||
|
constrainMode={ConstrainMode.unconstrained}
|
||||||
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
|
onRenderDetailsHeader={(props, defaultRender) => (
|
||||||
|
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
|
||||||
|
{defaultRender({ ...props })}
|
||||||
|
</Sticky>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ScrollablePane>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyJobsList;
|
||||||
@@ -1,13 +1,101 @@
|
|||||||
import React from 'react';
|
import { MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||||
|
import ShimmerTree, { IndentLevel } from 'Common/ShimmerTree';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { getCopyJobs, updateCopyJobStatus } from '../Actions/CopyJobActions';
|
||||||
|
import { convertToCamelCase } from '../CopyJobUtils';
|
||||||
|
import { CopyJobStatusType } from '../Enums';
|
||||||
import CopyJobsNotFound from '../MonitorCopyJobs/Components/CopyJobs.NotFound';
|
import CopyJobsNotFound from '../MonitorCopyJobs/Components/CopyJobs.NotFound';
|
||||||
|
import { CopyJobType } from '../Types';
|
||||||
|
import CopyJobsList from './Components/CopyJobsList';
|
||||||
|
|
||||||
|
const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 seconds)
|
||||||
|
|
||||||
interface MonitorCopyJobsProps { }
|
interface MonitorCopyJobsProps { }
|
||||||
|
|
||||||
const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
||||||
|
const [loading, setLoading] = React.useState(true); // Start with loading as true
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
const [jobs, setJobs] = React.useState<CopyJobType[]>([]);
|
||||||
|
const isUpdatingRef = React.useRef(false); // Use ref to track updating state
|
||||||
|
const isFirstFetchRef = React.useRef(true); // Use ref to track the first fetch
|
||||||
|
|
||||||
|
const indentLevels = React.useMemo<IndentLevel[]>(
|
||||||
|
() => Array(7).fill({ level: 0, width: "100%" }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchJobs = React.useCallback(async () => {
|
||||||
|
if (isUpdatingRef.current) return; // Skip if an update is in progress
|
||||||
|
try {
|
||||||
|
if (isFirstFetchRef.current) setLoading(true); // Show loading spinner only for the first fetch
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await getCopyJobs();
|
||||||
|
setJobs((prevJobs) => {
|
||||||
|
// Only update jobs if they are different
|
||||||
|
const isSame = JSON.stringify(prevJobs) === JSON.stringify(response);
|
||||||
|
return isSame ? prevJobs : response;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setError(error.message || "Failed to load copy jobs. Please try again later.");
|
||||||
|
} finally {
|
||||||
|
if (isFirstFetchRef.current) {
|
||||||
|
setLoading(false); // Hide loading spinner after the first fetch
|
||||||
|
isFirstFetchRef.current = false; // Mark the first fetch as complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchJobs();
|
||||||
|
|
||||||
|
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [fetchJobs]);
|
||||||
|
|
||||||
|
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => {
|
||||||
|
try {
|
||||||
|
isUpdatingRef.current = true; // Mark as updating
|
||||||
|
const updatedCopyJob = await updateCopyJobStatus(job, action);
|
||||||
|
if (updatedCopyJob) {
|
||||||
|
setJobs((prevJobs) =>
|
||||||
|
prevJobs.map((prevJob) =>
|
||||||
|
prevJob.Name === updatedCopyJob.properties.jobName
|
||||||
|
? {
|
||||||
|
...prevJob,
|
||||||
|
MigrationStatus: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType
|
||||||
|
} : prevJob
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(error.message || "Failed to update copy job status. Please try again later.");
|
||||||
|
} finally {
|
||||||
|
isUpdatingRef.current = false; // Mark as not updating
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const memoizedJobsList = React.useMemo(() => {
|
||||||
|
if (loading) return null;
|
||||||
|
if (jobs.length > 0) return <CopyJobsList jobs={jobs} handleActionClick={handleActionClick} />;
|
||||||
|
return <CopyJobsNotFound />;
|
||||||
|
}, [jobs, loading, handleActionClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='monitorCopyJobs flexContainer'>
|
<Stack className='monitorCopyJobs flexContainer'>
|
||||||
<CopyJobsNotFound />
|
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: '100%', padding: '1rem 2.5rem' }} />}
|
||||||
</div>
|
{error && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.error}
|
||||||
|
isMultiline={false}
|
||||||
|
onDismiss={() => setError(null)}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
{memoizedJobsList}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DatabaseAccount, Subscription } from "Contracts/DataModels";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ApiType } from "UserContext";
|
import { ApiType } from "UserContext";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CopyJobMigrationType } from "../Enums";
|
import { CopyJobMigrationType, CopyJobStatusType } from "../Enums";
|
||||||
|
|
||||||
export interface ContainerCopyProps {
|
export interface ContainerCopyProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@@ -58,7 +58,7 @@ export interface DatabaseContainerSectionProps {
|
|||||||
export interface CopyJobContextState {
|
export interface CopyJobContextState {
|
||||||
jobName: string;
|
jobName: string;
|
||||||
migrationType: CopyJobMigrationType;
|
migrationType: CopyJobMigrationType;
|
||||||
sourceReadAccessFromTarget: boolean;
|
sourceReadAccessFromTarget?: boolean;
|
||||||
// source details
|
// source details
|
||||||
source: {
|
source: {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
@@ -88,3 +88,53 @@ export interface CopyJobContextProviderType {
|
|||||||
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>;
|
setCopyJobState: React.Dispatch<React.SetStateAction<CopyJobContextState>>;
|
||||||
resetCopyJobState: () => void;
|
resetCopyJobState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CopyJobType = {
|
||||||
|
ID: string;
|
||||||
|
Mode: string;
|
||||||
|
Name: string;
|
||||||
|
Status: CopyJobStatusType;
|
||||||
|
CompletionPercentage: number;
|
||||||
|
Duration: string;
|
||||||
|
LastUpdatedTime: string;
|
||||||
|
timestamp: number;
|
||||||
|
Error?: CopyJobErrorType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopyJobErrorType {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopyJobError {
|
||||||
|
message: string;
|
||||||
|
navigateToStep?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataTransferJobType = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
jobName: string;
|
||||||
|
status: string;
|
||||||
|
lastUpdatedUtcTime: string;
|
||||||
|
processedCount: number;
|
||||||
|
totalCount: number;
|
||||||
|
mode: string;
|
||||||
|
duration: string;
|
||||||
|
source: {
|
||||||
|
databaseName: string;
|
||||||
|
collectionName: string;
|
||||||
|
component: string;
|
||||||
|
};
|
||||||
|
destination: {
|
||||||
|
databaseName: string;
|
||||||
|
collectionName: string;
|
||||||
|
component: string;
|
||||||
|
};
|
||||||
|
error: {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -75,3 +75,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitorCopyJobs {
|
||||||
|
padding: 0 2.5rem;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user