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