mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 11:36:47 +00:00
added copy job list refresh and reset functionality
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "../CopyJobUtils";
|
} from "../CopyJobUtils";
|
||||||
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
|
||||||
import { CopyJobStatusType } from "../Enums";
|
import { CopyJobStatusType } from "../Enums";
|
||||||
|
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||||
import { CopyJobContextState, CopyJobError, CopyJobType, DataTransferJobType } from "../Types";
|
import { CopyJobContextState, CopyJobError, CopyJobType, DataTransferJobType } from "../Types";
|
||||||
|
|
||||||
export const openCreateCopyJobPanel = () => {
|
export const openCreateCopyJobPanel = () => {
|
||||||
@@ -27,7 +28,14 @@ export const openCreateCopyJobPanel = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let copyJobsAbortController: AbortController | null = null;
|
||||||
|
|
||||||
export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
||||||
|
// Abort previous request if still in-flight
|
||||||
|
if (copyJobsAbortController) {
|
||||||
|
copyJobsAbortController.abort();
|
||||||
|
}
|
||||||
|
copyJobsAbortController = new AbortController();
|
||||||
try {
|
try {
|
||||||
const path = buildDataTransferJobPath({
|
const path = buildDataTransferJobPath({
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
@@ -36,13 +44,18 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const response: { value: DataTransferJobType[] } = await armRequest({
|
const response: { value: DataTransferJobType[] } = await armRequest({
|
||||||
host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion: COPY_JOB_API_VERSION
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path,
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: COPY_JOB_API_VERSION,
|
||||||
|
signal: copyJobsAbortController.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobs = response.value || [];
|
const jobs = response.value || [];
|
||||||
if (!Array.isArray(jobs)) {
|
if (!Array.isArray(jobs)) {
|
||||||
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||||
}
|
}
|
||||||
|
copyJobsAbortController = null;
|
||||||
|
|
||||||
/* 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 => {
|
||||||
@@ -91,7 +104,7 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const submitCreateCopyJob = async (state: CopyJobContextState) => {
|
export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: () => void) => {
|
||||||
try {
|
try {
|
||||||
const { source, target, migrationType, jobName } = state;
|
const { source, target, migrationType, jobName } = state;
|
||||||
const path = buildDataTransferJobPath({
|
const path = buildDataTransferJobPath({
|
||||||
@@ -117,10 +130,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: { value: DataTransferJobType } = await armRequest({
|
const response: DataTransferJobType = await armRequest({
|
||||||
host: configContext.ARM_ENDPOINT, path, method: "PUT", body, apiVersion: COPY_JOB_API_VERSION
|
host: configContext.ARM_ENDPOINT, path, method: "PUT", body, apiVersion: COPY_JOB_API_VERSION
|
||||||
});
|
});
|
||||||
return response.value;
|
MonitorCopyJobsRefState.getState().ref?.refreshJobList();
|
||||||
|
onSuccess();
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting create copy job:", error);
|
console.error("Error submitting create copy job:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -137,10 +152,10 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
|
|||||||
action: action
|
action: action
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: { value: DataTransferJobType } = await armRequest({
|
const response: DataTransferJobType = await armRequest({
|
||||||
host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion: COPY_JOB_API_VERSION
|
host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion: COPY_JOB_API_VERSION
|
||||||
});
|
});
|
||||||
return response.value;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
|
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { CommandButtonComponentProps } from "../../Controls/CommandButton/Comman
|
|||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import * as Actions from "../Actions/CopyJobActions";
|
import * as Actions from "../Actions/CopyJobActions";
|
||||||
import ContainerCopyMessages from "../ContainerCopyMessages";
|
import ContainerCopyMessages from "../ContainerCopyMessages";
|
||||||
|
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||||
import { CopyJobCommandBarBtnType } from "../Types";
|
import { CopyJobCommandBarBtnType } from "../Types";
|
||||||
|
|
||||||
function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
|
function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
|
||||||
|
const monitorCopyJobsRef = MonitorCopyJobsRefState(state => state.ref);
|
||||||
const buttons: CopyJobCommandBarBtnType[] = [
|
const buttons: CopyJobCommandBarBtnType[] = [
|
||||||
{
|
{
|
||||||
key: "createCopyJob",
|
key: "createCopyJob",
|
||||||
@@ -22,7 +24,7 @@ function getCopyJobBtns(): CopyJobCommandBarBtnType[] {
|
|||||||
iconSrc: RefreshIcon,
|
iconSrc: RefreshIcon,
|
||||||
label: ContainerCopyMessages.refreshButtonLabel,
|
label: ContainerCopyMessages.refreshButtonLabel,
|
||||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||||
onClick: () => { },
|
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ export default {
|
|||||||
Completed: "Completed",
|
Completed: "Completed",
|
||||||
Failed: "Failed",
|
Failed: "Failed",
|
||||||
Faulted: "Failed",
|
Faulted: "Failed",
|
||||||
Skipped: "Canceled",
|
Skipped: "Cancelled",
|
||||||
|
Cancelled: "Cancelled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ function navigationReducer(state: NavigationState, action: Action): NavigationSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCopyJobNavigation() {
|
export function useCopyJobNavigation() {
|
||||||
const { copyJobState } = useCopyJobContext();
|
const { copyJobState, resetCopyJobState } = useCopyJobContext();
|
||||||
const screens = useCreateCopyJobScreensList();
|
const screens = useCreateCopyJobScreensList();
|
||||||
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
const { validationCache: cache } = useCopyJobPrerequisitesCache();
|
||||||
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
const [state, dispatch] = useReducer(navigationReducer, { screenHistory: [SCREEN_KEYS.SelectAccount] });
|
||||||
@@ -58,6 +58,12 @@ export function useCopyJobNavigation() {
|
|||||||
|
|
||||||
const isPreviousDisabled = state.screenHistory.length <= 1;
|
const isPreviousDisabled = state.screenHistory.length <= 1;
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
dispatch({ type: "RESET" });
|
||||||
|
resetCopyJobState();
|
||||||
|
useSidePanel.getState().closeSidePanel();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handlePrimary = useCallback(() => {
|
const handlePrimary = useCallback(() => {
|
||||||
const transitions = {
|
const transitions = {
|
||||||
[SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions,
|
[SCREEN_KEYS.SelectAccount]: SCREEN_KEYS.AssignPermissions,
|
||||||
@@ -69,7 +75,7 @@ export function useCopyJobNavigation() {
|
|||||||
if (nextScreen) {
|
if (nextScreen) {
|
||||||
dispatch({ type: "NEXT", nextScreen });
|
dispatch({ type: "NEXT", nextScreen });
|
||||||
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
} else if (currentScreenKey === SCREEN_KEYS.PreviewCopyJob) {
|
||||||
submitCreateCopyJob(copyJobState);
|
submitCreateCopyJob(copyJobState, handleCancel);
|
||||||
}
|
}
|
||||||
}, [currentScreenKey, copyJobState]);
|
}, [currentScreenKey, copyJobState]);
|
||||||
|
|
||||||
@@ -77,11 +83,6 @@ export function useCopyJobNavigation() {
|
|||||||
dispatch({ type: "PREVIOUS" });
|
dispatch({ type: "PREVIOUS" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
dispatch({ type: "RESET" });
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentScreen,
|
currentScreen,
|
||||||
isPrimaryDisabled,
|
isPrimaryDisabled,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import create from "zustand";
|
||||||
|
import { MonitorCopyJobsRef } from "./MonitorCopyJobs";
|
||||||
|
|
||||||
|
type MonitorCopyJobsRefStateType = {
|
||||||
|
ref: MonitorCopyJobsRef;
|
||||||
|
setRef: (ref: MonitorCopyJobsRef) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MonitorCopyJobsRefState = create<MonitorCopyJobsRefStateType>((set) => ({
|
||||||
|
ref: null,
|
||||||
|
setRef: (ref) => set({ ref: ref }),
|
||||||
|
}));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
import { MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||||
import ShimmerTree, { IndentLevel } from 'Common/ShimmerTree';
|
import ShimmerTree, { IndentLevel } from 'Common/ShimmerTree';
|
||||||
import React, { useEffect } from 'react';
|
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
import { getCopyJobs, updateCopyJobStatus } from '../Actions/CopyJobActions';
|
import { getCopyJobs, updateCopyJobStatus } from '../Actions/CopyJobActions';
|
||||||
import { convertToCamelCase } from '../CopyJobUtils';
|
import { convertToCamelCase } from '../CopyJobUtils';
|
||||||
import { CopyJobStatusType } from '../Enums';
|
import { CopyJobStatusType } from '../Enums';
|
||||||
@@ -12,7 +12,11 @@ const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 second
|
|||||||
|
|
||||||
interface MonitorCopyJobsProps { }
|
interface MonitorCopyJobsProps { }
|
||||||
|
|
||||||
const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
export interface MonitorCopyJobsRef {
|
||||||
|
refreshJobList: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>((_props, ref) => {
|
||||||
const [loading, setLoading] = React.useState(true); // Start with loading as true
|
const [loading, setLoading] = React.useState(true); // Start with loading as true
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [jobs, setJobs] = React.useState<CopyJobType[]>([]);
|
const [jobs, setJobs] = React.useState<CopyJobType[]>([]);
|
||||||
@@ -48,12 +52,21 @@ const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
|
|
||||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [fetchJobs]);
|
}, [fetchJobs]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refreshJobList: () => {
|
||||||
|
if (isUpdatingRef.current) {
|
||||||
|
setError("Please wait for the current update to complete before refreshing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchJobs();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => {
|
const handleActionClick = React.useCallback(async (job: CopyJobType, action: string) => {
|
||||||
try {
|
try {
|
||||||
isUpdatingRef.current = true; // Mark as updating
|
isUpdatingRef.current = true; // Mark as updating
|
||||||
@@ -64,7 +77,7 @@ const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
|||||||
prevJob.Name === updatedCopyJob.properties.jobName
|
prevJob.Name === updatedCopyJob.properties.jobName
|
||||||
? {
|
? {
|
||||||
...prevJob,
|
...prevJob,
|
||||||
MigrationStatus: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType
|
Status: convertToCamelCase(updatedCopyJob.properties.status) as CopyJobStatusType
|
||||||
} : prevJob
|
} : prevJob
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -97,6 +110,6 @@ const MonitorCopyJobs: React.FC<MonitorCopyJobsProps> = () => {
|
|||||||
{memoizedJobsList}
|
{memoizedJobsList}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default MonitorCopyJobs;
|
export default MonitorCopyJobs;
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import React, { Suspense } from 'react';
|
import { MonitorCopyJobsRefState } from 'Explorer/ContainerCopy/MonitorCopyJobs/MonitorCopyJobRefState';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
import CopyJobCommandBar from './CommandBar/CopyJobCommandBar';
|
import CopyJobCommandBar from './CommandBar/CopyJobCommandBar';
|
||||||
import { ContainerCopyProps } from './Types';
|
|
||||||
import './containerCopyStyles.less';
|
import './containerCopyStyles.less';
|
||||||
|
import MonitorCopyJobs, { MonitorCopyJobsRef } from './MonitorCopyJobs/MonitorCopyJobs';
|
||||||
const MonitorCopyJobs = React.lazy(() => import('./MonitorCopyJobs/MonitorCopyJobs'));
|
import { ContainerCopyProps } from './Types';
|
||||||
|
|
||||||
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => {
|
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ container }) => {
|
||||||
|
const monitorCopyJobsRef = React.useRef<MonitorCopyJobsRef>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (monitorCopyJobsRef.current) {
|
||||||
|
MonitorCopyJobsRefState.getState().setRef(monitorCopyJobsRef.current);
|
||||||
|
}
|
||||||
|
}, [monitorCopyJobsRef.current]);
|
||||||
return (
|
return (
|
||||||
<div id="containerCopyWrapper" className="flexContainer hideOverflows">
|
<div id="containerCopyWrapper" className="flexContainer hideOverflows">
|
||||||
<CopyJobCommandBar container={container} />
|
<CopyJobCommandBar container={container} />
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<MonitorCopyJobs ref={monitorCopyJobsRef} />
|
||||||
<MonitorCopyJobs />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface Options {
|
|||||||
queryParams?: ARMQueryParams;
|
queryParams?: ARMQueryParams;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
customHeaders?: Record<string, string>;
|
customHeaders?: Record<string, string>;
|
||||||
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function armRequestWithoutPolling<T>({
|
export async function armRequestWithoutPolling<T>({
|
||||||
@@ -59,6 +60,7 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders,
|
customHeaders,
|
||||||
|
signal,
|
||||||
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
||||||
const url = new URL(path, host);
|
const url = new URL(path, host);
|
||||||
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
@@ -81,6 +83,7 @@ export async function armRequestWithoutPolling<T>({
|
|||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -116,6 +119,7 @@ export async function armRequest<T>({
|
|||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders,
|
customHeaders,
|
||||||
|
signal
|
||||||
}: Options): Promise<T> {
|
}: Options): Promise<T> {
|
||||||
const armRequestResult = await armRequestWithoutPolling<T>({
|
const armRequestResult = await armRequestWithoutPolling<T>({
|
||||||
host,
|
host,
|
||||||
@@ -126,6 +130,7 @@ export async function armRequest<T>({
|
|||||||
queryParams,
|
queryParams,
|
||||||
contentType,
|
contentType,
|
||||||
customHeaders,
|
customHeaders,
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
const operationStatusUrl = armRequestResult.operationStatusUrl;
|
const operationStatusUrl = armRequestResult.operationStatusUrl;
|
||||||
if (operationStatusUrl) {
|
if (operationStatusUrl) {
|
||||||
|
|||||||
Reference in New Issue
Block a user