mirror of https://github.com/openspug/spug
style migrate v3
parent
1f2a4249b1
commit
682cbfa8a5
|
@ -3,329 +3,39 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* 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 = (
|
||||
<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})
|
||||
};
|
||||
|
||||
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: <Icon type="loading"/>});
|
||||
this._fetchNextRunTime()
|
||||
} else {
|
||||
this.setState({nextRunTime: null})
|
||||
}
|
||||
} else {
|
||||
this.setState({nextRunTime: <Icon type="loading"/>});
|
||||
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: <span style={{fontSize: 12, color: '#52c41a'}}>{msg}</span>})
|
||||
} else {
|
||||
this.setState({nextRunTime: <span style={{fontSize: 12, color: '#ff4d4f'}}>{msg}</span>})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
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 (
|
||||
<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 labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<div style={{display: page === 0 ? 'block' : 'none'}}>
|
||||
<Form.Item 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 required label="任务名称">
|
||||
{getFieldDecorator('name', {initialValue: info['name']})(
|
||||
<Input placeholder="请输入任务名称"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
required
|
||||
label="任务内容"
|
||||
extra={<LinkButton onClick={() => this.setState({showTmp: true})}>从模板添加</LinkButton>}>
|
||||
<ACEditor
|
||||
mode="sh"
|
||||
value={command}
|
||||
width="100%"
|
||||
height="150px"
|
||||
onChange={val => this.setState({command: val})}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="失败通知" extra={<span>
|
||||
任务执行失败告警通知,
|
||||
<a target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.dev/docs/install-error/#%E9%92%89%E9%92%89%E6%94%B6%E4%B8%8D%E5%88%B0%E9%80%9A%E7%9F%A5%EF%BC%9F">钉钉收不到通知?</a>
|
||||
</span>}>
|
||||
{getFieldDecorator('rst_notify.value', {initialValue: info['rst_notify']['value']})(
|
||||
<Input
|
||||
addonBefore={getFieldDecorator('rst_notify.mode', {initialValue: info['rst_notify']['mode']})(
|
||||
<Select style={{width: 100}}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">钉钉</Select.Option>
|
||||
<Select.Option value="3">企业微信</Select.Option>
|
||||
<Select.Option value="2">Webhook</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
disabled={getFieldValue('rst_notify.mode') === '0'}
|
||||
placeholder="请输入"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="备注信息">
|
||||
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
||||
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{minHeight: 224, display: page === 1 ? 'block' : 'none'}}>
|
||||
<Form.Item required label="执行对象">
|
||||
{store.targets.map((id, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Select
|
||||
value={id}
|
||||
showSearch
|
||||
placeholder="请选择"
|
||||
optionFilterProp="children"
|
||||
style={{width: '80%', marginRight: 10}}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => store.editTarget(index, v)}>
|
||||
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option>
|
||||
{hostStore.records.filter(x => x.id === id || hasHostPermission(x.id)).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 wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" style={{width: '80%'}} onClick={store.addTarget}>
|
||||
<Icon type="plus"/>添加执行对象
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{display: page === 2 ? 'block' : 'none'}}>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
{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">
|
||||
<Form.Item required label="执行规则" help="兼容Cron风格,可参考官方例子">
|
||||
<Input
|
||||
suffix={nextRunTime || <span/>}
|
||||
value={lds.get(args, 'cron.rule')}
|
||||
placeholder="例如每天凌晨1点执行:0 1 * * *"
|
||||
onChange={e => this.handleCronArgs('rule', e.target.value)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="生效时间" help="定义的执行规则在到达该时间后生效">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.start') ? moment(args['cron']['start']) : undefined}
|
||||
onChange={v => this.handleCronArgs('start', v)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="结束时间" help="执行规则在到达该时间后不再执行">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.stop') ? moment(args['cron']['stop']) : undefined}
|
||||
onChange={v => this.handleCronArgs('stop', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane disabled tab="日历间隔" key="calendarinterval">
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
{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>
|
||||
{showTmp && <TemplateSelector
|
||||
onOk={v => this.setState({command: command + v})}
|
||||
onCancel={() => this.setState({showTmp: false})}/>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create()(ComForm)
|
||||
}, [])
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
title={store.record.id ? '编辑任务' : '新建任务'}
|
||||
onCancel={() => store.formVisible = false}
|
||||
footer={null}>
|
||||
<Steps current={store.page} className={styles.steps}>
|
||||
<Steps.Step key={0} title="创建任务"/>
|
||||
<Steps.Step key={1} title="选择执行对象"/>
|
||||
<Steps.Step key={2} title="设置触发器"/>
|
||||
</Steps>
|
||||
{store.page === 0 && <Step1/>}
|
||||
{store.page === 1 && <Step2/>}
|
||||
{store.page === 2 && <Step3/>}
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: <ExclamationCircleOutlined/>,
|
||||
title: '添加任务类型',
|
||||
content: (
|
||||
<Form layout="vertical" style={{marginTop: 24}}>
|
||||
<Form.Item required label="任务类型">
|
||||
<Input onChange={e => type = e.target.value}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
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 (
|
||||
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required label="任务类型" style={{marginBottom: 0}}>
|
||||
<Form.Item name="type" style={{display: 'inline-block', width: '80%'}}>
|
||||
<Select placeholder="请选择任务类型">
|
||||
{store.types.map(item => (
|
||||
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item style={{display: 'inline-block', width: '20%', textAlign: 'right'}}>
|
||||
<Button type="link" onClick={handleAddZone}>添加类型</Button>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item required name="name" label="任务名称">
|
||||
<Input placeholder="请输入任务名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item required label="任务内容" extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
|
||||
<ACEditor mode="sh" value={command} width="100%" height="150px" onChange={setCommand}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="失败通知" extra={(
|
||||
<span>
|
||||
任务执行失败告警通知,
|
||||
<a target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.dev/docs/install-error/#%E9%92%89%E9%92%89%E6%94%B6%E4%B8%8D%E5%88%B0%E9%80%9A%E7%9F%A5%EF%BC%9F">钉钉收不到通知?</a>
|
||||
</span>)}>
|
||||
<Input
|
||||
value={store.record.rst_notify.value}
|
||||
onChange={e => store.record.rst_notify.value = e.target.value}
|
||||
addonBefore={(
|
||||
<Select style={{width: 100}} value={store.record.rst_notify.mode}
|
||||
onChange={v => store.record.rst_notify.mode = v}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">钉钉</Select.Option>
|
||||
<Select.Option value="3">企业微信</Select.Option>
|
||||
<Select.Option value="2">Webhook</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
disabled={store.record.rst_notify.mode === '0'}
|
||||
placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
|
||||
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
|
||||
</Form.Item>
|
||||
{showTmp && <TemplateSelector onOk={v => setCommand(command + v)} onCancel={() => setShowTmp(false)}/>}
|
||||
</Form>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* 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 (
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required label="执行对象">
|
||||
{store.targets.map((id, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Select
|
||||
value={id}
|
||||
showSearch
|
||||
placeholder="请选择"
|
||||
optionFilterProp="children"
|
||||
style={{width: '80%', marginRight: 10, marginBottom: 12}}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => store.editTarget(index, v)}>
|
||||
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option>
|
||||
{hostStore.records.filter(x => x.id === id || hasHostPermission(x.id)).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 && (
|
||||
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delTarget(index)}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" style={{width: '80%'}} onClick={store.addTarget}>
|
||||
<PlusOutlined/>添加执行对象
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={store.targets.filter(x => x).length === 0} type="primary"
|
||||
onClick={() => store.page += 1}>下一步</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
|
@ -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(<LoadingOutlined/>);
|
||||
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(<span style={{fontSize: 12, color: '#52c41a'}}>{res.msg}</span>)
|
||||
} else {
|
||||
setNextRunTime(<span style={{fontSize: 12, color: '#ff4d4f'}}>{res.msg}</span>)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setNextRunTime(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form layout="vertical" wrapperCol={{span: 14, offset: 6}}>
|
||||
<Form.Item>
|
||||
<Tabs activeKey={trigger} onChange={setTrigger} tabPosition="left" style={{minHeight: 200}}>
|
||||
<Tabs.TabPane tab="普通间隔" key="interval">
|
||||
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。">
|
||||
<InputNumber
|
||||
style={{width: 200}}
|
||||
placeholder="请输入"
|
||||
value={args['interval']}
|
||||
onChange={v => 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: 200}}
|
||||
placeholder="请选择执行时间"
|
||||
onOk={() => false}
|
||||
value={args['date'] ? moment(args['date']) : undefined}
|
||||
onChange={v => handleArgs('date', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="UNIX Cron" key="cron">
|
||||
<Form.Item required label="执行规则" help="兼容Cron风格,可参考官方例子">
|
||||
<Input
|
||||
suffix={nextRunTime || <span/>}
|
||||
value={lds.get(args, 'cron.rule')}
|
||||
placeholder="例如每天凌晨1点执行:0 1 * * *"
|
||||
onChange={e => handleCronArgs('rule', e.target.value)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="生效时间" help="定义的执行规则在到达该时间后生效">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.start') ? moment(args['cron']['start']) : undefined}
|
||||
onChange={v => handleCronArgs('start', v)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="结束时间" help="执行规则在到达该时间后不再执行">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.stop') ? moment(args['cron']['stop']) : undefined}
|
||||
onChange={v => handleCronArgs('stop', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="primary" loading={loading} disabled={!args[trigger]} onClick={handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
|
@ -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 {
|
|||
<LinkButton onClick={() => this.handleTest(info)}>执行测试</LinkButton>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<LinkButton auth="schedule.schedule.edit" onClick={() => this.handleActive(info)}>{info.is_active ? '禁用任务' : '激活任务'}</LinkButton>
|
||||
<LinkButton auth="schedule.schedule.edit"
|
||||
onClick={() => this.handleActive(info)}>{info.is_active ? '禁用任务' : '激活任务'}</LinkButton>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<LinkButton onClick={() => store.showRecord(info)}>历史记录</LinkButton>
|
||||
|
@ -71,11 +70,12 @@ class ComTable extends React.Component {
|
|||
width: 180,
|
||||
render: info => (
|
||||
<Action>
|
||||
<Action.Button disabled={!info['latest_run_time']} onClick={() => store.showInfo(info)}>详情</Action.Button>
|
||||
<Action.Button disabled={info['latest_run_time'] === '1970-01-01'}
|
||||
onClick={() => store.showInfo(info)}>详情</Action.Button>
|
||||
<Action.Button auth="schedule.schedule.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
|
||||
<Dropdown overlay={() => this.moreMenus(info)} trigger={['click']}>
|
||||
<LinkButton>
|
||||
更多 <Icon type="down"/>
|
||||
更多 <DownOutlined/>
|
||||
</LinkButton>
|
||||
</Dropdown>
|
||||
</Action>
|
||||
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={store.isFetching}
|
||||
dataSource={data}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showLessItems: true,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}
|
||||
columns={this.columns}/>
|
||||
{store.formVisible && <ComForm/>}
|
||||
{store.infoVisible && <Info/>}
|
||||
{store.recordVisible && <Record/>}
|
||||
</React.Fragment>
|
||||
<TableCard
|
||||
rowKey="id"
|
||||
title="任务列表"
|
||||
loading={store.isFetching}
|
||||
dataSource={store.dataSource}
|
||||
onReload={store.fetchRecords}
|
||||
actions={[
|
||||
<AuthButton
|
||||
auth="schedule.schedule.add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => store.showForm()}>新建</AuthButton>,
|
||||
<Radio.Group value={store.f_active} onChange={e => store.f_active = e.target.value}>
|
||||
<Radio.Button value="">全部</Radio.Button>
|
||||
<Radio.Button value="1">已激活</Radio.Button>
|
||||
<Radio.Button value="0">未激活</Radio.Button>
|
||||
</Radio.Group>
|
||||
]}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showLessItems: true,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}
|
||||
columns={this.columns}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<AuthCard auth="schedule.schedule.view">
|
||||
<AuthDiv auth="schedule.schedule.view">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>首页</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>任务计划</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<SearchForm>
|
||||
<SearchForm.Item span={6} title="状态">
|
||||
<Select allowClear value={store.f_status} onChange={v => store.f_status = v} placeholder="请选择">
|
||||
<Select.Option value={-3}>未激活</Select.Option>
|
||||
<Select.Option value={-2}>已激活</Select.Option>
|
||||
<Select.Option value={-1}>待调度</Select.Option>
|
||||
<Select.Option value={0}>成功</Select.Option>
|
||||
<Select.Option value={1}>异常</Select.Option>
|
||||
|
@ -34,14 +39,11 @@ export default observer(function () {
|
|||
<SearchForm.Item span={6} title="名称">
|
||||
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
|
||||
</SearchForm.Item>
|
||||
<SearchForm.Item span={6}>
|
||||
<Button type="primary" icon="sync" onClick={store.fetchRecords}>刷新</Button>
|
||||
</SearchForm.Item>
|
||||
</SearchForm>
|
||||
<AuthDiv auth="schedule.schedule.add" style={{marginBottom: 16}}>
|
||||
<Button type="primary" icon="plus" onClick={() => store.showForm()}>新建</Button>
|
||||
</AuthDiv>
|
||||
<ComTable/>
|
||||
</AuthCard>
|
||||
{store.formVisible && <ComForm/>}
|
||||
{store.infoVisible && <Info/>}
|
||||
{store.recordVisible && <Record/>}
|
||||
</AuthDiv>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* 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') => {
|
||||
|
|
Loading…
Reference in New Issue