added copy job list refresh and reset functionality

This commit is contained in:
Bikram Choudhury
2025-10-23 18:37:23 +05:30
parent 7b437b62ce
commit 6483bd146d
8 changed files with 80 additions and 27 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -110,7 +110,8 @@ export default {
Completed: "Completed", Completed: "Completed",
Failed: "Failed", Failed: "Failed",
Faulted: "Failed", Faulted: "Failed",
Skipped: "Canceled", Skipped: "Cancelled",
Cancelled: "Cancelled",
} }
} }
} }

View File

@@ -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,

View File

@@ -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 }),
}));

View File

@@ -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;

View File

@@ -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>
); );
}; };

View File

@@ -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) {