更新新建文件订阅

This commit is contained in:
whyour 2022-05-15 15:25:23 +08:00
parent 5523d537dc
commit f6a122e5ea
7 changed files with 344 additions and 84 deletions

View File

@ -29,7 +29,7 @@ export default (app: Router) => {
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional(),
intervalSchedule: Joi.object().optional(),
interval_schedule: Joi.object().optional(),
name: Joi.string().optional(),
url: Joi.string().required(),
whitelist: Joi.string().optional(),
@ -158,7 +158,7 @@ export default (app: Router) => {
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional(),
intervalSchedule: Joi.object().optional(),
interval_schedule: Joi.object().optional(),
name: Joi.string().optional(),
url: Joi.string().required(),
whitelist: Joi.string().optional(),

View File

@ -2,13 +2,14 @@ import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { SimpleIntervalSchedule } from 'toad-scheduler';
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
export class Subscription {
id?: number;
name?: string;
type?: 'public-repo' | 'private-repo' | 'file';
schedule_type?: 'crontab' | 'interval';
schedule?: string;
intervalSchedule?: SimpleIntervalSchedule;
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
url?: string;
whitelist?: string;
blacklist?: string;
@ -20,7 +21,7 @@ export class Subscription {
| { private_key: string }
| { username: string; password: string };
pid?: number;
isDisabled?: 1 | 0;
is_disabled?: 1 | 0;
log_path?: string;
alias: string;
command?: string;
@ -30,6 +31,10 @@ export class Subscription {
this.name = options.name;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
this.whitelist = options.whitelist;
this.blacklist = options.blacklist;
@ -39,11 +44,11 @@ export class Subscription {
this.pull_type = options.pull_type;
this.pull_option = options.pull_option;
this.pid = options.pid;
this.isDisabled = options.isDisabled;
this.is_disabled = options.is_disabled;
this.log_path = options.log_path;
this.schedule_type = options.schedule_type;
this.alias = options.alias;
this.intervalSchedule = options.intervalSchedule;
this.interval_schedule = options.interval_schedule;
}
}
@ -72,7 +77,7 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
unique: 'compositeIndex',
type: DataTypes.STRING,
},
intervalSchedule: {
interval_schedule: {
unique: 'compositeIndex',
type: DataTypes.JSON,
},
@ -85,7 +90,7 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
pull_type: DataTypes.STRING,
pull_option: DataTypes.JSON,
pid: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
is_disabled: DataTypes.NUMBER,
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' },

View File

@ -100,10 +100,11 @@ export default class SubscriptionService {
needCreate && this.scheduleService.createCronTask(doc as any);
} else {
this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule as any;
needCreate &&
this.scheduleService.createIntervalTask(
doc as any,
doc.intervalSchedule as SimpleIntervalSchedule,
{ [type]: value } as SimpleIntervalSchedule,
);
}
}

View File

@ -73,7 +73,7 @@ enum OperationPath {
const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [
{
title: '任务名',
title: '',
dataIndex: 'name',
key: 'name',
width: 150,
@ -127,7 +127,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
},
},
{
title: '任务',
title: '命令',
dataIndex: 'command',
key: 'command',
width: 250,
@ -152,7 +152,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
},
},
{
title: '任务定时',
title: '定时规则',
dataIndex: 'schedule',
key: 'schedule',
width: 110,

View File

@ -10,6 +10,7 @@ import {
Menu,
Typography,
Input,
Tooltip,
} from 'antd';
import {
ClockCircleOutlined,
@ -20,6 +21,9 @@ import {
EditOutlined,
StopOutlined,
DeleteOutlined,
FileTextOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@ -28,6 +32,7 @@ import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less';
import SubscriptionLogModal from './logModal';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@ -39,10 +44,17 @@ export enum SubscriptionStatus {
'queued',
}
export enum IntervalSchedule {
'days' = '天',
'hours' = '时',
'minutes' = '分',
'seconds' = '秒',
}
const Subscription = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [
{
title: '订阅名',
title: '',
dataIndex: 'name',
key: 'name',
width: 150,
@ -53,39 +65,35 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
},
},
{
title: '订阅',
dataIndex: 'command',
key: 'command',
width: 250,
title: '链接',
dataIndex: 'url',
key: 'url',
align: 'center' as const,
render: (text: string, record: any) => {
return (
<Paragraph
style={{
wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
{text}
</Paragraph>
);
},
sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2,
},
},
{
title: '订阅定时',
dataIndex: 'schedule',
key: 'schedule',
width: 110,
title: '分支',
dataIndex: 'branch',
key: 'branch',
width: 130,
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
},
{
title: '定时规则',
width: 180,
align: 'center' as const,
render: (text: string, record: any) => {
const { type, value } = record.interval_schedule;
return (
<span>
{record.schedule_type === 'interval'
? `${value}${(IntervalSchedule as any)[type]}`
: record.schedule}
</span>
);
},
},
{
@ -93,7 +101,7 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 85,
width: 110,
filters: [
{
text: '运行中',
@ -148,10 +156,45 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
title: '操作',
key: 'action',
align: 'center' as const,
width: 100,
width: 130,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
<Space size="middle">
{record.status === SubscriptionStatus.idle && (
<Tooltip title={isPc ? '运行' : ''}>
<a
onClick={(e) => {
e.stopPropagation();
runSubscription(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
)}
{record.status !== SubscriptionStatus.idle && (
<Tooltip title={isPc ? '停止' : ''}>
<a
onClick={(e) => {
e.stopPropagation();
stopSubsciption(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={(e) => {
e.stopPropagation();
setLogSubscription({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} />
</Space>
);
@ -168,23 +211,78 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
const runSubscription = (record: any, index: number) => {
Modal.confirm({
title: '确认运行',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
status: SubscriptionStatus.running,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
}
const stopSubsciption = (record: any, index: number) => {
Modal.confirm({
title: '确认停止',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
pid: null,
status: SubscriptionStatus.idle,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const getSubscriptions = () => {
@ -438,6 +536,13 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
handleCancel={handleCancel}
subscription={editedSubscription}
/>
<SubscriptionLogModal
visible={isLogModalVisible}
handleCancel={() => {
setIsLogModalVisible(false);
}}
cron={logSubscription}
/>
</PageContainer>
);
};

View File

@ -0,0 +1,158 @@
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';
import {
Loading3QuartersOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const { Countdown } = Statistic;
const SubscriptionLogModal = ({
cron,
handleCancel,
visible,
data,
logUrl,
}: {
cron?: any;
visible: boolean;
handleCancel: () => void;
data?: string;
logUrl?: string;
}) => {
const [value, setValue] = useState<string>('启动中...');
const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => {
if (isFirst) {
setLoading(true);
}
request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => {
if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string;
setValue(log || '暂无日志');
setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'),
);
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
setTimeout(() => {
getCronLog();
}, 2000);
}
if (
log &&
log.includes('重启面板') &&
cron.status === CrontabStatus.running
) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 30000);
}
}
})
.finally(() => {
if (isFirst) {
setLoading(false);
}
});
};
const cancel = () => {
localStorage.removeItem('logCron');
handleCancel();
};
const titleElement = () => {
return (
<>
{(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</>
);
};
useEffect(() => {
if (cron && cron.id && visible) {
getCronLog(true);
}
}, [cron, visible]);
useEffect(() => {
if (data) {
setValue(data);
}
}, [data]);
useEffect(() => {
setIsPhone(document.body.clientWidth < 768);
}, []);
return (
<Modal
title={titleElement()}
visible={visible}
centered
className="log-modal"
bodyStyle={{
minHeight: '300px',
}}
forceRender
onOk={() => cancel()}
onCancel={() => cancel()}
footer={[
<Button type="primary" onClick={() => cancel()}>
</Button>,
]}
>
{loading ? (
<PageLoading />
) : (
<pre
style={
isPhone
? {
fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83,
}
: {}
}
>
{value}
</pre>
)}
</Modal>
);
};
export default SubscriptionLogModal;

View File

@ -3,7 +3,6 @@ import { Modal, message, InputNumber, Form, Radio, Select, Input } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cron_parser from 'cron-parser';
import EditableTagGroup from '@/components/tag';
const { Option } = Select;
const repoUrlRegx = /[^\/\:]+\/[^\/]+(?=\.git)/;
@ -39,9 +38,7 @@ const SubscriptionModal = ({
},
);
if (code === 200) {
message.success(
subscription ? '更新Subscription成功' : '新建Subscription成功',
);
message.success(subscription ? '更新订阅成功' : '新建订阅成功');
} else {
message.error(data);
}
@ -108,21 +105,18 @@ const SubscriptionModal = ({
const [intervalNumber, setIntervalNumber] = useState<number>();
const intervalTypeChange = (e) => {
setIntervalType(e.target.value);
onChange?.({ [e.target.value]: intervalNumber });
onChange?.({ type: e.target.value, value: intervalNumber });
};
const numberChange = (value: number) => {
setIntervalNumber(value);
onChange?.({ [intervalType]: value });
onChange?.({ type: intervalType, value });
};
useEffect(() => {
if (value) {
const key = Object.keys(value)[0];
if (key) {
setIntervalType(key);
setIntervalNumber(value[key]);
}
setIntervalType(value.type);
setIntervalNumber(value.value);
}
}, [value]);
return (
@ -131,11 +125,11 @@ const SubscriptionModal = ({
addonBefore="每"
precision={0}
min={1}
defaultValue={intervalNumber}
value={intervalNumber}
style={{ width: 'calc(100% - 58px)' }}
onChange={numberChange}
/>
<Select defaultValue={intervalType} onChange={intervalTypeChange}>
<Select value={intervalType} onChange={intervalTypeChange}>
<Option value="days"></Option>
<Option value="hours"></Option>
<Option value="minutes"></Option>
@ -176,10 +170,10 @@ const SubscriptionModal = ({
};
useEffect(() => {
form.resetFields();
setType('public-repo');
setScheduleType('crontab');
setPullType('ssh-key');
form.setFieldsValue(subscription || {});
setType((subscription && subscription.type) || 'public-repo');
setScheduleType((subscription && subscription.schedule_type) || 'crontab');
setPullType((subscription && subscription.pull_type) || 'ssh-key');
}, [subscription, visible]);
return (
@ -201,14 +195,9 @@ const SubscriptionModal = ({
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
name="form_in_modal"
initialValues={subscription}
layout="vertical"
>
<Form.Item name="name" label="别名">
<Input placeholder="请输入订阅别名" />
<Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称">
<Input placeholder="请输入订阅名" />
</Form.Item>
<Form.Item
name="type"
@ -230,7 +219,9 @@ const SubscriptionModal = ({
{ pattern: type === 'file' ? fileUrlRegx : repoUrlRegx },
]}
>
<Input
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入订阅链接"
onPaste={onUrlChange}
onChange={onUrlChange}
@ -281,7 +272,7 @@ const SubscriptionModal = ({
</Radio.Group>
</Form.Item>
<Form.Item
name={scheduleType === 'crontab' ? 'schedule' : 'intervalSchedule'}
name={scheduleType === 'crontab' ? 'schedule' : 'interval_schedule'}
label="定时规则"
rules={[
{ required: true },