mirror of
https://github.com/whyour/qinglong.git
synced 2025-07-27 14:46:06 +08:00
定时任务支持批量启用禁用删除运行
This commit is contained in:
parent
2092e93dc2
commit
5d43add6eb
|
@ -51,18 +51,16 @@ export default (app: Router) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.put(
|
||||||
'/crons/:id/run',
|
'/crons/run',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
body: Joi.array().items(Joi.string().required()),
|
||||||
id: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.run(req.params.id);
|
const data = await cronService.run(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
|
@ -71,18 +69,16 @@ export default (app: Router) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.put(
|
||||||
'/crons/:id/disable',
|
'/crons/disable',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
body: Joi.array().items(Joi.string().required()),
|
||||||
id: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.disabled(req.params.id);
|
const data = await cronService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
|
@ -91,18 +87,16 @@ export default (app: Router) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.put(
|
||||||
'/crons/:id/enable',
|
'/crons/enable',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
body: Joi.array().items(Joi.string().required()),
|
||||||
id: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.enabled(req.params.id);
|
const data = await cronService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
|
@ -159,17 +153,15 @@ export default (app: Router) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/crons/:id',
|
'/crons',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
body: Joi.array().items(Joi.string().required()),
|
||||||
id: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.remove(req.params.id);
|
const data = await cronService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
|
|
|
@ -33,7 +33,7 @@ const run = async () => {
|
||||||
) {
|
) {
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
schedule.scheduleJob(task.schedule, function () {
|
||||||
let command = task.command as string;
|
let command = task.command as string;
|
||||||
if (!command.startsWith('task ') && !command.startsWith('ql ')) {
|
if (!command.includes('task ') && !command.includes('ql ')) {
|
||||||
command = `task ${command}`;
|
command = `task ${command}`;
|
||||||
}
|
}
|
||||||
exec(command);
|
exec(command);
|
||||||
|
|
|
@ -74,8 +74,8 @@ export default class CronService {
|
||||||
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
|
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(_id: string) {
|
public async remove(ids: string[]) {
|
||||||
this.cronDb.remove({ _id }, {});
|
this.cronDb.remove({ _id: { $in: ids } }, { multi: true });
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,69 +112,84 @@ export default class CronService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(_id: string) {
|
public async run(ids: string[]) {
|
||||||
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => {
|
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
|
||||||
let res = docs[0];
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
const doc = docs[i];
|
||||||
this.logger.silly('Running job');
|
this.runSingle(doc);
|
||||||
this.logger.silly('ID: ' + _id);
|
|
||||||
this.logger.silly('Original command: ' + res.command);
|
|
||||||
|
|
||||||
let logFile = `${config.manualLogPath}${res._id}.log`;
|
|
||||||
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
|
|
||||||
|
|
||||||
let cmdStr = res.command;
|
|
||||||
if (!cmdStr.startsWith('task') && !cmdStr.startsWith('ql ')) {
|
|
||||||
cmdStr = `task ${cmdStr}`;
|
|
||||||
}
|
}
|
||||||
if (cmdStr.endsWith('.js')) {
|
|
||||||
cmdStr = `${cmdStr} now`;
|
|
||||||
}
|
|
||||||
const cmd = spawn(cmdStr, { shell: true });
|
|
||||||
|
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
|
|
||||||
|
|
||||||
cmd.stdout.on('data', (data) => {
|
|
||||||
this.logger.silly(`stdout: ${data}`);
|
|
||||||
fs.appendFileSync(logFile, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.stderr.on('data', (data) => {
|
|
||||||
this.logger.error(`stderr: ${data}`);
|
|
||||||
fs.appendFileSync(logFile, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('close', (code) => {
|
|
||||||
this.logger.silly(`child process exited with code ${code}`);
|
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('error', (err) => {
|
|
||||||
this.logger.silly(err);
|
|
||||||
fs.appendFileSync(logFile, err.stack);
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('exit', (code: number, signal: any) => {
|
|
||||||
this.logger.silly(`cmd exit ${code}`);
|
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
|
||||||
fs.appendFileSync(logFile, `\n\n执行结束...`);
|
|
||||||
});
|
|
||||||
|
|
||||||
cmd.on('disconnect', () => {
|
|
||||||
this.logger.silly(`cmd disconnect`);
|
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
|
||||||
fs.appendFileSync(logFile, `\n\n连接断开...`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(_id: string) {
|
private async runSingle(cron: Crontab) {
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
|
let { _id, command } = cron;
|
||||||
|
|
||||||
|
this.logger.silly('Running job');
|
||||||
|
this.logger.silly('ID: ' + _id);
|
||||||
|
this.logger.silly('Original command: ' + command);
|
||||||
|
|
||||||
|
let logFile = `${config.manualLogPath}${_id}.log`;
|
||||||
|
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
|
||||||
|
|
||||||
|
let cmdStr = command;
|
||||||
|
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
||||||
|
cmdStr = `task ${cmdStr}`;
|
||||||
|
}
|
||||||
|
if (cmdStr.endsWith('.js')) {
|
||||||
|
cmdStr = `${cmdStr} now`;
|
||||||
|
}
|
||||||
|
const cmd = spawn(cmdStr, { shell: true });
|
||||||
|
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
|
||||||
|
|
||||||
|
cmd.stdout.on('data', (data) => {
|
||||||
|
this.logger.silly(`stdout: ${data}`);
|
||||||
|
fs.appendFileSync(logFile, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.stderr.on('data', (data) => {
|
||||||
|
this.logger.error(`stderr: ${data}`);
|
||||||
|
fs.appendFileSync(logFile, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('close', (code) => {
|
||||||
|
this.logger.silly(`child process exited with code ${code}`);
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('error', (err) => {
|
||||||
|
this.logger.silly(err);
|
||||||
|
fs.appendFileSync(logFile, err.stack);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('exit', (code: number, signal: any) => {
|
||||||
|
this.logger.silly(`cmd exit ${code}`);
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||||
|
fs.appendFileSync(logFile, `\n\n执行结束...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('disconnect', () => {
|
||||||
|
this.logger.silly(`cmd disconnect`);
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||||
|
fs.appendFileSync(logFile, `\n\n连接断开...`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(ids: string[]) {
|
||||||
|
this.cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: CrontabStatus.disabled } },
|
||||||
|
{ multi: true },
|
||||||
|
);
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(_id: string) {
|
public async enabled(ids: string[]) {
|
||||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
this.cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: CrontabStatus.idle } },
|
||||||
|
{ multi: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(_id: string) {
|
public async log(_id: string) {
|
||||||
|
|
|
@ -434,7 +434,6 @@ const Config = () => {
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="cookie-wrapper"
|
className="cookie-wrapper"
|
||||||
title="Cookie管理"
|
title="Cookie管理"
|
||||||
loading={loading}
|
|
||||||
extra={[
|
extra={[
|
||||||
<Button key="2" type="primary" onClick={() => addCookie()}>
|
<Button key="2" type="primary" onClick={() => addCookie()}>
|
||||||
添加Cookie
|
添加Cookie
|
||||||
|
@ -463,9 +462,9 @@ const Config = () => {
|
||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="value"
|
rowKey="value"
|
||||||
size="middle"
|
size="middle"
|
||||||
bordered
|
|
||||||
scroll={{ x: 768 }}
|
scroll={{ x: 768 }}
|
||||||
components={components}
|
components={components}
|
||||||
|
loading={loading}
|
||||||
onRow={(record, index) => {
|
onRow={(record, index) => {
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
|
|
|
@ -39,6 +39,18 @@ enum CrontabStatus {
|
||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OperationName {
|
||||||
|
'启用',
|
||||||
|
'禁用',
|
||||||
|
'运行',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationPath {
|
||||||
|
'enable',
|
||||||
|
'disable',
|
||||||
|
'run',
|
||||||
|
}
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
@ -142,6 +154,7 @@ const Crontab = () => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
const [logCron, setLogCron] = useState<any>();
|
const [logCron, setLogCron] = useState<any>();
|
||||||
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const getCrons = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -177,7 +190,7 @@ const Crontab = () => {
|
||||||
),
|
),
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.delete(`${config.apiPrefix}crons/${record._id}`)
|
.delete(`${config.apiPrefix}crons`, { data: [record._id] })
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
notification.success({
|
notification.success({
|
||||||
|
@ -213,7 +226,7 @@ const Crontab = () => {
|
||||||
),
|
),
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}crons/${record._id}/run`)
|
.put(`${config.apiPrefix}crons/run`, { data: [record._id] })
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
|
@ -252,21 +265,16 @@ const Crontab = () => {
|
||||||
),
|
),
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.get(
|
.put(
|
||||||
`${config.apiPrefix}crons/${record._id}/${
|
`${config.apiPrefix}crons/${
|
||||||
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
|
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
data: { _id: record._id },
|
data: [record._id],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
notification.success({
|
|
||||||
message: `${
|
|
||||||
record.status === CrontabStatus.disabled ? '启用' : '禁用'
|
|
||||||
}成功`,
|
|
||||||
});
|
|
||||||
const newStatus =
|
const newStatus =
|
||||||
record.status === CrontabStatus.disabled
|
record.status === CrontabStatus.disabled
|
||||||
? CrontabStatus.idle
|
? CrontabStatus.idle
|
||||||
|
@ -296,7 +304,7 @@ const Crontab = () => {
|
||||||
}> = ({ record, index }) => (
|
}> = ({ record, index }) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
arrow
|
arrow
|
||||||
trigger={['click', 'hover']}
|
trigger={['click']}
|
||||||
overlay={
|
overlay={
|
||||||
<Menu onClick={({ key }) => action(key, record, index)}>
|
<Menu onClick={({ key }) => action(key, record, index)}>
|
||||||
<Menu.Item key="edit" icon={<EditOutlined />}>
|
<Menu.Item key="edit" icon={<EditOutlined />}>
|
||||||
|
@ -383,6 +391,74 @@ const Crontab = () => {
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
|
setSelectedRowIds(selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowIds,
|
||||||
|
onChange: onSelectChange,
|
||||||
|
selections: [
|
||||||
|
Table.SELECTION_ALL,
|
||||||
|
Table.SELECTION_INVERT,
|
||||||
|
Table.SELECTION_NONE,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const delCrons = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: <>确认删除选中的定时任务吗</>,
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
notification.success({
|
||||||
|
message: '批量删除成功',
|
||||||
|
});
|
||||||
|
getCrons();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const operateCrons = (operationStatus: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${OperationName[operationStatus]}`,
|
||||||
|
content: <>确认{OperationName[operationStatus]}选中的定时任务吗</>,
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
|
||||||
|
data: selectedRowIds,
|
||||||
|
})
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
notification.success({
|
||||||
|
message: `批量${OperationName[operationStatus]}成功`,
|
||||||
|
});
|
||||||
|
getCrons();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logCron) {
|
if (logCron) {
|
||||||
localStorage.setItem('logCron', logCron._id);
|
localStorage.setItem('logCron', logCron._id);
|
||||||
|
@ -410,7 +486,6 @@ const Crontab = () => {
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="code-mirror-wrapper"
|
className="code-mirror-wrapper"
|
||||||
title="定时任务"
|
title="定时任务"
|
||||||
loading={loading}
|
|
||||||
extra={[
|
extra={[
|
||||||
<Search
|
<Search
|
||||||
placeholder="请输入名称或者关键词"
|
placeholder="请输入名称或者关键词"
|
||||||
|
@ -439,6 +514,38 @@ const Crontab = () => {
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Button type="primary" onClick={delCrons}>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateCrons(0)}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
批量启用
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateCrons(1)}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
批量禁用
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateCrons(2)}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
批量运行
|
||||||
|
</Button>
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
已选择
|
||||||
|
<a>{selectedRowIds?.length}</a>项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
@ -449,8 +556,9 @@ const Crontab = () => {
|
||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="_id"
|
rowKey="_id"
|
||||||
size="middle"
|
size="middle"
|
||||||
bordered
|
|
||||||
scroll={{ x: 768 }}
|
scroll={{ x: 768 }}
|
||||||
|
loading={loading}
|
||||||
|
rowSelection={rowSelection}
|
||||||
/>
|
/>
|
||||||
<CronLogModal
|
<CronLogModal
|
||||||
visible={isLogModalVisible}
|
visible={isLogModalVisible}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user