Files
alert/src/pages/rules/components/CopyRule/comp/BatchCloneModal.tsx
Johnton Chen 3c636bcaa2 refactor(copy-rule): extract context and improve batch clone modal
- 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>
2026-06-15 23:05:56 +08:00

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;