- Extract MultiCopyPopupContext to standalone context.ts file - Add Task type and improve type safety in BatchCloneModal - Use useRef/useCallback to stabilize batch clone handlers - Add cleanup effect for selectRules in RuleCheckCard on unmount Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
216 lines
6.1 KiB
TypeScript
216 lines
6.1 KiB
TypeScript
import {
|
|
CheckCircleOutlined,
|
|
CloseCircleOutlined,
|
|
ExclamationCircleOutlined,
|
|
SyncOutlined,
|
|
} from '@ant-design/icons';
|
|
import {
|
|
Alert,
|
|
type AlertProps,
|
|
Button,
|
|
Divider,
|
|
Flex,
|
|
List,
|
|
Modal,
|
|
Progress,
|
|
Space,
|
|
Tag,
|
|
Tooltip,
|
|
} from 'antd';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { processSingleRule } from '@/pages/rules/utils';
|
|
import { $t } from '@/utils/i18n';
|
|
|
|
type TaskStatus = 'waiting' | 'processing' | 'success' | 'warning' | 'error';
|
|
type Task = { name: string; status: TaskStatus; errorMsg: string };
|
|
|
|
type BatchCloneModalProps = {
|
|
visible: boolean;
|
|
selectedRows: Partial<API.RuleListItem>[];
|
|
onClose?: (cb?: () => void) => void;
|
|
onFinish?: (cb?: () => void) => void;
|
|
};
|
|
const BatchCloneModal = ({
|
|
visible,
|
|
selectedRows,
|
|
onClose,
|
|
onFinish,
|
|
}: BatchCloneModalProps) => {
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [currentRunning, setCurrentRunning] = useState(0);
|
|
const selectedRowsRef = useRef(selectedRows);
|
|
selectedRowsRef.current = selectedRows;
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setTasks(
|
|
selectedRows.map((row) => ({
|
|
name: row.name || '',
|
|
status: 'waiting',
|
|
errorMsg: '',
|
|
})),
|
|
);
|
|
setIsRunning(false);
|
|
setCurrentRunning(0);
|
|
}
|
|
}, [visible]);
|
|
|
|
const finishedTasks = useMemo(
|
|
() => tasks.filter((t) => t.status === 'success' || t.status === 'warning'),
|
|
[tasks],
|
|
);
|
|
|
|
const finishedMessage = useMemo(() => {
|
|
const hasWarning = finishedTasks.some((t) => t.status === 'warning');
|
|
const type: AlertProps['type'] = hasWarning
|
|
? 'warning'
|
|
: finishedTasks.length === tasks.length
|
|
? 'success'
|
|
: 'error';
|
|
return {
|
|
type,
|
|
message:
|
|
type === 'success'
|
|
? $t('pages.clones.step3.success')
|
|
: type === 'warning'
|
|
? $t('pages.clones.step3.warning')
|
|
: $t('pages.clones.step3.error'),
|
|
};
|
|
}, [finishedTasks, tasks]);
|
|
|
|
const startBatch = useCallback(async () => {
|
|
const rows = selectedRowsRef.current;
|
|
setIsRunning(true);
|
|
for (let i = 0; i < rows.length; i++) {
|
|
setCurrentRunning(i + 1);
|
|
setTasks((prev) => {
|
|
const next = [...prev];
|
|
next[i].status = 'processing';
|
|
return next;
|
|
});
|
|
try {
|
|
await processSingleRule({ payload: rows[i] });
|
|
setTasks((prev) => {
|
|
const next = [...prev];
|
|
next[i].status = 'success';
|
|
return next;
|
|
});
|
|
} catch (e: any) {
|
|
setTasks((prev) => {
|
|
const next = [...prev];
|
|
next[i].status = [1, 2, 3].includes(e.errType) ? 'warning' : 'error';
|
|
next[i].errorMsg = e.errMsg ? $t(e.errMsg) : e.respMessage;
|
|
return next;
|
|
});
|
|
}
|
|
}
|
|
setIsRunning(false);
|
|
}, []);
|
|
|
|
const handleReset = useCallback(() => {
|
|
setTasks([]);
|
|
setIsRunning(false);
|
|
setCurrentRunning(0);
|
|
}, []);
|
|
|
|
return (
|
|
<Modal
|
|
title={$t('pages.clones.step3.progress')}
|
|
open={visible}
|
|
closable={false}
|
|
maskClosable={false}
|
|
footer={null}
|
|
centered
|
|
destroyOnHidden
|
|
>
|
|
<Progress
|
|
percent={Math.round((finishedTasks.length / tasks.length) * 100)}
|
|
style={{ marginBottom: 20 }}
|
|
/>
|
|
<div style={{ maxHeight: '80vh', overflowY: 'auto' }}>
|
|
<List
|
|
size="small"
|
|
dataSource={tasks}
|
|
renderItem={(item) => (
|
|
<List.Item style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
<span style={{ whiteSpace: 'nowrap' }}>{item.name}</span>
|
|
{item.status === 'processing' && (
|
|
<Tag color="blue" icon={<SyncOutlined spin />}>
|
|
{$t('pages.clones.step3.status1')}
|
|
</Tag>
|
|
)}
|
|
{item.status === 'success' && (
|
|
<Tag color="green" icon={<CheckCircleOutlined />}>
|
|
{$t('pages.clones.step3.status2')}
|
|
</Tag>
|
|
)}
|
|
{item.status === 'error' && (
|
|
<Tooltip
|
|
title={item.errorMsg || $t('pages.clones.step3.status3')}
|
|
>
|
|
<Tag color="red" icon={<CloseCircleOutlined />}>
|
|
{$t('pages.clones.step3.status3')}
|
|
</Tag>
|
|
<ExclamationCircleOutlined />
|
|
</Tooltip>
|
|
)}
|
|
{item.status === 'warning' && (
|
|
<Tooltip
|
|
title={item.errorMsg || $t('pages.clones.step3.status4')}
|
|
>
|
|
<Tag color="orange" icon={<CloseCircleOutlined />}>
|
|
{$t('pages.clones.step3.status4')}
|
|
</Tag>
|
|
<ExclamationCircleOutlined />
|
|
</Tooltip>
|
|
)}
|
|
{item.status === 'waiting' && (
|
|
<Tag color="default">{$t('pages.clones.step3.status5')}</Tag>
|
|
)}
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
</div>
|
|
<Space direction="vertical" className="w-100">
|
|
<Divider style={{ margin: '10px 0' }} />
|
|
{!isRunning && currentRunning === tasks.length && (
|
|
<>
|
|
<Alert
|
|
message={finishedMessage.message}
|
|
type={finishedMessage.type}
|
|
/>
|
|
<Flex justify="flex-end">
|
|
<Button
|
|
type="primary"
|
|
onClick={() => {
|
|
onFinish?.(handleReset);
|
|
}}
|
|
>
|
|
{$t('pages.clones.step3.understand')}
|
|
</Button>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
|
|
{!isRunning && currentRunning === 0 && (
|
|
<Flex justify="flex-end" gap={10}>
|
|
<Button
|
|
onClick={() => {
|
|
onClose?.(handleReset);
|
|
}}
|
|
>
|
|
{$t('pages.model.cancel')}
|
|
</Button>
|
|
<Button type="primary" onClick={startBatch}>
|
|
{$t('pages.clones.step3.start')}
|
|
</Button>
|
|
</Flex>
|
|
)}
|
|
</Space>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default BatchCloneModal;
|