定时任务增加分组

This commit is contained in:
whyour 2022-08-27 14:50:27 +08:00
parent 9e997410ab
commit f8f63890e5
8 changed files with 622 additions and 1 deletions

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();
@ -9,6 +10,96 @@ const route = Router();
export default (app: Router) => {
app.use('/crons', route);
route.get(
'/views',
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.string().required(),
filters: Joi.string().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.string().optional(),
filters: Joi.string().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.get(
'/',
celebrate({

47
back/data/cronView.ts Normal file
View File

@ -0,0 +1,47 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
interface SortType {
type: 'ASD' | '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;
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,
},
);

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();

82
back/services/cronView.ts Normal file
View File

@ -0,0 +1,82 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
const tab = new CrontabView(payload);
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({});
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;
}
}

View File

@ -106,4 +106,23 @@
.ant-tabs-nav-wrap {
flex: unset !important;
}
.view-more {
margin-left: 20px;
padding: 8px 0;
cursor: pointer;
.ant-tabs-ink-bar {
width: 0;
}
&:hover,
&:focus-visible {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
}
}

View File

@ -28,6 +28,10 @@ import {
PauseCircleOutlined,
FieldTimeOutlined,
PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@ -40,6 +44,8 @@ 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';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@ -355,6 +361,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
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 goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
@ -909,6 +919,49 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
</>
);
const viewAction = (key: string) => {
switch (key) {
case 'new':
setIsCreateViewModalVisible(true);
break;
case 'manage':
setIsViewManageModalVisible(true);
break;
default:
break;
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
{
label: 'Clicking me will not close the menu.',
key: '1',
icon: <UnorderedListOutlined />,
},
{
type: 'divider',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
]}
/>
);
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper"
@ -937,6 +990,17 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
size="small"
tabPosition="top"
className="crontab-view"
tabBarExtraContent={
<Dropdown overlay={menu} trigger={['click']}>
<div className="view-more">
<Space>
<DownOutlined />
</Space>
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</Dropdown>
}
>
<Tabs.TabPane tab="全部任务" key="all">
{panelContent}
@ -974,6 +1038,18 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
theme={theme}
isPhone={isPhone}
/>
<ViewCreateModal
visible={isCreateViewModalVisible}
handleCancel={() => {
setIsCreateViewModalVisible(false);
}}
/>
<ViewManageModal
visible={isViewManageModalVisible}
handleCancel={() => {
setIsViewManageModalVisible(false);
}}
/>
</PageContainer>
);
};

View File

@ -0,0 +1,82 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const ViewCreateModal = ({
view,
handleCancel,
visible,
}: {
view?: any;
visible: boolean;
handleCancel: (param?: string) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
const { value, name } = values;
const method = view ? 'put' : 'post';
let payload;
if (!view) {
payload = [{ value, name }];
} else {
payload = { ...values, id: view.id };
}
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: payload,
},
);
if (code !== 200) {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [view, visible]);
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
visible={visible}
forceRender
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" initialValues={view}>
<Form.Item
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称' }]}
>
<Input placeholder="请输入视图名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default ViewCreateModal;

View File

@ -0,0 +1,223 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Modal, message, Space, Table, Tag, Typography } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import Paragraph from 'antd/lib/skeleton/Paragraph';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
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 = ({
handleCancel,
visible,
}: {
visible: boolean;
handleCancel: () => void;
}) => {
const columns: any = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '显示',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 70,
render: (text: string, record: any, index: number) => {
return <Space size="middle" style={{ cursor: 'text' }}></Space>;
},
},
{
title: '操作',
key: 'action',
width: 120,
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 [loading, setLoading] = useState<any>(true);
const editView = (record: any, index: number) => {
// setEditedEnv(record);
// setIsModalVisible(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('删除成功');
const result = [...list];
result.splice(index, 1);
setList(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const getCronViews = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then((data: any) => {
console.log(data);
})
.finally(() => {
setLoading(false);
});
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setList(newData);
} else {
message.error(data);
}
});
},
[list],
);
useEffect(() => {
getCronViews();
}, []);
return (
<Modal
title="视图管理"
visible={visible}
centered
onCancel={() => handleCancel()}
className="view-manage-modal"
forceRender
footer={false}
>
{loading ? (
<PageLoading />
) : (
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
pagination={false}
dataSource={list}
rowKey="id"
size="middle"
components={components}
loading={loading}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
)}
</Modal>
);
};
export default ViewManageModal;