mirror of
https://github.com/whyour/qinglong.git
synced 2026-07-17 01:14:50 +08:00
全新定时任务管理
This commit is contained in:
+306
-32
@@ -1,39 +1,311 @@
|
||||
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
||||
import { Button, notification, Modal } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
notification,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileTextOutlined,
|
||||
EllipsisOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
EditOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import { request } from '@/utils/http';
|
||||
import CronModal from './modal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
enum CrontabStatus {
|
||||
'idle',
|
||||
'running',
|
||||
'disabled',
|
||||
}
|
||||
|
||||
const Crontab = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: '任务名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => (
|
||||
<span>{record.name || record._id}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务',
|
||||
dataIndex: 'command',
|
||||
key: 'command',
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '任务定时',
|
||||
dataIndex: 'schedule',
|
||||
key: 'schedule',
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any) => (
|
||||
<>
|
||||
{record.status === CrontabStatus.idle && (
|
||||
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||
空闲中
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === CrontabStatus.running && (
|
||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||
运行中
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === CrontabStatus.disabled && (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
已禁用
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center' as const,
|
||||
render: (text: string, record: any, index: number) => (
|
||||
<Space size="middle">
|
||||
<Tooltip title="运行">
|
||||
<a
|
||||
onClick={() => {
|
||||
runCron(record);
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title="日志">
|
||||
<a
|
||||
onClick={() => {
|
||||
logCron(record);
|
||||
}}
|
||||
>
|
||||
<FileTextOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<MoreBtn key="more" record={record} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const [width, setWdith] = useState('100%');
|
||||
const [marginLeft, setMarginLeft] = useState(0);
|
||||
const [marginTop, setMarginTop] = useState(-72);
|
||||
const [value, setValue] = useState('');
|
||||
const [value, setValue] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [editedCron, setEditedCron] = useState();
|
||||
|
||||
const getConfig = () => {
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}config/crontab`)
|
||||
.then((data) => {
|
||||
.get(`${config.apiPrefix}crons`)
|
||||
.then((data: any) => {
|
||||
setValue(data.data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const updateConfig = () => {
|
||||
const addCron = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const editCron = (record: any) => {
|
||||
setEditedCron(record);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const delCron = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: (
|
||||
<>
|
||||
确认删除定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.delete(`${config.apiPrefix}crons`, {
|
||||
data: { _id: record._id },
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
});
|
||||
getCrons();
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const runCron = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认运行',
|
||||
content: (
|
||||
<>
|
||||
确认运行定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.get(`${config.apiPrefix}crons/${record._id}/run`)
|
||||
.then((data: any) => {
|
||||
getCrons();
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const enabledOrDisabledCron = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: (
|
||||
<>
|
||||
确认禁用定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}crons/${record._id}/${
|
||||
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
|
||||
}`,
|
||||
{
|
||||
data: { _id: record._id },
|
||||
},
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (data.code === 200) {
|
||||
notification.success({
|
||||
message: '禁用成功',
|
||||
});
|
||||
getCrons();
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const logCron = (record: any) => {
|
||||
request
|
||||
.post(`${config.apiPrefix}save`, {
|
||||
data: { content: value, name: 'crontab.list' },
|
||||
})
|
||||
.then((data) => {
|
||||
notification.success({
|
||||
message: data.msg,
|
||||
.get(`${config.apiPrefix}crons/${record._id}/log`)
|
||||
.then((data: any) => {
|
||||
Modal.info({
|
||||
width: 650,
|
||||
title: `${record.name || record._id}`,
|
||||
content: (
|
||||
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
||||
{data.data || '暂无日志'}
|
||||
</pre>
|
||||
),
|
||||
onOk() {},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const MoreBtn: React.FC<{
|
||||
record: any;
|
||||
}> = ({ record }) => (
|
||||
<Dropdown
|
||||
arrow
|
||||
trigger={['click', 'hover']}
|
||||
overlay={
|
||||
<Menu onClick={({ key }) => action(key, record)}>
|
||||
<Menu.Item key="edit" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enableordisable"
|
||||
icon={
|
||||
record.status === CrontabStatus.disabled ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<StopOutlined />
|
||||
)
|
||||
}
|
||||
>
|
||||
{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a>
|
||||
<EllipsisOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
const action = (key: string | number, record: any) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
editCron(record);
|
||||
break;
|
||||
case 'enableordisable':
|
||||
enabledOrDisabledCron(record);
|
||||
break;
|
||||
case 'delete':
|
||||
delCron(record);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = (needUpdate?: boolean) => {
|
||||
setIsModalVisible(false);
|
||||
if (needUpdate) {
|
||||
getCrons();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (document.body.clientWidth < 768) {
|
||||
setWdith('auto');
|
||||
@@ -44,16 +316,17 @@ const Crontab = () => {
|
||||
setMarginLeft(0);
|
||||
setMarginTop(-72);
|
||||
}
|
||||
getConfig();
|
||||
getCrons();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="code-mirror-wrapper"
|
||||
title="crontab.list"
|
||||
title="定时任务"
|
||||
loading={loading}
|
||||
extra={[
|
||||
<Button key="1" type="primary" onClick={updateConfig}>
|
||||
保存
|
||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||
添加定时
|
||||
</Button>,
|
||||
]}
|
||||
header={{
|
||||
@@ -68,23 +341,24 @@ const Crontab = () => {
|
||||
marginLeft,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
mode: 'shell',
|
||||
<Table
|
||||
columns={columns}
|
||||
pagination={{
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: true,
|
||||
defaultPageSize: 20,
|
||||
}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
setValue(value);
|
||||
}}
|
||||
onChange={(editor, data, value) => {}}
|
||||
dataSource={value}
|
||||
rowKey="pin"
|
||||
size="middle"
|
||||
bordered
|
||||
scroll={{ x: 768 }}
|
||||
/>
|
||||
<CronModal
|
||||
visible={isModalVisible}
|
||||
handleCancel={handleCancel}
|
||||
cron={editedCron}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, notification, Input, Form } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import cronParse from 'cron-parser';
|
||||
|
||||
const CronModal = ({
|
||||
cron,
|
||||
handleCancel,
|
||||
visible,
|
||||
}: {
|
||||
cron?: any;
|
||||
visible: boolean;
|
||||
handleCancel: (needUpdate?: boolean) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
const method = cron ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (cron) {
|
||||
payload._id = cron._id;
|
||||
}
|
||||
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
||||
data: payload,
|
||||
});
|
||||
if (code === 200) {
|
||||
notification.success({
|
||||
message: cron ? '更新Cron成功' : '添加Cron成功',
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: data,
|
||||
});
|
||||
}
|
||||
handleCancel(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cron) {
|
||||
form.setFieldsValue(cron);
|
||||
}
|
||||
}, [cron]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron ? '编辑定时' : '新建定时'}
|
||||
visible={visible}
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
handleOk(values);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
|
||||
<Form.Item name="name" label="名称">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="command" label="任务" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label="时间"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (cronParse.parseString(value).expressions.length > 0) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('Cron表达式格式有误');
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CronModal;
|
||||
Reference in New Issue
Block a user