mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-23 23:06:06 +08:00
支持定时任务视图筛选条件关系切换
This commit is contained in:
parent
c72abd29ec
commit
7038e15ad2
|
@ -30,6 +30,7 @@ export default (app: Router) => {
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
@ -51,6 +52,7 @@ export default (app: Router) => {
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
sorts: Joi.array().optional().allow(null),
|
sorts: Joi.array().optional().allow(null),
|
||||||
filters: Joi.array().optional(),
|
filters: Joi.array().optional(),
|
||||||
|
filterRelation: Joi.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|
|
@ -7,7 +7,8 @@ interface SortType {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterType {
|
interface FilterType {
|
||||||
type: 'or' | 'and';
|
property: string;
|
||||||
|
operation: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export class CrontabView {
|
||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
filters?: FilterType[];
|
filters?: FilterType[];
|
||||||
sorts?: SortType[];
|
sorts?: SortType[];
|
||||||
|
filterRelation?: 'and' | 'or';
|
||||||
|
|
||||||
constructor(options: CrontabView) {
|
constructor(options: CrontabView) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
|
@ -26,6 +28,7 @@ export class CrontabView {
|
||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
this.filters = options.filters;
|
this.filters = options.filters;
|
||||||
this.sorts = options.sorts;
|
this.sorts = options.sorts;
|
||||||
|
this.filterRelation = options.filterRelation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,5 +46,6 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
||||||
isDisabled: DataTypes.NUMBER,
|
isDisabled: DataTypes.NUMBER,
|
||||||
filters: DataTypes.JSON,
|
filters: DataTypes.JSON,
|
||||||
sorts: DataTypes.JSON,
|
sorts: DataTypes.JSON,
|
||||||
|
filterRelation: { type: DataTypes.STRING, defaultValue: 'and' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,6 +28,7 @@ export class Subscription {
|
||||||
extensions?: string;
|
extensions?: string;
|
||||||
sub_before?: string;
|
sub_before?: string;
|
||||||
sub_after?: string;
|
sub_after?: string;
|
||||||
|
proxy?: string;
|
||||||
|
|
||||||
constructor(options: Subscription) {
|
constructor(options: Subscription) {
|
||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
|
@ -54,6 +55,7 @@ export class Subscription {
|
||||||
this.extensions = options.extensions;
|
this.extensions = options.extensions;
|
||||||
this.sub_before = options.sub_before;
|
this.sub_before = options.sub_before;
|
||||||
this.sub_after = options.sub_after;
|
this.sub_after = options.sub_after;
|
||||||
|
this.proxy = options.proxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,5 +104,6 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
||||||
log_path: DataTypes.STRING,
|
log_path: DataTypes.STRING,
|
||||||
schedule_type: DataTypes.STRING,
|
schedule_type: DataTypes.STRING,
|
||||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
alias: { type: DataTypes.STRING, unique: 'alias' },
|
||||||
|
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,8 +18,8 @@ export default async () => {
|
||||||
await AppModel.sync();
|
await AppModel.sync();
|
||||||
await AuthModel.sync();
|
await AuthModel.sync();
|
||||||
await EnvModel.sync();
|
await EnvModel.sync();
|
||||||
await SubscriptionModel.sync();
|
await SubscriptionModel.sync({ alter: true });
|
||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync({ alter: true });
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
// const queryInterface = sequelize.getQueryInterface();
|
// const queryInterface = sequelize.getQueryInterface();
|
||||||
|
|
|
@ -116,8 +116,9 @@ export default class CronService {
|
||||||
|
|
||||||
private formatViewQuery(query: any, viewQuery: any) {
|
private formatViewQuery(query: any, viewQuery: any) {
|
||||||
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
if (viewQuery.filters && viewQuery.filters.length > 0) {
|
||||||
if (!query[Op.and]) {
|
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
|
||||||
query[Op.and] = [];
|
if (!query[primaryOperate]) {
|
||||||
|
query[primaryOperate] = [];
|
||||||
}
|
}
|
||||||
for (const col of viewQuery.filters) {
|
for (const col of viewQuery.filters) {
|
||||||
const { property, value, operation } = col;
|
const { property, value, operation } = col;
|
||||||
|
@ -166,7 +167,7 @@ export default class CronService {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
query[Op.and].push(q);
|
query[primaryOperate].push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
||||||
import { initPosition } from '../data/env';
|
import {
|
||||||
|
initPosition,
|
||||||
|
maxPosition,
|
||||||
|
minPosition,
|
||||||
|
stepPosition,
|
||||||
|
} from '../data/env';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronViewService {
|
export default class CronViewService {
|
||||||
|
@ -16,6 +21,8 @@ export default class CronViewService {
|
||||||
position = position / 2;
|
position = position / 2;
|
||||||
const tab = new CrontabView({ ...payload, position });
|
const tab = new CrontabView({ ...payload, position });
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
|
|
||||||
|
await this.checkPosition(tab.position!);
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +69,22 @@ export default class CronViewService {
|
||||||
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkPosition(position: number) {
|
||||||
|
const precisionPosition = parseFloat(position.toPrecision(16));
|
||||||
|
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
||||||
|
const envs = await this.list();
|
||||||
|
let position = initPosition;
|
||||||
|
for (const env of envs) {
|
||||||
|
position = position - stepPosition;
|
||||||
|
await this.updateDb({ id: env.id, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrecisionPosition(position: number): number {
|
||||||
|
return parseFloat(position.toPrecision(16));
|
||||||
|
}
|
||||||
|
|
||||||
public async move({
|
public async move({
|
||||||
id,
|
id,
|
||||||
fromIndex,
|
fromIndex,
|
||||||
|
@ -85,8 +108,10 @@ export default class CronViewService {
|
||||||
}
|
}
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
id,
|
||||||
position: targetPosition,
|
position: this.getPrecisionPosition(targetPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.checkPosition(targetPosition);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import ScheduleService from './schedule';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import { promiseExec } from '../config/util';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SystemService {
|
export default class SystemService {
|
||||||
|
@ -88,9 +87,13 @@ export default class SystemService {
|
||||||
let lastVersion = '';
|
let lastVersion = '';
|
||||||
let lastLog = '';
|
let lastLog = '';
|
||||||
try {
|
try {
|
||||||
const lastVersionFileContent = await promiseExec(
|
const result = await got.get(
|
||||||
`curl ${config.lastVersionFile}?t=${Date.now()}`,
|
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||||
|
{
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
const lastVersionFileContent = result.body;
|
||||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||||
lastLog = lastVersionFileContent.match(logRegx)
|
lastLog = lastVersionFileContent.match(logRegx)
|
||||||
? lastVersionFileContent.match(logRegx)![1]
|
? lastVersionFileContent.match(logRegx)![1]
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"p-queue": "7.2.0",
|
"p-queue": "7.2.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.25.3",
|
"sequelize": "^6.25.5",
|
||||||
"serve-handler": "^6.1.3",
|
"serve-handler": "^6.1.3",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
|
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|
|
@ -179,6 +179,6 @@ tr.drop-over-upward td {
|
||||||
|
|
||||||
.view-filters-container.active {
|
.view-filters-container.active {
|
||||||
.filter-item > div > .ant-form-item-control {
|
.filter-item > div > .ant-form-item-control {
|
||||||
padding-left: 40px;
|
margin-left: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -387,7 +387,7 @@ const Crontab = () => {
|
||||||
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
|
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
|
||||||
const [moreMenuActive, setMoreMenuActive] = useState(false);
|
const [moreMenuActive, setMoreMenuActive] = useState(false);
|
||||||
const tableRef = useRef<any>();
|
const tableRef = useRef<any>();
|
||||||
const tableScrollHeight = useTableScrollHeight(tableRef)
|
const tableScrollHeight = useTableScrollHeight(tableRef);
|
||||||
|
|
||||||
const goToScriptManager = (record: any) => {
|
const goToScriptManager = (record: any) => {
|
||||||
const cmd = record.command.split(' ') as string[];
|
const cmd = record.command.split(' ') as string[];
|
||||||
|
@ -414,7 +414,8 @@ const Crontab = () => {
|
||||||
const getCrons = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { page, size, sorter, filters } = pageConf;
|
const { page, size, sorter, filters } = pageConf;
|
||||||
let url = `${config.apiPrefix
|
let url = `${
|
||||||
|
config.apiPrefix
|
||||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||||
filters,
|
filters,
|
||||||
)}`;
|
)}`;
|
||||||
|
@ -428,6 +429,7 @@ const Crontab = () => {
|
||||||
url += `&queryString=${JSON.stringify({
|
url += `&queryString=${JSON.stringify({
|
||||||
filters: viewConf.filters,
|
filters: viewConf.filters,
|
||||||
sorts: viewConf.sorts,
|
sorts: viewConf.sorts,
|
||||||
|
filterRelation: viewConf.filterRelation || 'and',
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
request
|
request
|
||||||
|
@ -582,7 +584,8 @@ const Crontab = () => {
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
|
`${config.apiPrefix}crons/${
|
||||||
|
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
|
@ -625,7 +628,8 @@ const Crontab = () => {
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
|
`${config.apiPrefix}crons/${
|
||||||
|
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
|
@ -999,7 +1003,11 @@ const Crontab = () => {
|
||||||
<div ref={tableRef}>
|
<div ref={tableRef}>
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5 }}
|
||||||
|
onClick={delCrons}
|
||||||
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
|
||||||
const PROPERTIES = [
|
const PROPERTIES = [
|
||||||
{ name: '命令', value: 'command' },
|
{ name: '命令', value: 'command' },
|
||||||
|
@ -42,6 +43,11 @@ const STATUS = [
|
||||||
{ name: '已禁用', value: 2 },
|
{ name: '已禁用', value: 2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
enum ViewFilterRelation {
|
||||||
|
'and' = '且',
|
||||||
|
'or' = '或',
|
||||||
|
}
|
||||||
|
|
||||||
const ViewCreateModal = ({
|
const ViewCreateModal = ({
|
||||||
view,
|
view,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
|
@ -53,10 +59,11 @@ const ViewCreateModal = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [operationMap, setOperationMap] = useState<any>();
|
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
values.filterRelation = filterRelation;
|
||||||
const method = view ? 'put' : 'post';
|
const method = view ? 'put' : 'post';
|
||||||
try {
|
try {
|
||||||
const { code, data } = await request[method](
|
const { code, data } = await request[method](
|
||||||
|
@ -87,12 +94,7 @@ const ViewCreateModal = ({
|
||||||
}, [view, visible]);
|
}, [view, visible]);
|
||||||
|
|
||||||
const operationElement = (
|
const operationElement = (
|
||||||
<Select
|
<Select style={{ width: 100 }}>
|
||||||
style={{ width: 100 }}
|
|
||||||
onChange={() => {
|
|
||||||
setOperationMap({});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OPERATIONS.map((x) => (
|
{OPERATIONS.map((x) => (
|
||||||
<Select.Option key={x.name} value={x.value}>
|
<Select.Option key={x.name} value={x.value}>
|
||||||
{x.name}
|
{x.name}
|
||||||
|
@ -164,9 +166,13 @@ const ViewCreateModal = ({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.List name="filters">
|
<Form.List name="filters">
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<div style={{ position: 'relative' }} className={`view-filters-container ${fields.length > 1 ? 'active' : ''}`}>
|
<div
|
||||||
{
|
style={{ position: 'relative' }}
|
||||||
fields.length > 1 && (
|
className={`view-filters-container ${
|
||||||
|
fields.length > 1 ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{fields.length > 1 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -176,7 +182,7 @@ const ViewCreateModal = ({
|
||||||
borderRight: 'none',
|
borderRight: 'none',
|
||||||
height: 56 * (fields.length - 1),
|
height: 56 * (fields.length - 1),
|
||||||
top: 46,
|
top: 46,
|
||||||
left: 15
|
left: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
@ -186,16 +192,22 @@ const ViewCreateModal = ({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
translate: '-50% -50%',
|
translate: '-50% -50%',
|
||||||
padding: '0 5px',
|
padding: '0 0 0 3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterRelation(
|
||||||
|
filterRelation === 'and' ? 'or' : 'and',
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
或
|
<span>{ViewFilterRelation[filterRelation]}</span>
|
||||||
|
<IconFont type="ql-icon-d-caret" />
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
<div>
|
<div>
|
||||||
{fields.map(({ key, name, ...restField }, index) => (
|
{fields.map(({ key, name, ...restField }, index) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -203,9 +215,12 @@ const ViewCreateModal = ({
|
||||||
key={key}
|
key={key}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
required
|
required
|
||||||
className='filter-item'
|
className="filter-item"
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
className="view-create-modal-filters"
|
||||||
|
align="baseline"
|
||||||
>
|
>
|
||||||
<Space className="view-create-modal-filters" align="baseline">
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'property']}
|
name={[name, 'property']}
|
||||||
|
@ -241,7 +256,9 @@ const ViewCreateModal = ({
|
||||||
))}
|
))}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<a
|
<a
|
||||||
onClick={() => add({ property: 'command', operation: 'Reg' })}
|
onClick={() =>
|
||||||
|
add({ property: 'command', operation: 'Reg' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
新增筛选条件
|
新增筛选条件
|
||||||
|
|
Loading…
Reference in New Issue
Block a user