Added monitor copy job list screen

This commit is contained in:
Bikram Choudhury
2025-10-23 16:53:18 +05:30
parent c504d97f7c
commit 7b437b62ce
15 changed files with 750 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { CopyJobType } from '../Types';
import CopyJobsList from './Components/CopyJobsList';
const FETCH_INTERVAL_MS = 30 * 1000; // Interval time in milliseconds (30 seconds)
interface 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 (
<div className='monitorCopyJobs flexContainer'>
<CopyJobsNotFound />
</div>
<Stack className='monitorCopyJobs flexContainer'>
{loading && <ShimmerTree indentLevels={indentLevels} style={{ width: '100%', padding: '1rem 2.5rem' }} />}
{error && (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={() => setError(null)}
>
{error}
</MessageBar>
)}
{memoizedJobsList}
</Stack>
);
}