mirror of https://github.com/openspug/spug
A web add schedule module
parent
76f3f33c31
commit
2d51d2bb7a
|
@ -11,6 +11,7 @@
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mobx": "^5.15.0",
|
"mobx": "^5.15.0",
|
||||||
"mobx-react": "^6.1.4",
|
"mobx-react": "^6.1.4",
|
||||||
|
"moment": "^2.24.0",
|
||||||
"react": "^16.11.0",
|
"react": "^16.11.0",
|
||||||
"react-ace": "^8.0.0",
|
"react-ace": "^8.0.0",
|
||||||
"react-dom": "^16.11.0",
|
"react-dom": "^16.11.0",
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default [
|
||||||
{title: '模板管理', path: '/exec/template'},
|
{title: '模板管理', path: '/exec/template'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{icon: 'schedule', title: '任务计划', path: '/schedule'},
|
||||||
{
|
{
|
||||||
icon: 'setting', title: '系统管理', child: [
|
icon: 'setting', title: '系统管理', child: [
|
||||||
{title: '账户管理', path: '/system/account'},
|
{title: '账户管理', path: '/system/account'},
|
||||||
|
|
|
@ -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 = (
|
||||||
|
<Form>
|
||||||
|
<Form.Item required label="任务类型">
|
||||||
|
<Input onChange={val => this.setState({type: val.target.value})}/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
title={store.record.id ? '编辑任务' : '新建任务'}
|
||||||
|
okText={page === 0 ? '下一步' : '确定'}
|
||||||
|
onCancel={() => store.formVisible = false}
|
||||||
|
footer={null}>
|
||||||
|
<Steps current={page} className={styles.steps}>
|
||||||
|
<Steps.Step key={0} title="创建任务"/>
|
||||||
|
<Steps.Step key={1} title="选择执行对象"/>
|
||||||
|
<Steps.Step key={2} title="设置触发器"/>
|
||||||
|
</Steps>
|
||||||
|
<Form>
|
||||||
|
<div style={{display: page === 0 ? 'block' : 'none'}}>
|
||||||
|
<Form.Item {...itemLayout} required label="任务类型">
|
||||||
|
<Col span={16}>
|
||||||
|
{getFieldDecorator('type', {initialValue: info['type']})(
|
||||||
|
<Select placeholder="请选择任务类型">
|
||||||
|
{store.types.map(item => (
|
||||||
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col span={6} offset={2}>
|
||||||
|
<Button type="link" onClick={this.handleAddZone}>添加类型</Button>
|
||||||
|
</Col>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...itemLayout} required label="任务名称">
|
||||||
|
{getFieldDecorator('name', {initialValue: info['name']})(
|
||||||
|
<Input placeholder="请输入任务名称"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...itemLayout} required label="任务内容">
|
||||||
|
<SHEditor
|
||||||
|
value={this.state.command}
|
||||||
|
onChange={val => this.setState({command: val})}
|
||||||
|
height="300px"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...itemLayout} label="备注信息">
|
||||||
|
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
||||||
|
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div style={{minHeight: 224, display: page === 1 ? 'block' : 'none'}}>
|
||||||
|
<Form.Item {...itemLayout} required label="执行对象">
|
||||||
|
{store.targets.map((id, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Select
|
||||||
|
value={id}
|
||||||
|
placeholder="请选择"
|
||||||
|
style={{width: '60%', marginRight: 10}}
|
||||||
|
onChange={v => store.editTarget(index, v)}>
|
||||||
|
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option>
|
||||||
|
{hostStore.records.map(item => (
|
||||||
|
<Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
|
||||||
|
{item.name}({item['hostname']}:{item['port']})
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{store.targets.length > 1 && (
|
||||||
|
<Icon className={styles.delIcon} type="minus-circle-o" onClick={() => store.delTarget(index)}/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...itemTailLayout}>
|
||||||
|
<Button type="dashed" style={{width: '60%'}} onClick={store.addTarget}>
|
||||||
|
<Icon type="plus"/>添加执行对象
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div style={{display: page === 2 ? 'block' : 'none'}}>
|
||||||
|
<Form.Item {...itemTailLayout}>
|
||||||
|
{getFieldDecorator('trigger', {valuePropName: 'activeKey', initialValue: info['trigger'] || 'interval'})(
|
||||||
|
<Tabs tabPosition="left" style={{minHeight: 200}}>
|
||||||
|
<Tabs.TabPane tab="普通间隔" key="interval">
|
||||||
|
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。">
|
||||||
|
<InputNumber
|
||||||
|
style={{width: 150}}
|
||||||
|
placeholder="请输入"
|
||||||
|
value={args['interval']}
|
||||||
|
onChange={v => this.handleArgs('interval', v)}/>
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="一次性" key="date">
|
||||||
|
<Form.Item required label="执行时间" extra="仅在指定时间运行一次。">
|
||||||
|
<DatePicker
|
||||||
|
showTime
|
||||||
|
disabledDate={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)}/>
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="UNIX Cron" key="cron">
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="日历间隔" key="calendarinterval">
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Form.Item {...itemTailLayout}>
|
||||||
|
{page === 2 &&
|
||||||
|
<Button disabled={!b3} type="primary" onClick={this.handleSubmit} loading={loading}>提交</Button>}
|
||||||
|
{page === 0 &&
|
||||||
|
<Button disabled={!b1} type="primary" onClick={() => this.setState({page: page + 1})}>下一步</Button>}
|
||||||
|
{page === 1 &&
|
||||||
|
<Button disabled={!b2} type="primary" onClick={() => this.setState({page: page + 1})}>下一步</Button>}
|
||||||
|
{page !== 0 &&
|
||||||
|
<Button style={{marginLeft: 20}} onClick={() => this.setState({page: page - 1})}>上一步</Button>}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form.create()(ComForm)
|
|
@ -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 <Tag color={this.colors[info['latest_status']]}>{info['latest_status_alias']}</Tag>
|
||||||
|
} else {
|
||||||
|
return <Tag>未激活</Tag>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
title: '最近时间',
|
||||||
|
dataIndex: 'latest_run_time',
|
||||||
|
}, {
|
||||||
|
title: '描述信息',
|
||||||
|
dataIndex: 'desc',
|
||||||
|
ellipsis: true
|
||||||
|
}, {
|
||||||
|
title: '操作',
|
||||||
|
render: info => (
|
||||||
|
<span>
|
||||||
|
<LinkButton onClick={() => this.handleActive(info)}>{info.is_active ? '禁用' : '激活'}</LinkButton>
|
||||||
|
<Divider type="vertical"/>
|
||||||
|
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
|
||||||
|
<Divider type="vertical"/>
|
||||||
|
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}];
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Table rowKey="id" loading={store.isFetching} dataSource={data} columns={this.columns}/>
|
||||||
|
{store.formVisible && <ComForm/>}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComTable
|
|
@ -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 (
|
||||||
|
<Card>
|
||||||
|
<SearchForm>
|
||||||
|
<SearchForm.Item span={8} title="任务类型">
|
||||||
|
<Select allowClear onChange={v => store.f_type = v} placeholder="请选择">
|
||||||
|
{store.types.map(item => (
|
||||||
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</SearchForm.Item>
|
||||||
|
<SearchForm.Item span={8} title="任务名称">
|
||||||
|
<Input allowClear onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
|
||||||
|
</SearchForm.Item>
|
||||||
|
<SearchForm.Item span={8}>
|
||||||
|
<Button type="primary" icon="sync" onClick={store.fetchRecords}>刷新</Button>
|
||||||
|
</SearchForm.Item>
|
||||||
|
</SearchForm>
|
||||||
|
<div style={{marginBottom: 16}}>
|
||||||
|
<Button type="primary" icon="plus" onClick={() => store.showForm()}>新建</Button>
|
||||||
|
</div>
|
||||||
|
<ComTable/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,10 @@
|
||||||
|
.steps {
|
||||||
|
width: 520px;
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delIcon {
|
||||||
|
font-size: 24px;
|
||||||
|
position: relative;
|
||||||
|
top: 4px
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { makeRoute } from "../../libs/router";
|
||||||
|
import Index from './index';
|
||||||
|
|
||||||
|
|
||||||
|
export default [
|
||||||
|
makeRoute('', Index),
|
||||||
|
]
|
|
@ -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()
|
|
@ -4,6 +4,7 @@ import homeRoutes from './pages/home/routes';
|
||||||
import hostRoutes from './pages/host/routes';
|
import hostRoutes from './pages/host/routes';
|
||||||
import systemRoutes from './pages/system/routes';
|
import systemRoutes from './pages/system/routes';
|
||||||
import execRoutes from './pages/exec/routes';
|
import execRoutes from './pages/exec/routes';
|
||||||
|
import scheduleRoutes from './pages/schedule/routes';
|
||||||
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -11,4 +12,5 @@ export default [
|
||||||
makeModuleRoute('/host', hostRoutes),
|
makeModuleRoute('/host', hostRoutes),
|
||||||
makeModuleRoute('/system', systemRoutes),
|
makeModuleRoute('/system', systemRoutes),
|
||||||
makeModuleRoute('/exec', execRoutes),
|
makeModuleRoute('/exec', execRoutes),
|
||||||
|
makeModuleRoute('/schedule', scheduleRoutes),
|
||||||
]
|
]
|
Loading…
Reference in New Issue