mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-22 22:36:06 +08:00
全新定时任务管理
This commit is contained in:
parent
32be5a6591
commit
8a45599919
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,6 +20,7 @@
|
|||
/src/.umi-test
|
||||
/.env.local
|
||||
.env
|
||||
.history
|
||||
|
||||
/config
|
||||
/log
|
||||
|
|
167
back/api/cron.ts
167
back/api/cron.ts
|
@ -3,15 +3,180 @@ import { Container } from 'typedi';
|
|||
import { Logger } from 'winston';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import CronService from '../services/cron';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/', route);
|
||||
route.get(
|
||||
'/cron',
|
||||
'/crons',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.crontabs();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/crons',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/run',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.run(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/disable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.disabled(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/enable',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.enabled(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/:id/log',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.log(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/crons',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string(),
|
||||
_id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.delete(
|
||||
'/crons/:id',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.remove(req.params.id);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/crons/import',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cookieService = Container.get(CronService);
|
||||
const data = await cookieService.import_crontab();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
|
|
27
back/data/cron.ts
Normal file
27
back/data/cron.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
export class Crontab {
|
||||
name?: string;
|
||||
command: string;
|
||||
schedule: string;
|
||||
timestamp?: string;
|
||||
created?: number;
|
||||
saved?: boolean;
|
||||
_id?: string;
|
||||
status?: CrontabStatus;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
this.command = options.command;
|
||||
this.schedule = options.schedule;
|
||||
this.saved = options.saved;
|
||||
this._id = options._id;
|
||||
this.created = options.created;
|
||||
this.status = options.status || CrontabStatus.idle;
|
||||
this.timestamp = new Date().toString();
|
||||
}
|
||||
}
|
||||
|
||||
export enum CrontabStatus {
|
||||
'idle',
|
||||
'running',
|
||||
'disabled',
|
||||
}
|
183
back/services/cron.ts
Normal file
183
back/services/cron.ts
Normal file
|
@ -0,0 +1,183 @@
|
|||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import DataStore from 'nedb';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { getFileContentByName } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
private cronDb = new DataStore({ filename: config.cronDbFile });
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||
this.cronDb.loadDatabase((err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public async create(payload: Crontab): Promise<void> {
|
||||
const tab = new Crontab(payload);
|
||||
tab.created = new Date().valueOf();
|
||||
tab.saved = false;
|
||||
this.cronDb.insert(tab);
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<void> {
|
||||
const { _id, ...other } = payload;
|
||||
const doc = await this.get(_id);
|
||||
const tab = new Crontab({ ...doc, ...other });
|
||||
tab.saved = false;
|
||||
this.cronDb.update({ _id }, tab, { returnUpdatedDocs: true });
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async status(_id: string, stopped: boolean) {
|
||||
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
|
||||
}
|
||||
|
||||
public async remove(_id: string) {
|
||||
this.cronDb.remove({ _id }, {});
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async crontabs(): Promise<Crontab[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb
|
||||
.find({})
|
||||
.sort({ created: -1 })
|
||||
.exec((err, docs) => {
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async get(_id: string): Promise<Crontab> {
|
||||
return new Promise((resolve) => {
|
||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||
resolve(docs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async run(_id: string) {
|
||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||
let res = docs[0];
|
||||
|
||||
this.logger.silly('Running job');
|
||||
this.logger.silly('ID: ' + _id);
|
||||
this.logger.silly('Original command: ' + res.command);
|
||||
|
||||
let logFile = `${config.manualLogPath}${res._id}.log`;
|
||||
fs.writeFileSync(logFile, `${new Date().toString()}\n\n`);
|
||||
|
||||
const cmd = spawn(res.command, { 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async disabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
|
||||
await this.set_crontab();
|
||||
}
|
||||
|
||||
public async enabled(_id: string) {
|
||||
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||
}
|
||||
|
||||
public async log(_id: string) {
|
||||
let logFile = `${config.manualLogPath}${_id}.log`;
|
||||
return getFileContentByName(logFile);
|
||||
}
|
||||
|
||||
private make_command(tab: Crontab) {
|
||||
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab() {
|
||||
const tabs = await this.crontabs();
|
||||
var crontab_string = '';
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.status !== CrontabStatus.disabled) {
|
||||
crontab_string += tab.schedule;
|
||||
crontab_string += ' ';
|
||||
crontab_string += this.make_command(tab);
|
||||
crontab_string += '\n';
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.silly(crontab_string);
|
||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
||||
}
|
||||
|
||||
private reload_db() {
|
||||
this.cronDb.loadDatabase();
|
||||
}
|
||||
|
||||
public import_crontab() {
|
||||
exec('crontab -l', (error, stdout, stderr) => {
|
||||
var lines = stdout.split('\n');
|
||||
var namePrefix = new Date().getTime();
|
||||
|
||||
lines.reverse().forEach((line, index) => {
|
||||
line = line.replace(/\t+/g, ' ');
|
||||
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||
var command = line.replace(regex, '').trim();
|
||||
var schedule = line.replace(command, '').trim();
|
||||
|
||||
var is_valid = false;
|
||||
try {
|
||||
is_valid = cron_parser.parseString(line).expressions.length > 0;
|
||||
} catch (e) {}
|
||||
if (command && schedule && is_valid) {
|
||||
var name = namePrefix + '_' + index;
|
||||
|
||||
this.cronDb.findOne({ command, schedule }, (err, doc) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!doc) {
|
||||
this.create({ name, command, schedule });
|
||||
} else {
|
||||
doc.command = command;
|
||||
doc.schedule = schedule;
|
||||
this.update(doc);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public autosave_crontab() {
|
||||
return this.set_crontab();
|
||||
}
|
||||
}
|
|
@ -29,13 +29,14 @@
|
|||
"celebrate": "^13.0.3",
|
||||
"codemirror": "^5.59.4",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^3.3.0",
|
||||
"darkreader": "^4.9.27",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"got": "^11.8.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.10.6",
|
||||
"nedb": "^1.8.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
|
@ -51,6 +52,7 @@
|
|||
"@types/express": "^4.17.8",
|
||||
"@types/express-jwt": "^6.0.1",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/nedb": "^1.8.11",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
|
|
74
shell/api.sh
Executable file
74
shell/api.sh
Executable file
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_token() {
|
||||
echo $AuthConf
|
||||
local authInfo=$(cat $AuthConf)
|
||||
token=$(get_json_value "$authInfo" "token")
|
||||
}
|
||||
|
||||
get_json_value() {
|
||||
local json=$1
|
||||
local key=$2
|
||||
|
||||
if [[ -z "$3" ]]; then
|
||||
local num=1
|
||||
else
|
||||
local num=$3
|
||||
fi
|
||||
|
||||
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
|
||||
|
||||
echo ${value}
|
||||
}
|
||||
|
||||
add_cron_api() {
|
||||
local currentTimeStamp=$(date +%s%3)
|
||||
if [ $# -eq 1 ]; then
|
||||
local schedule=$(echo "$1" | awk -F ": " '{print $1}')
|
||||
local command=$(echo "$1" | awk -F ": " '{print $2}')
|
||||
local name=$(echo "$1" | awk -F ": " '{print $3}')
|
||||
else
|
||||
local schedule=$1
|
||||
local command=$2
|
||||
local name=$3
|
||||
fi
|
||||
|
||||
local api=$(curl "http://localhost:5678/api/crons?t=$currentTimeStamp" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://localhost:5700" \
|
||||
-H "Referer: http://localhost:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
||||
--compressed)
|
||||
echo $api
|
||||
code=$(get_json_value $api "code")
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name 添加成功"
|
||||
else
|
||||
echo -e "$name 添加失败"
|
||||
fi
|
||||
}
|
||||
|
||||
del_cron_api() {
|
||||
local id=$1
|
||||
local currentTimeStamp=$(date +%s%3)
|
||||
local api=$(curl "http://localhost:5678/api/crons/$id?t=$currentTimeStamp" \
|
||||
-X 'DELETE' \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://localhost:5700" \
|
||||
-H "Referer: http://localhost:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
|
||||
echo $api
|
||||
code=$(get_json_value $api "code")
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "$name 删除成功"
|
||||
else
|
||||
echo -e "$name 删除失败"
|
||||
fi
|
||||
}
|
|
@ -16,6 +16,7 @@ ScriptsDir=$ShellDir/scripts
|
|||
ConfigDir=$ShellDir/config
|
||||
FileConf=$ConfigDir/config.sh
|
||||
CookieConf=$ConfigDir/cookie.sh
|
||||
AuthConf=$ConfigDir/auth.json
|
||||
ExtraShell=$ConfigDir/extra.sh
|
||||
FileConfSample=$ShellDir/sample/config.sh.sample
|
||||
ListCronSample=$ShellDir/sample/crontab.list.sample
|
||||
|
@ -44,6 +45,8 @@ Import_Conf() {
|
|||
fi
|
||||
[ -f $CookieConf ] && . $CookieConf
|
||||
[ -f $FileConf ] && . $FileConf
|
||||
|
||||
. $ShellDir/shell/api.sh
|
||||
}
|
||||
|
||||
# 更新shell
|
||||
|
@ -198,7 +201,7 @@ Git_Pull_Scripts_Next() {
|
|||
|
||||
Diff_Cron() {
|
||||
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
|
||||
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
|
||||
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*)$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
|
||||
if [ -s $ListCurrentTask ]; then
|
||||
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
|
||||
else
|
||||
|
@ -218,7 +221,7 @@ Del_Cron() {
|
|||
echo
|
||||
JsDrop=$(cat $ListJsDrop)
|
||||
for Cron in $JsDrop; do
|
||||
perl -i -ne "{print unless / $Cron( |$)/}" $ListCronCurrent
|
||||
del_cron_api "$Cron"
|
||||
done
|
||||
crontab $ListCronCurrent
|
||||
echo -e "成功删除失效的脚本与定时任务\n"
|
||||
|
@ -234,7 +237,8 @@ Add_Cron() {
|
|||
if [[ $Cron == jd_bean_sign ]]; then
|
||||
echo "4 0,9 * * * $ShellJs $Cron" >> $ListCronCurrent
|
||||
else
|
||||
cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1$ShellJs \2|" >> $ListCronCurrent
|
||||
param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1\: $ShellJs \2: \2|")
|
||||
add_cron_api "$param"
|
||||
fi
|
||||
done
|
||||
|
||||
|
@ -274,6 +278,8 @@ echo -e "--------------------------------------------------------------\n"
|
|||
Import_Conf
|
||||
Random_Pull_Cron
|
||||
|
||||
get_token
|
||||
|
||||
# 更新shell
|
||||
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
|
||||
Git_Pull_Shell
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
92
src/pages/crontab/modal.tsx
Normal file
92
src/pages/crontab/modal.tsx
Normal file
|
@ -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;
|
Loading…
Reference in New Issue
Block a user