Compare commits

...

20 Commits

Author SHA1 Message Date
whyour 31a5386cfe 更新版本 v2.14.0 2022-09-03 15:26:04 +08:00
whyour 0198ebbbff 修复任务视图tab激活状态 2022-09-03 15:16:35 +08:00
whyour ce3df8704d 修复任务视图tab样式 2022-09-03 15:02:05 +08:00
whyour 1b4ca3a684 修复视图拖拽数据同步 2022-09-03 14:56:32 +08:00
whyour e31c2c0955 完善定时任务视图拖拽 2022-09-03 12:39:47 +08:00
whyour 4e389865b5 定时任务视图tab高亮 2022-09-03 11:58:21 +08:00
whyour e5d8adf955 修复定时任务视图排序 2022-09-03 02:15:32 +08:00
whyour 9a3181bc44 任务视图增加状态筛选 2022-09-03 01:56:47 +08:00
whyour 2f05c95422 完善定时任务视图 2022-09-02 17:52:06 +08:00
whyour bc5a3a2028 修复nginx配置文件 2022-09-02 11:28:08 +08:00
whyour 1fe91508f6 修复baseUrl 2022-09-02 10:54:44 +08:00
whyour b9253c8191 修复定时任务搜索api 2022-09-02 00:35:20 +08:00
whyour 7516acbc41 修复定时任务搜索 2022-08-31 00:02:35 +08:00
whyour 3b3d45da29 修复定时任务排序 2022-08-28 15:45:30 +08:00
whyour 918d68d140 修复布局样式 2022-08-28 00:44:17 +08:00
whyour a3f56be299 修改整体布局,减少无用间隔 2022-08-28 00:38:25 +08:00
whyour f8f63890e5 定时任务增加分组 2022-08-27 14:50:27 +08:00
whyour 9e997410ab 测试dockerfile 2022-08-26 18:42:25 +08:00
whyour 529880642f 修复typescript类型错误 2022-08-26 17:52:37 +08:00
景大侠 d2590edab3 支持非js任务随机延迟 (#1580)
* 修复非JS任务不能随机延迟的问题
2022-08-26 17:11:16 +08:00
29 changed files with 1267 additions and 133 deletions
+6 -2
View File
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -96,7 +96,7 @@ systemctl restart docker
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -124,6 +124,10 @@ docker-compose up -d
docker-compose down
```
3. access
Open your browser and visit http://{ip}:5700
## Use
1. built-in commands
+6 -2
View File
@@ -57,7 +57,7 @@ https://podman.io/getting-started/installation
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -97,7 +97,7 @@ systemctl restart docker
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
@@ -125,6 +125,10 @@ docker-compose up -d
docker-compose down
```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用
1. 内置命令
+127 -13
View File
@@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
@@ -10,28 +11,141 @@ export default (app: Router) => {
app.use('/crons', route);
route.get(
'/',
celebrate({
query: Joi.object({
searchText: Joi.string().required().allow(''),
page: Joi.string().required(),
size: Joi.string().required(),
t: Joi.string().required(),
}),
}),
'/views',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any);
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional(),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional(),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
+47
View File
@@ -0,0 +1,47 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
}
}
interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
},
);
+2 -1
View File
@@ -6,9 +6,9 @@ import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { sequelize } from '../data';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config';
export default async () => {
@@ -19,6 +19,7 @@ export default async () => {
await AuthModel.sync();
await EnvModel.sync();
await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try {
// const queryInterface = sequelize.getQueryInterface();
+99 -24
View File
@@ -113,16 +113,56 @@ export default class CronService {
}
}
public async crontabs(params?: {
searchText: string;
page: string;
size: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchText;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
break;
case 'NotReg':
operate = Op.notLike;
break;
case 'In':
query[Op.or] = [
{
[property]: value,
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
query[Op.and] = [
{
[property]: {
[Op.notIn]: value,
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate) {
query[property] = {
[Op.or]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
}
}
}
let query = {};
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) {
const textArray = searchText.split(':');
switch (textArray[0]) {
@@ -166,14 +206,49 @@ export default class CronService {
break;
}
}
}
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sortField: string;
sortType: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const sortField = params?.sortField || '';
const sortType = params?.sortType || '';
const viewQuery = JSON.parse(params?.queryString || '{}');
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatViewSort(order, viewQuery);
if (sortType && sortField) {
order.unshift([sortField, sortType]);
}
let condition: any = {
where: query,
order: [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
],
order: order,
};
if (page && size) {
condition.offset = (page - 1) * size;
@@ -181,7 +256,7 @@ export default class CronService {
}
try {
const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count();
const count = await CrontabModel.count({ where: query });
return { data: result, total: count };
} catch (error) {
throw error;
@@ -298,7 +373,13 @@ export default class CronService {
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
@@ -321,16 +402,10 @@ export default class CronService {
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
+92
View File
@@ -0,0 +1,92 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position * 2
: views[toIndex].position / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
});
return newDoc;
}
}
+2 -2
View File
@@ -245,7 +245,7 @@ export default class NotificationService {
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const { access_token } = await got
const tokenRes: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
@@ -296,7 +296,7 @@ export default class NotificationService {
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
timeout: this.timeout,
retry: 0,
+1 -2
View File
@@ -79,13 +79,12 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await callbacks.onEnd?.(
cp,
endTime,
+4 -4
View File
@@ -1,9 +1,9 @@
FROM node:alpine
FROM python:alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
@@ -29,10 +29,10 @@ RUN set -x \
perl \
openssl \
nginx \
python3 \
nodejs \
jq \
openssh \
py3-pip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+1
View File
@@ -9,6 +9,7 @@ make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
echo
+15 -15
View File
@@ -1,12 +1,12 @@
upstream api {
upstream baseApi {
server 0.0.0.0:5600;
}
upstream public {
upstream publicApi {
server 0.0.0.0:5400;
}
map $http_upgrade $connection_upgrade {
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
}
@@ -16,42 +16,42 @@ server {
root /ql/static/dist;
ssl_session_timeout 5m;
location /api/public {
location QL_BASE_URL/api/public/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://public;
proxy_pass http://publicApi/api/public/;
}
location /api {
location QL_BASE_URL/api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api;
proxy_pass http://baseApi/api/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /open {
location QL_BASE_URL/open/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api;
proxy_pass http://baseApi/open/;
}
gzip on;
gzip_static on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
gzip_http_version 1.0;
location / {
index index.html index.htm;
try_files $uri $uri/ /index.html;
location QL_BASE_URL/ {
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
}
location ~ .*\.(html)$ {
+19 -19
View File
@@ -5,41 +5,41 @@ error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules/*.conf;
events {
worker_connections 1024;
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
server_tokens off;
client_max_body_size 20m;
client_body_buffer_size 20m;
client_max_body_size 20m;
client_body_buffer_size 20m;
keepalive_timeout 65;
keepalive_timeout 65;
sendfile on;
sendfile on;
tcp_nodelay on;
tcp_nodelay on;
ssl_prefer_server_ciphers on;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m;
ssl_session_cache shared:SSL:2m;
gzip on;
gzip_static on;
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
+1
View File
@@ -117,6 +117,7 @@
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0",
+8
View File
@@ -29,6 +29,14 @@ MaxConcurrentNum="5"
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
+1
View File
@@ -31,6 +31,7 @@ copy_dep() {
echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n"
}
+1
View File
@@ -67,6 +67,7 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
+25 -10
View File
@@ -24,12 +24,29 @@ define_program() {
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local current_min=$(date "+%-M")
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
delay_second=$(($(gen_random_num $random_delay_max) + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
@@ -78,10 +95,8 @@ run_nohup() {
run_normal() {
local file_param=$1
define_program "$file_param"
if [[ $file_param == *.js ]]; then
if [[ $# -eq 1 ]]; then
random_delay
fi
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
local time=$(date "+$time_format")
@@ -356,7 +371,7 @@ main() {
fi
time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
+26 -8
View File
@@ -56,7 +56,13 @@
overflow: auto;
height: 100%;
background-color: @component-background;
padding: 16px;
padding: 12px;
}
}
.ql-container-wrapper-has-tab {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
padding-top: 0;
}
}
@@ -136,7 +142,7 @@
flex: 1;
.ant-pro-grid-content-children {
height: calc(100% - 36px);
height: 100%;
> div,
.log-container,
@@ -264,16 +270,24 @@
}
}
.ant-pro-page-container-children-content {
margin: 18px 18px 18px;
}
.ant-pro-basicLayout-content {
margin: 18px;
margin: 12px;
.ant-pro-page-container {
margin: -18px -18px -18px;
margin: -12px;
}
.ant-pro-page-container-warp .ant-page-header {
border-bottom: 1px solid #eee;
}
.ant-pro-page-container-children-content {
margin: 0;
}
}
.ant-pro-global-header {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
}
.ant-menu-item.ant-pro-sider-collapsed-button {
@@ -312,6 +326,10 @@
background-color: #373739;
}
.ant-layout-sider {
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
+4 -5
View File
@@ -186,9 +186,8 @@ export default function (props: any) {
if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
document.title = `${
(config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`;
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
@@ -282,7 +281,7 @@ export default function (props: any) {
shape="square"
size="small"
icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
@@ -304,7 +303,7 @@ export default function (props: any) {
shape="square"
size="small"
icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
-1
View File
@@ -108,7 +108,6 @@ const CronDetailModal = ({
wordWrap: 'on',
}}
onMount={(editor, monaco) => {
console.log(monaco);
editorRef.current = editor;
}}
/>
+67
View File
@@ -106,4 +106,71 @@
.ant-tabs-nav-wrap {
flex: unset !important;
}
.ant-tabs-nav-operations {
position: absolute;
visibility: hidden;
pointer-events: none;
}
.view-more {
margin-left: 32px;
padding: 8px 0;
cursor: pointer;
.ant-tabs-ink-bar {
width: 0;
}
&:hover,
&:focus-visible {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
&.active {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
}
&.more-active {
.ant-tabs-nav-list {
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: unset;
}
.ant-tabs-ink-bar {
width: 0 !important;
}
}
}
}
.view-create-modal-filters {
display: flex;
.ant-space-item:nth-child(3) {
flex: 1;
}
}
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
.view-manage-modal {
.ant-modal-body {
padding-top: 10px;
}
}
+170 -15
View File
@@ -13,6 +13,7 @@ import {
Input,
Popover,
Tabs,
TablePaginationConfig,
} from 'antd';
import {
ClockCircleOutlined,
@@ -28,6 +29,11 @@ import {
PauseCircleOutlined,
FieldTimeOutlined,
PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
CheckOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -40,6 +46,10 @@ import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -124,7 +134,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
),
sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
multiple: 2,
},
},
{
@@ -149,7 +158,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
},
sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
},
},
{
@@ -160,7 +168,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
},
},
{
@@ -347,14 +354,24 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [pageConf, setPageConf] = useState<{ page: number; size: number }>(
{} as any,
);
const [pageConf, setPageConf] = useState<{
page: number;
size: number;
sorter: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const [total, setTotal] = useState<number>();
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState(false);
const [isViewManageModalVisible, setIsViewManageModalVisible] =
useState(false);
const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false);
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
@@ -376,10 +393,21 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCrons = () => {
setLoading(true);
const { page, size, sorter } = pageConf;
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}`;
if (sorter && sorter.field) {
url += `&sortField=${sorter.field}&sortType=${
sorter.order === 'ascend' ? 'ASC' : 'DESC'
}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
filters: viewConf.filters,
sorts: viewConf.sorts,
})}`;
}
request
.get(
`${config.apiPrefix}crons?searchText=${searchText}&page=${pageConf.page}&size=${pageConf.size}`,
)
.get(url)
.then((_data: any) => {
const { data, total } = _data.data;
setValue(
@@ -781,8 +809,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
});
};
const onPageChange = (page: number, pageSize: number | undefined) => {
setPageConf({ page, size: pageSize as number });
const onPageChange = (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setPageConf({ page: current as number, size: pageSize as number, sorter });
localStorage.setItem('pageSize', String(pageSize));
};
@@ -805,16 +838,25 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
if (pageConf.page && pageConf.size) {
getCrons();
}
}, [pageConf]);
}, [pageConf, viewConf]);
useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view);
}
}, [viewConf, enabledCronViews]);
useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
getCronViews();
}, []);
const panelContent = (
@@ -879,7 +921,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
columns={columns}
pagination={{
current: pageConf.page,
onChange: onPageChange,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
@@ -905,13 +946,83 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</>
);
const viewAction = (key: string) => {
switch (key) {
case 'new':
setIsCreateViewModalVisible(true);
break;
case 'manage':
setIsViewManageModalVisible(true);
break;
default:
tabClick(key);
break;
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(2).map((x) => ({
label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span>
{viewConf?.id === x.id && (
<CheckOutlined style={{ color: '#1890ff' }} />
)}
</Space>
),
key: x.id,
icon: <UnorderedListOutlined />,
})),
{
type: 'divider',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
]}
/>
);
const getCronViews = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then((data: any) => {
setCronViews(data.data);
setEnabledCronViews(data.data.filter((x) => !x.isDisabled));
})
.finally(() => {
setLoading(false);
});
};
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setViewConf(view ? view : null);
};
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper"
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
title="定时任务"
extra={[
<Search
@@ -936,11 +1047,32 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
defaultActiveKey="all"
size="small"
tabPosition="top"
className="crontab-view"
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={
<Dropdown
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 200 }}
>
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
<Space>
<DownOutlined />
</Space>
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</Dropdown>
}
onTabClick={tabClick}
>
<Tabs.TabPane tab="全部任务" key="all">
{panelContent}
</Tabs.TabPane>
{[...enabledCronViews].slice(0, 2).map((x) => (
<Tabs.TabPane tab={x.name} key={x.id}>
{panelContent}
</Tabs.TabPane>
))}
</Tabs>
<CronLogModal
visible={isLogModalVisible}
@@ -974,6 +1106,29 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
theme={theme}
isPhone={isPhone}
/>
<ViewCreateModal
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
<ViewManageModal
cronViews={cronViews}
visible={isViewManageModalVisible}
handleCancel={() => {
setIsViewManageModalVisible(false);
}}
cronViewChange={(data) => {
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
</PageContainer>
);
};
+263
View File
@@ -0,0 +1,263 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Statistic,
Button,
Space,
Select,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
const PROPERTIES = [
{ name: '命令', value: 'command' },
{ name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' },
];
const OPERATIONS = [
{ name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' },
{ name: '不属于', value: 'Nin' },
// { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' },
// { name: '不为空', value: 'NotNull' },
];
const SORTTYPES = [
{ name: '顺序', value: 'ASC' },
{ name: '倒序', value: 'DESC' },
];
const STATUS = [
{ name: '运行中', value: 0 },
{ name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 },
];
const ViewCreateModal = ({
view,
handleCancel,
visible,
}: {
view?: any;
visible: boolean;
handleCancel: (param?: any) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>();
const handleOk = async (values: any) => {
setLoading(true);
const method = view ? 'put' : 'post';
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: view ? { ...values, id: view.id } : values,
},
);
if (code !== 200) {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
};
useEffect(() => {
if (!view) {
form.resetFields();
}
form.setFieldsValue(
view || {
filters: [{ property: 'command', operation: 'Reg' }],
},
);
}, [view, visible]);
const operationElement = (
<Select
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
{OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const propertyElement = (props: any) => {
return (
<Select style={{ width: 120 }}>
{props.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
};
const typeElement = (
<Select style={{ width: 120 }}>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const statusElement = (
<Select mode="multiple" allowClear placeholder="请选择状态">
{STATUS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
visible={visible}
forceRender
width={580}
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="env_modal">
<Form.Item
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称' }]}
>
<Input placeholder="请输入视图名称" />
</Form.Item>
<Form.List name="filters">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-filters" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES)}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Form.Item>
))}
<Form.Item>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
<Form.List name="sorts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-filters" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES)}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
};
export default ViewCreateModal;
+269
View File
@@ -0,0 +1,269 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Space,
Table,
Tag,
Typography,
Button,
Switch,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import ViewCreateModal from './viewCreateModal';
const { Text } = Typography;
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const ViewManageModal = ({
cronViews,
handleCancel,
visible,
cronViewChange,
}: {
cronViews: any[];
visible: boolean;
handleCancel: () => void;
cronViewChange: (data?: any) => void;
}) => {
const columns: any = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '显示',
key: 'isDisabled',
dataIndex: 'isDisabled',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
return (
<Switch
checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)}
/>
);
},
},
{
title: '操作',
key: 'action',
width: 140,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle">
<a onClick={() => editView(record, index)}>
<EditOutlined />
</a>
<a onClick={() => deleteView(record, index)}>
<DeleteOutlined />
</a>
</Space>
);
},
},
];
const [list, setList] = useState<any[]>([]);
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState<boolean>(false);
const [editedView, setEditedView] = useState<any>(null);
const editView = (record: any, index: number) => {
setEditedView(record);
setIsCreateViewModalVisible(true);
};
const deleteView = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
cronViewChange();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list);
cronViewChange();
} else {
message.error(data);
}
});
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
})
.then((data: any) => {
if (data.code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setList(newData);
cronViewChange();
} else {
message.error(data);
}
});
},
[list],
);
useEffect(() => {
setList(cronViews);
}, [cronViews]);
return (
<Modal
title="视图管理"
visible={visible}
centered
width={620}
onCancel={() => handleCancel()}
className="view-manage-modal"
forceRender
footer={false}
maskClosable={false}
>
<Space
style={{
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 10,
}}
>
<Button
key="2"
type="primary"
onClick={() => setIsCreateViewModalVisible(true)}
>
</Button>
</Space>
<DndProvider backend={HTML5Backend}>
<Table
bordered
columns={columns}
pagination={false}
dataSource={list}
rowKey="id"
size="middle"
style={{ marginBottom: 20 }}
components={components}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<ViewCreateModal
view={editedView}
visible={isCreateViewModalVisible}
handleCancel={(data) => {
cronViewChange(data);
setIsCreateViewModalVisible(false);
}}
/>
</Modal>
);
};
export default ViewManageModal;
+1 -1
View File
@@ -444,7 +444,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
return (
<PageContainer
className="ql-container-wrapper dependence-wrapper"
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab"
title="依赖管理"
extra={[
<Search
+1 -1
View File
@@ -313,7 +313,7 @@ const Setting = ({
return (
<PageContainer
className="ql-container-wrapper"
className="ql-container-wrapper ql-container-wrapper-has-tab"
title="系统设置"
header={{
style: headerStyle,
+2 -2
View File
@@ -181,8 +181,8 @@ export function getTableScroll({
id,
}: { extraHeight?: number; id?: string } = {}) {
if (typeof extraHeight == 'undefined') {
// 47 + 40 + 10 + 24
extraHeight = 121;
// 47 + 40 + 12
extraHeight = 99;
}
let tHeader = null;
if (id) {
+7 -6
View File
@@ -1,7 +1,8 @@
export const version = '2.13.9';
export const changeLogLink = 'https://t.me/jiao_long/324';
export const changeLog = `2.13.9 版本说明
1. 修改定时任务分页功能,加快每页数据获取
2. 定时任务增加每页数据可设置为最大,使任务一页展示(数据获取速度也会变慢)
3. favicon修改😀😀
export const version = '2.14.0';
export const changeLogLink = 'https://t.me/jiao_long/325';
export const changeLog = `2.14.0 版本说明
1. 定时任务自定义视图功能来啦,支持自定义筛选条件、排序规则等
2. 配置文件可设置需要延迟的脚本后缀,默认只有js脚本随机延迟执行,感谢 https://github.com/cddjr
3. 修复定时任务搜索及搜索分页
4. 其他bug修复
`;