From f6a122e5ea2e406709425b6f50b33b2d1317aec5 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 15 May 2022 15:25:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=B0=E5=BB=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=AE=A2=E9=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/api/subscription.ts | 4 +- back/data/subscription.ts | 17 ++- back/services/subscription.ts | 3 +- src/pages/crontab/index.tsx | 6 +- src/pages/subscription/index.tsx | 195 +++++++++++++++++++++------- src/pages/subscription/logModal.tsx | 158 ++++++++++++++++++++++ src/pages/subscription/modal.tsx | 45 +++---- 7 files changed, 344 insertions(+), 84 deletions(-) create mode 100644 src/pages/subscription/logModal.tsx diff --git a/back/api/subscription.ts b/back/api/subscription.ts index 60996d09..28fcdd12 100644 --- a/back/api/subscription.ts +++ b/back/api/subscription.ts @@ -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(), diff --git a/back/data/subscription.ts b/back/data/subscription.ts index 20a9ac8a..25b765b8 100644 --- a/back/data/subscription.ts +++ b/back/data/subscription.ts @@ -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( unique: 'compositeIndex', type: DataTypes.STRING, }, - intervalSchedule: { + interval_schedule: { unique: 'compositeIndex', type: DataTypes.JSON, }, @@ -85,7 +90,7 @@ export const SubscriptionModel = sequelize.define( 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' }, diff --git a/back/services/subscription.ts b/back/services/subscription.ts index b32f5b43..889fabe6 100644 --- a/back/services/subscription.ts +++ b/back/services/subscription.ts @@ -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, ); } } diff --git a/src/pages/crontab/index.tsx b/src/pages/crontab/index.tsx index 2a52ca96..1015e5cf 100644 --- a/src/pages/crontab/index.tsx +++ b/src/pages/crontab/index.tsx @@ -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, diff --git a/src/pages/subscription/index.tsx b/src/pages/subscription/index.tsx index 5eac2df0..4bbbf087 100644 --- a/src/pages/subscription/index.tsx +++ b/src/pages/subscription/index.tsx @@ -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 ( - - {text} - - ); - }, 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 ( + + {record.schedule_type === 'interval' + ? `每${value}${(IntervalSchedule as any)[type]}` + : record.schedule} + + ); }, }, { @@ -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 ( + {record.status === SubscriptionStatus.idle && ( + + { + e.stopPropagation(); + runSubscription(record, index); + }} + > + + + + )} + {record.status !== SubscriptionStatus.idle && ( + + { + e.stopPropagation(); + stopSubsciption(record, index); + }} + > + + + + )} + + { + e.stopPropagation(); + setLogSubscription({ ...record, timestamp: Date.now() }); + }} + > + + + ); @@ -168,23 +211,78 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => { const [pageSize, setPageSize] = useState(20); const [tableScrollHeight, setTableScrollHeight] = useState(); const [searchValue, setSearchValue] = useState(''); + const [isLogModalVisible, setIsLogModalVisible] = useState(false); + const [logSubscription, setLogSubscription] = useState(); - 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: ( + <> + 确认运行定时任务{' '} + + {record.name} + {' '} + 吗 + + ), + 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: ( + <> + 确认停止定时任务{' '} + + {record.name} + {' '} + 吗 + + ), + 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} /> + { + setIsLogModalVisible(false); + }} + cron={logSubscription} + /> ); }; diff --git a/src/pages/subscription/logModal.tsx b/src/pages/subscription/logModal.tsx new file mode 100644 index 00000000..338793c8 --- /dev/null +++ b/src/pages/subscription/logModal.tsx @@ -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('启动中...'); + const [loading, setLoading] = useState(true); + const [executing, setExecuting] = useState(true); + const [isPhone, setIsPhone] = useState(false); + const [theme, setTheme] = useState(''); + + 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: ( + + 系统将在 + + 秒后自动刷新 + + ), + duration: 10, + }); + setTimeout(() => { + window.location.reload(); + }, 30000); + } + } + }) + .finally(() => { + if (isFirst) { + setLoading(false); + } + }); + }; + + const cancel = () => { + localStorage.removeItem('logCron'); + handleCancel(); + }; + + const titleElement = () => { + return ( + <> + {(executing || loading) && } + {!executing && !loading && } + 日志-{cron && cron.name}{' '} + + ); + }; + + useEffect(() => { + if (cron && cron.id && visible) { + getCronLog(true); + } + }, [cron, visible]); + + useEffect(() => { + if (data) { + setValue(data); + } + }, [data]); + + useEffect(() => { + setIsPhone(document.body.clientWidth < 768); + }, []); + + return ( + cancel()} + onCancel={() => cancel()} + footer={[ + , + ]} + > + {loading ? ( + + ) : ( +
+          {value}
+        
+ )} +
+ ); +}; + +export default SubscriptionLogModal; diff --git a/src/pages/subscription/modal.tsx b/src/pages/subscription/modal.tsx index cab726cf..700044fc 100644 --- a/src/pages/subscription/modal.tsx +++ b/src/pages/subscription/modal.tsx @@ -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(); 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} /> - @@ -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} > -
- - + + + -