style migrate v3

pull/289/head
vapao 2020-11-27 01:40:16 +08:00
parent 1f2a4249b1
commit 682cbfa8a5
8 changed files with 399 additions and 385 deletions

View File

@ -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>
)
})

View File

@ -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

View File

@ -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>
)
})

View File

@ -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>
)
})

View File

@ -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>
)
})

View File

@ -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}/>
)
}
}

View File

@ -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>
)
})

View File

@ -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') => {