mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-01 15:22:08 +00:00
Added monitor copy job list screen
This commit is contained in:
@@ -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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user