A 新增任务计划触发器"监控告警",可用于故障自愈

4.0
vapao 2022-10-30 09:58:26 +08:00
parent 7e6cba42a0
commit cc4829772e
14 changed files with 413 additions and 237 deletions

View File

@ -3,7 +3,7 @@
# Released under the AGPL-3.0 License.
from django_redis import get_redis_connection
from apps.host.models import Host
from apps.monitor.utils import handle_notify
from apps.monitor.utils import handle_notify, handle_trigger_event
from socket import socket
import subprocess
import platform
@ -105,6 +105,7 @@ def monitor_worker_handler(job):
if not v_time or int(time.time()) - int(v_time) >= quiet * 60:
rds.hset(key, f_time, int(time.time()))
logging.warning('send fault alarm notification')
handle_trigger_event(task_id, addr if tp in ('3', '4') else None)
handle_notify(task_id, target, is_ok, message, v_count)

View File

@ -4,6 +4,8 @@
from django.db import close_old_connections
from apps.alarm.models import Alarm
from apps.monitor.models import Detection
from apps.schedule.models import Task
from apps.schedule.scheduler import Scheduler
from libs.spug import Notification
import json
@ -41,3 +43,17 @@ def handle_notify(task_id, target, is_ok, out, fault_times):
grp = json.loads(det.notify_grp)
notify = Notification(grp, event, target, det.name, out, duration)
notify.dispatch_monitor(json.loads(det.notify_mode))
def handle_trigger_event(task_id, target):
query = dict(is_active=True, trigger='monitor', trigger_args__regex=fr'[^0-9]{task_id}[^0-9]')
for item in Task.objects.filter(**query):
targets = []
for t in json.loads(item.targets):
if t == 'monitor':
if target:
targets.append(target)
else:
targets.append(t)
if targets:
Scheduler.dispatch(item.id, item.interpreter, item.command, targets)

View File

@ -31,9 +31,9 @@ class History(models.Model, ModelMixin):
class Task(models.Model, ModelMixin):
TRIGGERS = (
('date', '一次性'),
('calendarinterval', '日历间隔'),
('cron', 'UNIX cron'),
('interval', '普通间隔')
('interval', '普通间隔'),
('monitor', '监控告警'),
)
name = models.CharField(max_length=50)
type = models.CharField(max_length=50)
@ -54,12 +54,13 @@ class Task(models.Model, ModelMixin):
def to_dict(self, *args, **kwargs):
tmp = super().to_dict(*args, **kwargs)
tmp['trigger_alias'] = self.get_trigger_display()
tmp['targets'] = json.loads(self.targets)
tmp['latest_status'] = self.latest.status if self.latest else None
tmp['latest_run_time'] = self.latest.run_time if self.latest else None
tmp['latest_status_alias'] = self.latest.get_status_display() if self.latest else None
tmp['rst_notify'] = json.loads(self.rst_notify) if self.rst_notify else {'mode': '0'}
if self.trigger == 'cron':
if self.trigger in ('cron', 'monitor'):
tmp['trigger_args'] = json.loads(self.trigger_args)
return tmp

View File

@ -58,11 +58,8 @@ class Scheduler:
else:
raise TypeError(f'unknown schedule policy: {trigger!r}')
def _init_builtin_jobs(self):
self.scheduler.add_job(auto_run_by_day, 'cron', hour=1, minute=20)
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
def _dispatch(self, task_id, interpreter, command, targets):
@classmethod
def dispatch(cls, task_id, interpreter, command, targets):
output = {x: None for x in targets}
history = History.objects.create(
task_id=task_id,
@ -76,14 +73,18 @@ class Scheduler:
rds_cli.rpush(SCHEDULE_WORKER_KEY, json.dumps([history.id, t, interpreter, command]))
connections.close_all()
def _init_builtin_jobs(self):
self.scheduler.add_job(auto_run_by_day, 'cron', hour=1, minute=20)
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
def _init(self):
self.scheduler.start()
self._init_builtin_jobs()
try:
for task in Task.objects.filter(is_active=True):
for task in Task.objects.filter(is_active=True).exclude(trigger='monitor'):
trigger = self.parse_trigger(task.trigger, task.trigger_args)
self.scheduler.add_job(
self._dispatch,
self.dispatch,
trigger,
id=str(task.id),
args=(task.id, task.interpreter, task.command, json.loads(task.targets)),
@ -103,7 +104,7 @@ class Scheduler:
if task.action in ('add', 'modify'):
trigger = self.parse_trigger(task.trigger, task.trigger_args)
self.scheduler.add_job(
self._dispatch,
self.dispatch,
trigger,
id=str(task.id),
args=(task.id, task.interpreter, task.command, task.targets),

View File

@ -0,0 +1,23 @@
/**
* 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'
function Container(props) {
const {visible, style, className} = props;
return (
<div style={{display: visible ? 'block' : 'none', ...style}} className={className}>
{props.children}
</div>
)
}
Container.defaultProps = {
visible: true
}
export default Container

View File

@ -16,6 +16,7 @@ import TableCard from './TableCard';
import Breadcrumb from './Breadcrumb';
import AppSelector from './AppSelector';
import NotFound from './NotFound';
import Container from './Container';
export {
StatisticsCard,
@ -31,4 +32,5 @@ export {
Breadcrumb,
AppSelector,
NotFound,
Container,
}

View File

@ -42,9 +42,22 @@ class Store {
return records
}
@computed get cascaderOptions() {
let data = {}
for (let i of this.records) {
const tmp = {value: i.id, label: i.name}
if (data[i.group]) {
data[i.group]['children'].push(tmp)
} else {
data[i.group] = {value: i.group, label: i.group, children: [tmp]}
}
}
return Object.values(data)
}
fetchRecords = () => {
this.isFetching = true;
http.get('/api/monitor/')
return http.get('/api/monitor/')
.then(({groups, detections}) => {
const tmp = new Set();
detections.map(item => {

View File

@ -10,7 +10,7 @@ import Step1 from './Step1';
import Step2 from './Step2';
import Step3 from './Step3';
import store from './store';
import styles from './index.module.css';
import styles from './index.module.less';
import hostStore from '../host/store';
export default observer(function () {
@ -28,12 +28,12 @@ export default observer(function () {
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.Step key={1} title="设置触发器"/>
<Steps.Step key={2} title="选择执行对象"/>
</Steps>
{store.page === 0 && <Step1/>}
{store.page === 1 && <Step2/>}
{store.page === 2 && <Step3/>}
<Step1 visible={store.page === 0}/>
<Step2 visible={store.page === 1}/>
<Step3 visible={store.page === 2}/>
</Modal>
)
})

View File

@ -1,13 +1,18 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import { Form, Input, Select, Modal, Button, Radio } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { LinkButton, ACEditor } from 'components';
import { LinkButton, ACEditor, Container } from 'components';
import TemplateSelector from '../exec/task/TemplateSelector';
import { cleanCommand } from 'libs';
import store from './store';
export default observer(function () {
export default observer(function (props) {
const [form] = Form.useForm();
const [showTmp, setShowTmp] = useState(false);
const [command, setCommand] = useState(store.record.command || '');
@ -68,65 +73,68 @@ export default observer(function () {
}
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>
<Container visible={props.visible}>
<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 style={{display: 'inline-block', width: '20%', textAlign: 'right'}}>
<Button type="link" onClick={handleAddZone}>添加类型</Button>
<Form.Item required name="name" label="任务名称">
<Input placeholder="请输入任务名称"/>
</Form.Item>
</Form.Item>
<Form.Item required name="name" label="任务名称">
<Input placeholder="请输入任务名称"/>
</Form.Item>
<Form.Item required label="任务内容" extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
<Form.Item noStyle name="interpreter">
<Radio.Group buttonStyle="solid" style={{marginBottom: 12}}>
<Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button>
<Radio.Button value="python" style={{width: 80, textAlign: 'center'}}>Python</Radio.Button>
</Radio.Group>
<Form.Item required label="任务内容"
extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
<Form.Item noStyle name="interpreter">
<Radio.Group buttonStyle="solid" style={{marginBottom: 12}}>
<Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button>
<Radio.Button value="python" style={{width: 80, textAlign: 'center'}}>Python</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => (
<ACEditor mode={getFieldValue('interpreter')} value={command} width="100%" height="150px"
onChange={setCommand}/>
)}
</Form.Item>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => (
<ACEditor mode={getFieldValue('interpreter')} value={command} width="100%" height="150px"
onChange={setCommand}/>
)}
</Form.Item>
</Form.Item>
<Form.Item label="失败通知" extra={(
<span>
<Form.Item label="失败通知" extra={(
<span>
任务执行失败告警通知
<a target="_blank" rel="noopener noreferrer"
href="https://spug.cc/docs/use-problem#use-dd">钉钉收不到通知</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="4">飞书</Select.Option>
<Select.Option value="3">企业微信</Select.Option>
<Select.Option value="2">Webhook</Select.Option>
</Select>
)}
disabled={store.record.rst_notify.mode === '0'}
placeholder={modePlaceholder}/>
</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={handleSelect} onCancel={() => setShowTmp(false)}/>}
</Form>
<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="4">飞书</Select.Option>
<Select.Option value="3">企业微信</Select.Option>
<Select.Option value="2">Webhook</Select.Option>
</Select>
)}
disabled={store.record.rst_notify.mode === '0'}
placeholder={modePlaceholder}/>
</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={handleSelect} onCancel={() => setShowTmp(false)}/>}
</Form>
</Container>
)
})

View File

@ -3,61 +3,182 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Form, Select, Button } from 'antd';
import HostSelector from 'pages/host/Selector';
import store from './store';
import hostStore from 'pages/host/store';
import styles from './index.module.css';
import { Form, Tabs, DatePicker, InputNumber, Input, Button, Cascader } from 'antd';
import { LoadingOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Container } from 'components';
import { http, includes } from 'libs';
import S from './store';
import moment from 'moment';
import lds from 'lodash';
import styles from './index.module.less';
import mStore from '../monitor/store';
export default observer(function () {
const [visible, setVisible] = useState(false)
let lastFetchId = 0;
export default observer(function (props) {
const [nextRunTime, setNextRunTime] = useState(null);
useEffect(() => {
if (mStore.records.length === 0) {
mStore.fetchRecords()
.then(_initial)
} else {
_initial()
}
}, [])
function _initial() {
if (S.trigger_args?.monitor) {
let args = S.trigger_args.monitor
args = args.map(x => lds.find(mStore.records, {id: x}))
.filter(x => x).map(x => ([x.group, x.id]))
S.trigger_args.monitor = args
}
}
function handleArgs(key, val) {
S.trigger_args[key] = val
}
function handleCronArgs(key, val) {
let tmp = S.trigger_args['cron'] || {};
tmp[key] = val
S.trigger_args['cron'] = tmp
_fetchNextRunTime()
}
function handleMonitorArgs(index, val) {
const tmp = S.trigger_args['monitor'] ?? [[]]
if (index === undefined) {
tmp.push([])
} else if (val) {
tmp[index] = val
} else {
tmp.splice(index, 1)
}
S.trigger_args['monitor'] = tmp
}
function isValid() {
switch (S.trigger) {
case 'cron':
return lds.get(S.trigger_args, 'cron.rule')
case 'monitor':
return lds.get(S.trigger_args, 'monitor', []).map(x => x[1]).filter(x => x).length
default:
return S.trigger_args[S.trigger]
}
}
function _fetchNextRunTime() {
if (S.trigger === 'cron') {
const {rule, start, stop} = lds.get(S.trigger_args, 'cron', {});
if (rule && rule.trim().split(/ +/).length === 5) {
setNextRunTime(<LoadingOutlined/>);
lastFetchId += 1;
const fetchId = lastFetchId;
const formData = {rule}
if (start) formData.start = start.format('YYYY-MM-DD HH:mm:ss')
if (stop) formData.stop = stop.format('YYYY-MM-DD HH:mm:ss')
http.post('/api/schedule/run_time/', formData)
.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 (
<React.Fragment>
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} style={{minHeight: 350}}>
<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.rawRecords.map(item => (
<Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option>
<Container visible={props.visible}>
<Form layout="vertical" wrapperCol={{span: 14, offset: 5}}>
<Form.Item>
<Tabs activeKey={S.trigger} className={styles.tabs} onChange={v => S.trigger = v} tabPosition="left">
<Tabs.TabPane tab="普通间隔" key="interval">
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。">
<InputNumber
min={10}
style={{width: 200}}
placeholder="请输入"
value={S.trigger_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 < moment()}
style={{width: 200}}
placeholder="请选择执行时间"
onOk={() => false}
value={S.trigger_args.date}
onChange={v => handleArgs('date', v)}/>
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="UNIX Cron" key="cron">
<Form.Item required label="执行规则" extra="兼容Cron风格可参考官方例子。">
<Input
suffix={nextRunTime || <span/>}
value={S.trigger_args.cron?.rule}
placeholder="例如每天凌晨1点执行0 1 * * *"
onChange={e => handleCronArgs('rule', e.target.value)}/>
</Form.Item>
<Form.Item label="生效时间" extra="定义的执行规则在到达该时间后生效。">
<DatePicker
showTime
style={{width: '100%'}}
placeholder="可选输入"
value={S.trigger_args.cron?.start}
onChange={v => handleCronArgs('start', v)}/>
</Form.Item>
<Form.Item label="结束时间" extra="执行规则在到达该时间后不再执行。">
<DatePicker
showTime
style={{width: '100%'}}
placeholder="可选输入"
value={S.trigger_args.cron?.stop}
onChange={v => handleCronArgs('stop', v)}/>
</Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="监控告警" key="monitor">
<Form.Item required label="监控项目">
{lds.get(S.trigger_args, 'monitor', [[]]).map((item, index) => (
<React.Fragment key={index}>
<Cascader
placeholder="请选择"
value={item}
options={mStore.cascaderOptions}
style={{width: '80%', marginRight: 10, marginBottom: 12}}
showSearch={{filter: (i, p) => p.some(o => includes(o.label, i))}}
onChange={v => handleMonitorArgs(index, v)}/>
{S.trigger_args.monitor?.length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => handleMonitorArgs(index)}/>
)}
</React.Fragment>
))}
</Select>
{store.targets.length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delTarget(index)}/>
)}
</React.Fragment>
))}
</Form.Item>
<Form.Item extra="当监控项触发告警时执行。">
<Button type="dashed" style={{width: '80%'}} onClick={() => handleMonitorArgs()}>
<PlusOutlined/>添加监控项
</Button>
</Form.Item>
</Tabs.TabPane>
</Tabs>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button type="dashed" style={{width: '80%'}} onClick={() => setVisible(true)}>
<PlusOutlined/>添加执行对象
</Button>
<Button type="primary" disabled={!isValid()} onClick={() => S.page += 1}>下一步</Button>
<Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
</Form.Item>
<HostSelector
visible={visible}
selectedRowKeys={[...store.targets]}
onCancel={() => setVisible(false)}
onOk={(_, ids) => store.targets = ids}/>
</Form>
<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>
</React.Fragment>
</Container>
)
})
})

View File

@ -1,143 +1,103 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
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 { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Form, Select, Button, message } from 'antd';
import { Container } from 'components';
import HostSelector from 'pages/host/Selector';
import hostStore from 'pages/host/store';
import moment from 'moment';
import lds from 'lodash';
import { http } from 'libs';
import S from './store';
import styles from './index.module.less';
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);
export default observer(function (props) {
const [visible, setVisible] = useState(false)
const [loading, setLoading] = useState(false)
function handleSubmit() {
if (trigger === 'date' && args['date'] <= moment()) {
return message.error('任务执行时间不能早于当前时间')
}
setLoading(true)
const formData = lds.pick(store.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']);
formData['targets'] = store.targets.filter(x => x);
formData['trigger'] = trigger;
const formData = lds.pick(S.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']);
formData['targets'] = S.targets.filter(x => x);
formData['trigger'] = S.trigger;
formData['trigger_args'] = _parse_args();
http.post('/api/schedule/', formData)
.then(res => {
message.success('操作成功');
store.formVisible = false;
store.fetchRecords()
S.formVisible = false;
S.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) {
switch (S.trigger) {
case 'date':
return moment(args['date']).format('YYYY-MM-DD HH:mm:ss');
return S.trigger_args.date.format('YYYY-MM-DD HH:mm:ss');
case 'cron':
const {rule, start, stop} = args['cron'];
const {rule, start, stop} = S.trigger_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
});
case 'monitor':
return JSON.stringify(S.trigger_args.monitor.map(x => x[1]).filter(x => x))
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 S.trigger_args[S.trigger];
}
}
return (
<Form layout="vertical" wrapperCol={{span: 14, offset: 5}}>
<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="执行规则" extra="兼容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="生效时间" extra="定义的执行规则在到达该时间后生效">
<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="结束时间" extra="执行规则在到达该时间后不再执行">
<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>
<Container visible={props.visible} style={{width: 420, margin: '0 auto'}}>
<Form layout="vertical" style={{minHeight: 200}}>
<Form.Item required label="执行对象">
{S.targets.map((id, index) => (
<React.Fragment key={index}>
<Select
value={id}
showSearch
placeholder="请选择"
optionFilterProp="children"
style={{width: 'calc(100% - 40px)', marginRight: 10, marginBottom: 12}}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={v => S.editTarget(index, v)}>
<Select.Option value="local" disabled={S.targets.includes('local')}>本机</Select.Option>
{S.trigger === 'monitor' &&
<Select.Option value="monitor" disabled={S.targets.includes('monitor')}>告警关联主机</Select.Option>}
{hostStore.rawRecords.map(item => (
<Select.Option key={item.id} value={item.id} disabled={S.targets.includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option>
))}
</Select>
{S.targets.length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => S.delTarget(index)}/>
)}
</React.Fragment>
))}
</Form.Item>
<Form.Item extra="本机即Spug服务运行所在的容器或主机。">
<Button type="dashed" style={{width: '80%'}} disabled={S.trigger === 'monitor'}
onClick={() => setVisible(true)}>
<PlusOutlined/>添加执行对象
</Button>
</Form.Item>
<Form.Item>
<Button loading={loading} disabled={S.targets.filter(x => x).length === 0} type="primary"
onClick={handleSubmit}>提交</Button>
<Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
</Form.Item>
</Form>
<HostSelector
visible={visible}
selectedRowKeys={[...S.targets]}
onCancel={() => setVisible(false)}
onOk={(_, ids) => S.targets = ids}/>
</Container>
)
})
})

View File

@ -46,6 +46,9 @@ class ComTable extends React.Component {
}, {
title: '任务类型',
dataIndex: 'type',
}, {
title: '触发方式',
dataIndex: 'trigger_alias'
}, {
title: '最新状态',
render: info => {

View File

@ -12,4 +12,12 @@
.delIcon:hover {
color: #f5222d;
}
.tabs {
min-height: 200px;
:global(.ant-tabs-nav-list) {
align-items: flex-end;
}
}

View File

@ -13,6 +13,8 @@ class Store {
@observable record = {};
@observable page = 0;
@observable targets = [undefined];
@observable trigger = 'interval';
@observable trigger_args = {};
@observable isFetching = false;
@observable formVisible = false;
@observable infoVisible = false;
@ -56,7 +58,24 @@ class Store {
showForm = (info) => {
this.page = 0;
this.record = info || {interpreter: 'sh', rst_notify: {mode: '0'}, trigger: 'interval'};
if (info) {
this.record = info
this.trigger = info.trigger
if (info.trigger === 'date') {
this.trigger_args = {date: moment(info.trigger_args)}
} else if (info.trigger === 'cron') {
const args = info.trigger_args
if (args.start) args.start = moment(args.start)
if (args.stop) args.stop = moment(args.stop)
this.trigger_args = {cron: args}
} else {
this.trigger_args = {[info.trigger]: info.trigger_args}
}
} else {
this.record = {interpreter: 'sh', rst_notify: {mode: '0'}, trigger: 'interval'}
this.trigger = 'interval'
this.trigger_args = {}
}
this.formVisible = true
};