diff --git a/spug_web/package.json b/spug_web/package.json index 7d320c0..a1ff101 100644 --- a/spug_web/package.json +++ b/spug_web/package.json @@ -11,6 +11,7 @@ "lodash": "^4.17.15", "mobx": "^5.15.0", "mobx-react": "^6.1.4", + "moment": "^2.24.0", "react": "^16.11.0", "react-ace": "^8.0.0", "react-dom": "^16.11.0", diff --git a/spug_web/src/menus.js b/spug_web/src/menus.js index 48d0277..0eefd5b 100644 --- a/spug_web/src/menus.js +++ b/spug_web/src/menus.js @@ -7,6 +7,7 @@ export default [ {title: '模板管理', path: '/exec/template'}, ] }, + {icon: 'schedule', title: '任务计划', path: '/schedule'}, { icon: 'setting', title: '系统管理', child: [ {title: '账户管理', path: '/system/account'}, diff --git a/spug_web/src/pages/schedule/Form.js b/spug_web/src/pages/schedule/Form.js new file mode 100644 index 0000000..5877dc1 --- /dev/null +++ b/spug_web/src/pages/schedule/Form.js @@ -0,0 +1,230 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Modal, Form, Input, Select, Col, Button, Steps, Tabs, InputNumber, DatePicker, Icon, message } from 'antd'; +import { SHEditor } from 'components'; +import http from 'libs/http'; +import store from './store'; +import hostStore from '../host/store'; +import styles from './index.module.css'; +import moment from 'moment'; + +@observer +class ComForm extends React.Component { + constructor(props) { + super(props); + this.state = { + loading: false, + type: null, + page: 0, + args: {[store.record['trigger']]: store.record['trigger_args']}, + command: store.record['command'], + } + } + + componentDidMount() { + store.targets = store.record.id ? store.record['targets'] : [undefined]; + if (hostStore.records.length === 0) { + hostStore.fetchRecords() + } + } + + _parse_args = (trigger) => { + switch (trigger) { + case 'date': + return this.state.args['date'].format('YYYY-MM-DD HH:mm:ss'); + 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'] = 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}) + }; + + verifyButtonStatus = () => { + const data = this.props.form.getFieldsValue(); + const b1 = data['type'] && data['name'] && this.state.command; + const b2 = store.targets.filter(x => x).length > 0; + const b3 = this.state.args[data['trigger']]; + return [b1, b2, b3]; + }; + + render() { + const info = store.record; + const {getFieldDecorator} = this.props.form; + const {page, args, loading} = this.state; + const [b1, b2, b3] = this.verifyButtonStatus(); + const itemLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14} + }; + const itemTailLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14, offset: 6} + }; + return ( + store.formVisible = false} + footer={null}> + + + + + +
+
+ + + {getFieldDecorator('type', {initialValue: info['type']})( + + )} + + + + + + + {getFieldDecorator('name', {initialValue: info['name']})( + + )} + + + this.setState({command: val})} + height="300px"/> + + + {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)}/> + + + + + + + + )} + +
+ + {page === 2 && + } + {page === 0 && + } + {page === 1 && + } + {page !== 0 && + } + +
+
+ ) + } +} + +export default Form.create()(ComForm) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/Table.js b/spug_web/src/pages/schedule/Table.js new file mode 100644 index 0000000..cacc5d8 --- /dev/null +++ b/spug_web/src/pages/schedule/Table.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Table, Divider, Modal, Tag, message } from 'antd'; +import ComForm from './Form'; +import http from 'libs/http'; +import store from './store'; +import { LinkButton } from "components"; + +@observer +class ComTable extends React.Component { + componentDidMount() { + store.fetchRecords() + } + + colors = ['green', 'orange', 'red']; + + columns = [{ + title: '序号', + key: 'series', + render: (_, __, index) => index + 1, + width: 80, + }, { + title: '任务名称', + dataIndex: 'name', + }, { + title: '任务类型', + dataIndex: 'type', + }, { + title: '最新状态', + render: info => { + if (info.is_active) { + return {info['latest_status_alias']} + } else { + return 未激活 + } + }, + }, { + title: '最近时间', + dataIndex: 'latest_run_time', + }, { + title: '描述信息', + dataIndex: 'desc', + ellipsis: true + }, { + title: '操作', + render: info => ( + + this.handleActive(info)}>{info.is_active ? '禁用' : '激活'} + + store.showForm(info)}>编辑 + + this.handleDelete(info)}>删除 + + ) + }]; + + handleActive = (text) => { + Modal.confirm({ + title: '删除确认', + content: `确定要${text.is_active ? '禁用' : '激活'}任务【${text['name']}】?`, + onOk: () => { + return http.patch('/api/schedule/', {id: text.id, is_active: !text.is_active}) + .then(() => { + message.success('操作成功'); + store.fetchRecords() + }) + } + }) + }; + + handleDelete = (text) => { + Modal.confirm({ + title: '删除确认', + content: `确定要删除【${text['name']}】?`, + onOk: () => { + return http.delete('/api/schedule/', {params: {id: text.id}}) + .then(() => { + message.success('删除成功'); + store.fetchRecords() + }) + } + }) + }; + + render() { + let data = store.records; + 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 ( + + + {store.formVisible && } + + ) + } +} + +export default ComTable \ No newline at end of file diff --git a/spug_web/src/pages/schedule/index.js b/spug_web/src/pages/schedule/index.js new file mode 100644 index 0000000..9c8c199 --- /dev/null +++ b/spug_web/src/pages/schedule/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Card, Input, Select, Button } from 'antd'; +import { SearchForm } from 'components'; +import ComTable from './Table'; +import store from './store'; + +export default observer(function () { + return ( + + + + + + + store.f_name = e.target.value} placeholder="请输入"/> + + + + + +
+ +
+ +
+ ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/index.module.css b/spug_web/src/pages/schedule/index.module.css new file mode 100644 index 0000000..6848873 --- /dev/null +++ b/spug_web/src/pages/schedule/index.module.css @@ -0,0 +1,10 @@ +.steps { + width: 520px; + margin: 0 auto 30px; +} + +.delIcon { + font-size: 24px; + position: relative; + top: 4px +} \ No newline at end of file diff --git a/spug_web/src/pages/schedule/routes.js b/spug_web/src/pages/schedule/routes.js new file mode 100644 index 0000000..6e03f0b --- /dev/null +++ b/spug_web/src/pages/schedule/routes.js @@ -0,0 +1,7 @@ +import { makeRoute } from "../../libs/router"; +import Index from './index'; + + +export default [ + makeRoute('', Index), +] \ No newline at end of file diff --git a/spug_web/src/pages/schedule/store.js b/spug_web/src/pages/schedule/store.js new file mode 100644 index 0000000..28b865a --- /dev/null +++ b/spug_web/src/pages/schedule/store.js @@ -0,0 +1,43 @@ +import { observable } from "mobx"; +import http from 'libs/http'; + +class Store { + @observable records = []; + @observable types = []; + @observable record = {}; + @observable targets = [undefined]; + @observable isFetching = false; + @observable formVisible = false; + + @observable f_name; + @observable f_type; + + fetchRecords = () => { + this.isFetching = true; + http.get('/api/schedule/') + .then(({types, tasks}) => { + this.records = tasks; + this.types = types + }) + .finally(() => this.isFetching = false) + }; + + showForm = (info = {}) => { + this.formVisible = true; + this.record = info + }; + + addTarget = () => { + this.targets.push(undefined) + }; + + editTarget = (index, v) => { + this.targets[index] = v + }; + + delTarget = (index) => { + this.targets.splice(index, 1) + } +} + +export default new Store() \ No newline at end of file diff --git a/spug_web/src/routes.js b/spug_web/src/routes.js index 1b5d76f..5b77924 100644 --- a/spug_web/src/routes.js +++ b/spug_web/src/routes.js @@ -4,6 +4,7 @@ import homeRoutes from './pages/home/routes'; import hostRoutes from './pages/host/routes'; import systemRoutes from './pages/system/routes'; import execRoutes from './pages/exec/routes'; +import scheduleRoutes from './pages/schedule/routes'; export default [ @@ -11,4 +12,5 @@ export default [ makeModuleRoute('/host', hostRoutes), makeModuleRoute('/system', systemRoutes), makeModuleRoute('/exec', execRoutes), + makeModuleRoute('/schedule', scheduleRoutes), ] \ No newline at end of file