diff --git a/spug_web/src/pages/schedule/Form.js b/spug_web/src/pages/schedule/Form.js index 32b4d16..20dfbb2 100644 --- a/spug_web/src/pages/schedule/Form.js +++ b/spug_web/src/pages/schedule/Form.js @@ -3,329 +3,39 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { observer } from 'mobx-react'; -import { Modal, Form, Input, Select, Col, Button, Steps, Tabs, InputNumber, DatePicker, Icon, message } from 'antd'; -import { LinkButton, ACEditor } from 'components'; -import TemplateSelector from '../exec/task/TemplateSelector'; -import { http, cleanCommand, hasHostPermission } from 'libs'; +import { Modal, Steps } from 'antd'; +import Step1 from './Step1'; +import Step2 from './Step2'; +import Step3 from './Step3'; import store from './store'; -import hostStore from '../host/store'; import styles from './index.module.css'; -import moment from 'moment'; -import lds from 'lodash'; +import hostStore from '../host/store'; -@observer -class ComForm extends React.Component { - constructor(props) { - super(props); - this.isFirstRender = true; - this.lastFetchId = 0; - this._fetchNextRunTime = lds.debounce(this._fetchNextRunTime, 500); - this.state = { - loading: false, - type: null, - page: 0, - nextRunTime: null, - args: {[store.record['trigger']]: store.record['trigger_args']}, - command: store.record['command'] || '', - } - } - - componentDidMount() { +export default observer(function () { + useEffect(() => { store.targets = store.record.id ? store.record['targets'] : [undefined]; if (hostStore.records.length === 0) { hostStore.fetchRecords() } - } - - _parse_args = (trigger) => { - switch (trigger) { - case 'date': - return moment(this.state.args['date']).format('YYYY-MM-DD HH:mm:ss'); - case 'cron': - const {rule, start, stop} = this.state.args['cron']; - return JSON.stringify({ - rule, - start: start ? moment(start).format('YYYY-MM-DD HH:mm:ss') : null, - stop: stop ? moment(stop).format('YYYY-MM-DD HH:mm:ss') : null - }); - default: - return this.state.args[trigger]; - } - }; - - handleSubmit = () => { - const formData = this.props.form.getFieldsValue(); - if (formData['trigger'] === 'date' && this.state.args['date'] <= moment()) { - return message.error('任务执行时间不能早于当前时间') - } - this.setState({loading: true}); - formData['id'] = store.record.id; - formData['command'] = cleanCommand(this.state.command); - formData['targets'] = store.targets.filter(x => x); - formData['trigger_args'] = this._parse_args(formData['trigger']); - http.post('/api/schedule/', formData) - .then(res => { - message.success('操作成功'); - store.formVisible = false; - store.fetchRecords() - }, () => this.setState({loading: false})) - }; - - handleAddZone = () => { - Modal.confirm({ - icon: 'exclamation-circle', - title: '添加任务类型', - content: this.addZoneForm, - onOk: () => { - if (this.state.type) { - store.types.push(this.state.type); - this.props.form.setFieldsValue({'type': this.state.type}) - } - }, - }) - }; - - addZoneForm = ( -
- - this.setState({type: val.target.value})}/> - -
- ); - - handleArgs = (type, value) => { - const args = Object.assign(this.state.args, {[type]: value}); - this.setState({args}) - }; - - handleCronArgs = (key, value) => { - let args = this.state.args['cron'] || {}; - args = Object.assign(args, {[key]: value}); - this.setState({args: Object.assign(this.state.args, {cron: args})}, () => { - if (key === 'rule') { - value = value.trim(); - if (value.split(' ').length === 5) { - this.setState({nextRunTime: }); - this._fetchNextRunTime() - } else { - this.setState({nextRunTime: null}) - } - } else { - this.setState({nextRunTime: }); - this._fetchNextRunTime() - } - }); - }; - - _fetchNextRunTime = () => { - this.lastFetchId += 1; - const fetchId = this.lastFetchId; - const args = this._parse_args('cron'); - http.post('/api/schedule/run_time/', JSON.parse(args)) - .then(({success, msg}) => { - if (fetchId !== this.lastFetchId) return; - if (success) { - this.setState({nextRunTime: {msg}}) - } else { - this.setState({nextRunTime: {msg}}) - } - }) - }; - - verifyButtonStatus = () => { - const data = this.props.form.getFieldsValue(); - let b1 = data['type'] && data['name'] && this.state.command; - const b2 = store.targets.filter(x => x).length > 0; - const b3 = this.state.args[data['trigger']]; - if (!b1 && this.isFirstRender && store.record.id) { - this.isFirstRender = false; - b1 = true - } - return [b1, b2, b3]; - }; - - render() { - const info = store.record; - const {getFieldDecorator, getFieldValue} = this.props.form; - const {page, args, command, loading, showTmp, nextRunTime} = this.state; - const [b1, b2, b3] = this.verifyButtonStatus(); - return ( - store.formVisible = false} - footer={null}> - - - - - -
-
- - - {getFieldDecorator('type', {initialValue: info['type']})( - - )} - - - - - - - {getFieldDecorator('name', {initialValue: info['name']})( - - )} - - this.setState({showTmp: true})}>从模板添加}> - this.setState({command: val})}/> - - - 任务执行失败告警通知, - 钉钉收不到通知? - }> - {getFieldDecorator('rst_notify.value', {initialValue: info['rst_notify']['value']})( - - 关闭 - 钉钉 - 企业微信 - Webhook - - )} - disabled={getFieldValue('rst_notify.mode') === '0'} - placeholder="请输入"/> - )} - - - {getFieldDecorator('desc', {initialValue: info['desc']})( - - )} - -
-
- - {store.targets.map((id, index) => ( - - - {store.targets.length > 1 && ( - store.delTarget(index)}/> - )} - - ))} - - - - -
-
- - {getFieldDecorator('trigger', {valuePropName: 'activeKey', initialValue: info['trigger'] || 'interval'})( - - - - this.handleArgs('interval', v)}/> - - - - - v && v.format('YYYY-MM-DD') < moment().format('YYYY-MM-DD')} - style={{width: 150}} - placeholder="请选择执行时间" - onOk={() => false} - value={args['date'] ? moment(args['date']) : undefined} - onChange={v => this.handleArgs('date', v)}/> - - - - - } - value={lds.get(args, 'cron.rule')} - placeholder="例如每天凌晨1点执行:0 1 * * *" - onChange={e => this.handleCronArgs('rule', e.target.value)}/> - - - this.handleCronArgs('start', v)}/> - - - this.handleCronArgs('stop', v)}/> - - - - - - )} - -
- - {page === 2 && - } - {page === 0 && - } - {page === 1 && - } - {page !== 0 && - } - -
- {showTmp && this.setState({command: command + v})} - onCancel={() => this.setState({showTmp: false})}/>} -
- ) - } -} - -export default Form.create()(ComForm) + }, []) + return ( + store.formVisible = false} + footer={null}> + + + + + + {store.page === 0 && } + {store.page === 1 && } + {store.page === 2 && } + + ) +}) diff --git a/spug_web/src/pages/schedule/Info.js b/spug_web/src/pages/schedule/Info.js index d3eba15..67aa51a 100644 --- a/spug_web/src/pages/schedule/Info.js +++ b/spug_web/src/pages/schedule/Info.js @@ -4,7 +4,7 @@ * Released under the AGPL-3.0 License. */ import React from 'react'; -import { Modal, Form, Tabs, Spin } from 'antd'; +import { Modal, Tabs, Spin } from 'antd'; import { StatisticsCard } from 'components'; import http from 'libs/http'; import store from './store'; @@ -68,4 +68,4 @@ class ComForm extends React.Component { } } -export default Form.create()(ComForm) +export default ComForm diff --git a/spug_web/src/pages/schedule/Step1.js b/spug_web/src/pages/schedule/Step1.js new file mode 100644 index 0000000..25556ef --- /dev/null +++ b/spug_web/src/pages/schedule/Step1.js @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { observer } from 'mobx-react'; +import { Form, Input, Select, Modal, Button } from 'antd'; +import { ExclamationCircleOutlined } from '@ant-design/icons'; +import { LinkButton, ACEditor } from 'components'; +import TemplateSelector from '../exec/task/TemplateSelector'; +import { cleanCommand } from 'libs'; +import store from './store'; + +export default observer(function () { + const [form] = Form.useForm(); + const [showTmp, setShowTmp] = useState(false); + const [command, setCommand] = useState(store.record.command || ''); + + function handleAddZone() { + let type; + Modal.confirm({ + icon: , + title: '添加任务类型', + content: ( +
+ + type = e.target.value}/> + +
+ ), + onOk: () => { + if (type) { + store.types.push(type); + form.setFieldsValue({type}) + } + }, + }) + } + + function canNext() { + const formData = form.getFieldsValue() + return !(formData.type && formData.name && command) + } + + function handleNext() { + store.page += 1; + Object.assign(store.record, form.getFieldsValue(), {command: cleanCommand(command)}) + } + + return ( +
+ + + + + + + + + + + + setShowTmp(true)}>从模板添加}> + + + + 任务执行失败告警通知, + 钉钉收不到通知? + )}> + store.record.rst_notify.value = e.target.value} + addonBefore={( + + )} + disabled={store.record.rst_notify.mode === '0'} + placeholder="请输入"/> + + + + + + {() => } + + {showTmp && setCommand(command + v)} onCancel={() => setShowTmp(false)}/>} + + ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/Step2.js b/spug_web/src/pages/schedule/Step2.js new file mode 100644 index 0000000..f3b9c37 --- /dev/null +++ b/spug_web/src/pages/schedule/Step2.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { observer } from 'mobx-react'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Form, Select, Button } from 'antd'; +import { hasHostPermission } from 'libs'; +import store from './store'; +import hostStore from 'pages/host/store'; +import styles from './index.module.css'; + +export default observer(function () { + return ( +
+ + {store.targets.map((id, index) => ( + + + {store.targets.length > 1 && ( + store.delTarget(index)}/> + )} + + ))} + + + + + + + + +
+ ) +}) diff --git a/spug_web/src/pages/schedule/Step3.js b/spug_web/src/pages/schedule/Step3.js new file mode 100644 index 0000000..535b22d --- /dev/null +++ b/spug_web/src/pages/schedule/Step3.js @@ -0,0 +1,143 @@ +import React, { useState } from 'react'; +import { observer } from 'mobx-react'; +import { Form, Tabs, DatePicker, InputNumber, Input, Button, message } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; +import { http } from 'libs'; +import store from './store'; +import moment from 'moment'; +import lds from 'lodash'; + +let lastFetchId = 0; + +export default observer(function () { + const [loading, setLoading] = useState(false); + const [trigger, setTrigger] = useState(store.record.trigger); + const [args, setArgs] = useState({[store.record.trigger]: store.record.trigger_args}); + const [nextRunTime, setNextRunTime] = useState(null); + + function handleSubmit() { + if (trigger === 'date' && args['date'] <= moment()) { + return message.error('任务执行时间不能早于当前时间') + } + setLoading(true) + const formData = lds.pick(store.record, ['id', 'name', 'type', 'command', 'desc', 'rst_notify']); + formData['targets'] = store.targets.filter(x => x); + formData['trigger'] = trigger; + formData['trigger_args'] = _parse_args(); + http.post('/api/schedule/', formData) + .then(res => { + message.success('操作成功'); + store.formVisible = false; + store.fetchRecords() + }, () => setLoading(false)) + } + + function handleArgs(key, val) { + setArgs(Object.assign({}, args, {[key]: val})) + } + + function handleCronArgs(key, val) { + let tmp = args['cron'] || {}; + tmp = Object.assign(tmp, {[key]: val}); + setArgs(Object.assign({}, args, {cron: tmp})); + _fetchNextRunTime() + } + + function _parse_args() { + switch (trigger) { + case 'date': + return moment(args['date']).format('YYYY-MM-DD HH:mm:ss'); + case 'cron': + const {rule, start, stop} = args['cron']; + return JSON.stringify({ + rule, + start: start ? moment(start).format('YYYY-MM-DD HH:mm:ss') : null, + stop: stop ? moment(stop).format('YYYY-MM-DD HH:mm:ss') : null + }); + default: + return args[trigger]; + } + } + + function _fetchNextRunTime() { + if (trigger === 'cron') { + const rule = lds.get(args, 'cron.rule'); + if (rule && rule.trim().split(/ +/).length === 5) { + setNextRunTime(); + lastFetchId += 1; + const fetchId = lastFetchId; + const args = _parse_args(); + http.post('/api/schedule/run_time/', JSON.parse(args)) + .then(res => { + if (fetchId !== lastFetchId) return; + if (res.success) { + setNextRunTime({res.msg}) + } else { + setNextRunTime({res.msg}) + } + }) + } else { + setNextRunTime(null) + } + } + } + + return ( +
+ + + + + handleArgs('interval', v)}/> + + + + + v && v.format('YYYY-MM-DD') < moment().format('YYYY-MM-DD')} + style={{width: 200}} + placeholder="请选择执行时间" + onOk={() => false} + value={args['date'] ? moment(args['date']) : undefined} + onChange={v => handleArgs('date', v)}/> + + + + + } + value={lds.get(args, 'cron.rule')} + placeholder="例如每天凌晨1点执行:0 1 * * *" + onChange={e => handleCronArgs('rule', e.target.value)}/> + + + handleCronArgs('start', v)}/> + + + handleCronArgs('stop', v)}/> + + + + + + + + +
+ ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/Table.js b/spug_web/src/pages/schedule/Table.js index 00f2be3..ce20ead 100644 --- a/spug_web/src/pages/schedule/Table.js +++ b/spug_web/src/pages/schedule/Table.js @@ -5,13 +5,11 @@ */ import React from 'react'; import { observer } from 'mobx-react'; -import { Table, Modal, Tag, Dropdown, Icon, Menu, message } from 'antd'; -import ComForm from './Form'; -import {http} from 'libs'; +import { DownOutlined, PlusOutlined } from '@ant-design/icons'; +import { Modal, Tag, Dropdown, Menu, Radio, message } from 'antd'; +import { LinkButton, Action, TableCard, AuthButton } from 'components'; +import { http } from 'libs'; import store from './store'; -import { LinkButton, Action } from "components"; -import Info from './Info'; -import Record from './Record'; @observer class ComTable extends React.Component { @@ -27,7 +25,8 @@ class ComTable extends React.Component { this.handleTest(info)}>执行测试 - this.handleActive(info)}>{info.is_active ? '禁用任务' : '激活任务'} + this.handleActive(info)}>{info.is_active ? '禁用任务' : '激活任务'} store.showRecord(info)}>历史记录 @@ -71,11 +70,12 @@ class ComTable extends React.Component { width: 180, render: info => ( - store.showInfo(info)}>详情 + store.showInfo(info)}>详情 store.showForm(info)}>编辑 this.moreMenus(info)} trigger={['click']}> - 更多 + 更多 @@ -120,43 +120,33 @@ class ComTable extends React.Component { }; render() { - let data = store.records; - if (store.f_status !== undefined) { - if (store.f_status === -3) { - data = data.filter(item => !item['is_active']) - } else if (store.f_status === -2) { - data = data.filter(item => item['is_active']) - } else if (store.f_status === -1) { - data = data.filter(item => item['is_active'] && !item['latest_status_alias']) - } else { - data = data.filter(item => item['latest_status'] === store.f_status) - } - } - if (store.f_status === 0) data = data.filter(item => item['is_active']); - if (store.f_name) { - data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase())) - } - if (store.f_type) { - data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase())) - } return ( - - `共 ${total} 条`, - pageSizeOptions: ['10', '20', '50', '100'] - }} - columns={this.columns}/> - {store.formVisible && } - {store.infoVisible && } - {store.recordVisible && } - + } + onClick={() => store.showForm()}>新建, + store.f_active = e.target.value}> + 全部 + 已激活 + 未激活 + + ]} + pagination={{ + showSizeChanger: true, + showLessItems: true, + hideOnSinglePage: true, + showTotal: total => `共 ${total} 条`, + pageSizeOptions: ['10', '20', '50', '100'] + }} + columns={this.columns}/> ) } } diff --git a/spug_web/src/pages/schedule/index.js b/spug_web/src/pages/schedule/index.js index 356b666..76f3d09 100644 --- a/spug_web/src/pages/schedule/index.js +++ b/spug_web/src/pages/schedule/index.js @@ -5,19 +5,24 @@ */ import React from 'react'; import { observer } from 'mobx-react'; -import { Input, Select, Button } from 'antd'; -import { SearchForm, AuthDiv, AuthCard } from 'components'; +import { Input, Select } from 'antd'; +import { SearchForm, AuthDiv, Breadcrumb } from 'components'; import ComTable from './Table'; +import Info from './Info'; +import Record from './Record'; +import ComForm from './Form'; import store from './store'; export default observer(function () { return ( - + + + 首页 + 任务计划 + store.f_name = e.target.value} placeholder="请输入"/> - - - - - - - + {store.formVisible && } + {store.infoVisible && } + {store.recordVisible && } + ) }) diff --git a/spug_web/src/pages/schedule/store.js b/spug_web/src/pages/schedule/store.js index 4f95d27..c5c03b0 100644 --- a/spug_web/src/pages/schedule/store.js +++ b/spug_web/src/pages/schedule/store.js @@ -3,7 +3,7 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import { observable } from "mobx"; +import { observable, computed } from 'mobx'; import http from 'libs/http'; import moment from "moment"; @@ -11,6 +11,7 @@ class Store { @observable records = []; @observable types = []; @observable record = {}; + @observable page = 0; @observable targets = [undefined]; @observable isFetching = false; @observable formVisible = false; @@ -18,27 +19,45 @@ class Store { @observable recordVisible = false; @observable f_status; + @observable f_active = ''; @observable f_name; @observable f_type; + @computed get dataSource() { + let records = this.records; + if (this.f_active) records = records.filter(x => x.is_active === (this.f_active === '1')); + if (this.f_name) records = records.filter(x => x.name.toLowerCase().includes(this.f_name.toLowerCase())); + if (this.f_type) records = records.filter(x => x.type.toLowerCase().includes(this.f_type.toLowerCase())); + if (this.f_status !== undefined) { + if (this.f_status === -1) { + records = records.filter(x => x.is_active && !x.latest_status_alias); + } else { + records = records.filter(x => x.latest_status === this.f_status) + } + } + return records + } + fetchRecords = () => { this.isFetching = true; http.get('/api/schedule/') - .then(({types, tasks}) => { - tasks.map(item => { + .then(res => { + res.tasks.map(item => { const value = item['latest_run_time']; item['latest_run_time_alias'] = value ? moment(value).fromNow() : null; + item['latest_run_time'] = value || '1970-01-01'; return null }); - this.records = tasks; - this.types = types + this.records = res.tasks; + this.types = res.types }) .finally(() => this.isFetching = false) }; - showForm = (info = {rst_notify: {mode: '0'}}) => { - this.formVisible = true; - this.record = info + showForm = (info) => { + this.page = 0; + this.record = info || {rst_notify: {mode: '0'}, trigger: 'interval'}; + this.formVisible = true }; showInfo = (info, h_id = 'latest') => {