mirror of https://github.com/openspug/spug
A 批量执行支持参数化
parent
8dcf187e1e
commit
9d6b46fcb2
|
@ -14,7 +14,7 @@ def exec_worker_handler(job):
|
||||||
|
|
||||||
|
|
||||||
class Job:
|
class Job:
|
||||||
def __init__(self, key, name, hostname, port, username, pkey, command, interpreter, token=None):
|
def __init__(self, key, name, hostname, port, username, pkey, command, interpreter, params=None, token=None):
|
||||||
self.ssh = SSH(hostname, port, username, pkey)
|
self.ssh = SSH(hostname, port, username, pkey)
|
||||||
self.key = key
|
self.key = key
|
||||||
self.command = self._handle_command(command, interpreter)
|
self.command = self._handle_command(command, interpreter)
|
||||||
|
@ -28,6 +28,8 @@ class Job:
|
||||||
SPUG_SSH_USERNAME=username,
|
SPUG_SSH_USERNAME=username,
|
||||||
SPUG_INTERPRETER=interpreter
|
SPUG_INTERPRETER=interpreter
|
||||||
)
|
)
|
||||||
|
if isinstance(params, dict):
|
||||||
|
self.env.update({f'_SPUG_{k}': str(v) for k, v in params.items()})
|
||||||
|
|
||||||
def _send(self, message, with_expire=False):
|
def _send(self, message, with_expire=False):
|
||||||
if self.rds_cli is None:
|
if self.rds_cli is None:
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ExecTemplate(models.Model, ModelMixin):
|
||||||
interpreter = models.CharField(max_length=20, default='sh')
|
interpreter = models.CharField(max_length=20, default='sh')
|
||||||
host_ids = models.TextField(default='[]')
|
host_ids = models.TextField(default='[]')
|
||||||
desc = models.CharField(max_length=255, null=True)
|
desc = models.CharField(max_length=255, null=True)
|
||||||
|
parameters = models.TextField(default='[]')
|
||||||
created_at = models.CharField(max_length=20, default=human_datetime)
|
created_at = models.CharField(max_length=20, default=human_datetime)
|
||||||
created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
|
created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
|
||||||
updated_at = models.CharField(max_length=20, null=True)
|
updated_at = models.CharField(max_length=20, null=True)
|
||||||
|
@ -26,6 +26,7 @@ class ExecTemplate(models.Model, ModelMixin):
|
||||||
def to_view(self):
|
def to_view(self):
|
||||||
tmp = self.to_dict()
|
tmp = self.to_dict()
|
||||||
tmp['host_ids'] = json.loads(self.host_ids)
|
tmp['host_ids'] = json.loads(self.host_ids)
|
||||||
|
tmp['parameters'] = json.loads(self.parameters)
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -45,8 +46,11 @@ class ExecHistory(models.Model, ModelMixin):
|
||||||
def to_view(self):
|
def to_view(self):
|
||||||
tmp = self.to_dict()
|
tmp = self.to_dict()
|
||||||
tmp['host_ids'] = json.loads(self.host_ids)
|
tmp['host_ids'] = json.loads(self.host_ids)
|
||||||
if hasattr(self, 'template_name'):
|
if self.template:
|
||||||
tmp['template_name'] = self.template_name
|
tmp['template_name'] = self.template.name
|
||||||
|
tmp['interpreter'] = self.template.interpreter
|
||||||
|
tmp['parameters'] = json.loads(self.template.parameters)
|
||||||
|
tmp['command'] = self.template.body
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -31,6 +31,7 @@ class TemplateView(View):
|
||||||
Argument('body', help='请输入模版内容'),
|
Argument('body', help='请输入模版内容'),
|
||||||
Argument('interpreter', default='sh'),
|
Argument('interpreter', default='sh'),
|
||||||
Argument('host_ids', type=list, handler=json.dumps, default=[]),
|
Argument('host_ids', type=list, handler=json.dumps, default=[]),
|
||||||
|
Argument('parameters', type=list, handler=json.dumps, default=[]),
|
||||||
Argument('desc', required=False)
|
Argument('desc', required=False)
|
||||||
).parse(request.body)
|
).parse(request.body)
|
||||||
if error is None:
|
if error is None:
|
||||||
|
@ -59,7 +60,8 @@ def do_task(request):
|
||||||
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'),
|
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'),
|
||||||
Argument('command', help='请输入执行命令内容'),
|
Argument('command', help='请输入执行命令内容'),
|
||||||
Argument('interpreter', default='sh'),
|
Argument('interpreter', default='sh'),
|
||||||
Argument('template_id', type=int, required=False)
|
Argument('template_id', type=int, required=False),
|
||||||
|
Argument('params', type=dict, required=False)
|
||||||
).parse(request.body)
|
).parse(request.body)
|
||||||
if error is None:
|
if error is None:
|
||||||
if not has_host_perm(request.user, form.host_ids):
|
if not has_host_perm(request.user, form.host_ids):
|
||||||
|
@ -76,6 +78,7 @@ def do_task(request):
|
||||||
username=host.username,
|
username=host.username,
|
||||||
command=form.command,
|
command=form.command,
|
||||||
pkey=host.private_key,
|
pkey=host.private_key,
|
||||||
|
params=form.params
|
||||||
)
|
)
|
||||||
rds.rpush(settings.EXEC_WORKER_KEY, json.dumps(data))
|
rds.rpush(settings.EXEC_WORKER_KEY, json.dumps(data))
|
||||||
form.host_ids.sort()
|
form.host_ids.sort()
|
||||||
|
@ -106,5 +109,5 @@ def do_task(request):
|
||||||
|
|
||||||
@auth('exec.task.do')
|
@auth('exec.task.do')
|
||||||
def get_histories(request):
|
def get_histories(request):
|
||||||
records = ExecHistory.objects.filter(user=request.user).annotate(template_name=F('template__name'))
|
records = ExecHistory.objects.filter(user=request.user).select_related('template')
|
||||||
return json_response([x.to_view() for x in records])
|
return json_response([x.to_view() for x in records])
|
||||||
|
|
|
@ -23,13 +23,13 @@ let gCurrent;
|
||||||
|
|
||||||
function OutView(props) {
|
function OutView(props) {
|
||||||
const el = useRef()
|
const el = useRef()
|
||||||
const [term, setTerm] = useState(new Terminal())
|
const [term] = useState(new Terminal());
|
||||||
|
const [fitPlugin] = useState(new FitAddon());
|
||||||
const [current, setCurrent] = useState(Object.keys(store.outputs)[0])
|
const [current, setCurrent] = useState(Object.keys(store.outputs)[0])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
store.tag = ''
|
store.tag = ''
|
||||||
gCurrent = current
|
gCurrent = current
|
||||||
const fitPlugin = new FitAddon()
|
|
||||||
term.setOption('disableStdin', false)
|
term.setOption('disableStdin', false)
|
||||||
term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei')
|
term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei')
|
||||||
term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'})
|
term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'})
|
||||||
|
@ -39,7 +39,6 @@ function OutView(props) {
|
||||||
term.write('\x1b[36m### WebSocket connecting ...\x1b[0m')
|
term.write('\x1b[36m### WebSocket connecting ...\x1b[0m')
|
||||||
const resize = () => fitPlugin.fit();
|
const resize = () => fitPlugin.fit();
|
||||||
window.addEventListener('resize', resize)
|
window.addEventListener('resize', resize)
|
||||||
setTerm(term)
|
|
||||||
|
|
||||||
return () => window.removeEventListener('resize', resize);
|
return () => window.removeEventListener('resize', resize);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -55,6 +54,7 @@ function OutView(props) {
|
||||||
}
|
}
|
||||||
term.write(message)
|
term.write(message)
|
||||||
socket.send('ok');
|
socket.send('ok');
|
||||||
|
fitPlugin.fit()
|
||||||
}
|
}
|
||||||
socket.onmessage = e => {
|
socket.onmessage = e => {
|
||||||
if (e.data === 'pong') {
|
if (e.data === 'pong') {
|
||||||
|
@ -145,8 +145,8 @@ function OutView(props) {
|
||||||
<div className={style.title}>{store.outputs[current].title}</div>
|
<div className={style.title}>{store.outputs[current].title}</div>
|
||||||
<CodeOutlined className={style.icon} onClick={() => openTerminal(current)}/>
|
<CodeOutlined className={style.icon} onClick={() => openTerminal(current)}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.term}>
|
<div className={style.termContainer}>
|
||||||
<div ref={el} style={{width: '100%'}}/>
|
<div ref={el} className={style.term}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* 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 { Modal, Form, Input, Select, message } from 'antd';
|
||||||
|
|
||||||
|
|
||||||
|
function Render(props) {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'string':
|
||||||
|
return <Input value={props.value} onChange={props.onChange} placeholder="请输入"/>
|
||||||
|
case 'password':
|
||||||
|
return <Input.Password value={props.value} onChange={props.onChange} placeholder="请输入"/>
|
||||||
|
case 'select':
|
||||||
|
const options = props.options.split('\n').map(x => x.split(':'))
|
||||||
|
return (
|
||||||
|
<Select value={props.value} onChange={props.onChange} placeholder="请选择">
|
||||||
|
{options.map((item, index) => item.length > 1 ? (
|
||||||
|
<Select.Option key={index} value={item[0]}>{item[1]}</Select.Option>
|
||||||
|
) : (
|
||||||
|
<Select.Option key={index} value={item[0]}>{item[0]}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Parameter(props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const formData = form.getFieldsValue();
|
||||||
|
for (let item of props.parameters.filter(x => x.required)) {
|
||||||
|
if (!formData[item.variable]) {
|
||||||
|
return message.error(`${item.name} 是必填项。`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.onOk(formData);
|
||||||
|
props.onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={600}
|
||||||
|
maskClosable={false}
|
||||||
|
title="执行任务"
|
||||||
|
onCancel={props.onCancel}
|
||||||
|
okText="立即执行"
|
||||||
|
onOk={handleSubmit}>
|
||||||
|
<Form form={form} initialValues={props.parameter} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
|
{props.parameters.map(item => (
|
||||||
|
<Form.Item required={item.required} key={item.variable} name={item.variable} label={item.name}
|
||||||
|
tooltip={item.desc} initialValue={item.default}>
|
||||||
|
<Render type={item.type} options={item.options}/>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { Form, Button, Alert, Radio, Tooltip } from 'antd';
|
||||||
import { ACEditor, AuthDiv, Breadcrumb } from 'components';
|
import { ACEditor, AuthDiv, Breadcrumb } from 'components';
|
||||||
import Selector from 'pages/host/Selector';
|
import Selector from 'pages/host/Selector';
|
||||||
import TemplateSelector from './TemplateSelector';
|
import TemplateSelector from './TemplateSelector';
|
||||||
|
import Parameter from './Parameter';
|
||||||
import Output from './Output';
|
import Output from './Output';
|
||||||
import { http, cleanCommand } from 'libs';
|
import { http, cleanCommand } from 'libs';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
@ -22,6 +23,8 @@ function TaskIndex() {
|
||||||
const [command, setCommand] = useState('')
|
const [command, setCommand] = useState('')
|
||||||
const [template_id, setTemplateId] = useState()
|
const [template_id, setTemplateId] = useState()
|
||||||
const [histories, setHistories] = useState([])
|
const [histories, setHistories] = useState([])
|
||||||
|
const [parameters, setParameters] = useState([])
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
@ -30,6 +33,12 @@ function TaskIndex() {
|
||||||
}
|
}
|
||||||
}, [loading])
|
}, [loading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!command) {
|
||||||
|
setParameters([])
|
||||||
|
}
|
||||||
|
}, [command])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
store.host_ids = []
|
store.host_ids = []
|
||||||
|
@ -39,9 +48,12 @@ function TaskIndex() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit(params) {
|
||||||
|
if (!params && parameters.length > 0) {
|
||||||
|
return setVisible(true)
|
||||||
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const formData = {interpreter, template_id, host_ids: store.host_ids, command: cleanCommand(command)}
|
const formData = {interpreter, template_id, params, host_ids: store.host_ids, command: cleanCommand(command)}
|
||||||
http.post('/api/exec/do/', formData)
|
http.post('/api/exec/do/', formData)
|
||||||
.then(store.switchConsole)
|
.then(store.switchConsole)
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
|
@ -52,12 +64,14 @@ function TaskIndex() {
|
||||||
setTemplateId(tpl.id)
|
setTemplateId(tpl.id)
|
||||||
setInterpreter(tpl.interpreter)
|
setInterpreter(tpl.interpreter)
|
||||||
setCommand(tpl.body)
|
setCommand(tpl.body)
|
||||||
|
setParameters(tpl.parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(item) {
|
function handleClick(item) {
|
||||||
setTemplateId(item.template_id)
|
setTemplateId(item.template_id)
|
||||||
setInterpreter(item.interpreter)
|
setInterpreter(item.interpreter)
|
||||||
setCommand(item.command)
|
setCommand(item.command)
|
||||||
|
setParameters(item.parameters || [])
|
||||||
store.host_ids = item.host_ids
|
store.host_ids = item.host_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +112,8 @@ function TaskIndex() {
|
||||||
<Button style={{float: 'right'}} icon={<PlusOutlined/>} onClick={store.switchTemplate}>从执行模版中选择</Button>
|
<Button style={{float: 'right'}} icon={<PlusOutlined/>} onClick={store.switchTemplate}>从执行模版中选择</Button>
|
||||||
<ACEditor className={style.editor} mode={interpreter} value={command} width="100%" onChange={setCommand}/>
|
<ACEditor className={style.editor} mode={interpreter} value={command} width="100%" onChange={setCommand}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button loading={loading} icon={<ThunderboltOutlined/>} type="primary" onClick={handleSubmit}>开始执行</Button>
|
<Button loading={loading} icon={<ThunderboltOutlined/>} type="primary"
|
||||||
|
onClick={() => handleSubmit()}>开始执行</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className={style.right}>
|
<div className={style.right}>
|
||||||
|
@ -124,14 +139,15 @@ function TaskIndex() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{store.showTemplate &&
|
{store.showTemplate && <TemplateSelector onCancel={store.switchTemplate} onOk={handleTemplate}/>}
|
||||||
<TemplateSelector onCancel={store.switchTemplate} onOk={handleTemplate}/>}
|
|
||||||
{store.showConsole && <Output onBack={store.switchConsole}/>}
|
{store.showConsole && <Output onBack={store.switchConsole}/>}
|
||||||
|
{visible && <Parameter parameters={parameters} onCancel={() => setVisible(false)} onOk={v => handleSubmit(v)}/>}
|
||||||
<Selector
|
<Selector
|
||||||
visible={store.showHost}
|
visible={store.showHost}
|
||||||
selectedRowKeys={[...store.host_ids]}
|
selectedRowKeys={[...store.host_ids]}
|
||||||
onCancel={() => store.showHost = false}
|
onCancel={() => store.showHost = false}
|
||||||
onOk={(_, ids) => store.host_ids = ids}/>
|
onOk={(_, ids) => store.host_ids = ids}/>
|
||||||
|
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
|
max-width: 600px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
|
@ -253,11 +254,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.term {
|
.termContainer {
|
||||||
flex: 1;
|
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 8px 0 8px 12px;
|
padding: 8px 0 4px 12px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
|
.term {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 300px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,32 +3,41 @@
|
||||||
* Copyright (c) <spug.dev@gmail.com>
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Modal, Form, Input, Select, Button, Radio, message } from 'antd';
|
import { Modal, Form, Input, Select, Button, Radio, Table, Tooltip, message } from 'antd';
|
||||||
import { ACEditor } from 'components';
|
import { ACEditor } from 'components';
|
||||||
import Selector from 'pages/host/Selector';
|
import Selector from 'pages/host/Selector';
|
||||||
|
import Parameter from './Parameter';
|
||||||
import { http, cleanCommand } from 'libs';
|
import { http, cleanCommand } from 'libs';
|
||||||
import store from './store';
|
import lds from 'lodash';
|
||||||
|
import S from './store';
|
||||||
|
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [body, setBody] = useState(store.record.body);
|
const [body, setBody] = useState(S.record.body);
|
||||||
|
const [parameter, setParameter] = useState();
|
||||||
|
const [parameters, setParameters] = useState([]);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setParameters(S.record.parameters)
|
||||||
|
}, [])
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const formData = form.getFieldsValue();
|
const formData = form.getFieldsValue();
|
||||||
formData['id'] = store.record.id;
|
formData['id'] = S.record.id;
|
||||||
formData['body'] = cleanCommand(body);
|
formData['body'] = cleanCommand(body);
|
||||||
formData['host_ids'] = store.record.host_ids;
|
formData['host_ids'] = S.record.host_ids;
|
||||||
|
formData['parameters'] = parameters;
|
||||||
http.post('/api/exec/template/', formData)
|
http.post('/api/exec/template/', formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
store.formVisible = false;
|
S.formVisible = false;
|
||||||
store.fetchRecords()
|
S.fetchRecords()
|
||||||
}, () => setLoading(false))
|
}, () => setLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,28 +55,45 @@ export default observer(function () {
|
||||||
),
|
),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
if (type) {
|
if (type) {
|
||||||
store.types.push(type);
|
S.types.push(type);
|
||||||
form.setFieldsValue({type})
|
form.setFieldsValue({type})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = store.record;
|
function updateParameter(data) {
|
||||||
|
if (data.id) {
|
||||||
|
const index = lds.findIndex(parameters, {id: data.id})
|
||||||
|
parameters[index] = data
|
||||||
|
} else {
|
||||||
|
data.id = parameters.length + 1
|
||||||
|
parameters.push(data)
|
||||||
|
}
|
||||||
|
setParameters([...parameters])
|
||||||
|
setParameter(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function delParameter(index) {
|
||||||
|
parameters.splice(index, 1)
|
||||||
|
setParameters([...parameters])
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = S.record;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
width={800}
|
width={800}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
title={store.record.id ? '编辑模板' : '新建模板'}
|
title={S.record.id ? '编辑模板' : '新建模板'}
|
||||||
onCancel={() => store.formVisible = false}
|
onCancel={() => S.formVisible = false}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
onOk={handleSubmit}>
|
onOk={handleSubmit}>
|
||||||
<Form form={form} initialValues={info} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
<Form form={form} initialValues={info} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
<Form.Item required label="模板类型" style={{marginBottom: 0}}>
|
<Form.Item required label="模板类型" style={{marginBottom: 0}}>
|
||||||
<Form.Item name="type" style={{display: 'inline-block', width: 'calc(75%)', marginRight: 8}}>
|
<Form.Item name="type" style={{display: 'inline-block', width: 'calc(75%)', marginRight: 8}}>
|
||||||
<Select placeholder="请选择模板类型">
|
<Select placeholder="请选择模板类型">
|
||||||
{store.types.map(item => (
|
{S.types.map(item => (
|
||||||
<Select.Option value={item} key={item}>{item}</Select.Option>
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -91,9 +117,24 @@ export default observer(function () {
|
||||||
mode={getFieldValue('interpreter')}
|
mode={getFieldValue('interpreter')}
|
||||||
value={body}
|
value={body}
|
||||||
onChange={val => setBody(val)}
|
onChange={val => setBody(val)}
|
||||||
height="300px"/>
|
height="250px"/>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="参数化">
|
||||||
|
{parameters.length > 0 && (
|
||||||
|
<Table pagination={false} bordered rowKey="id" size="small" dataSource={parameters}>
|
||||||
|
<Table.Column title="参数名" dataIndex="name"
|
||||||
|
render={(_, row) => <Tooltip title={row.desc}>{row.name}</Tooltip>}/>
|
||||||
|
<Table.Column title="变量名" dataIndex="variable"/>
|
||||||
|
<Table.Column title="操作" width={90} render={(item, _, index) => [
|
||||||
|
<Button key="1" type="link" icon={<EditOutlined/>} onClick={() => setParameter(item)}/>,
|
||||||
|
<Button danger key="2" type="link" icon={<DeleteOutlined/>} onClick={() => delParameter(index)}/>
|
||||||
|
]}>
|
||||||
|
</Table.Column>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
<Button type="link" style={{padding: 0}} onClick={() => setParameter({})}>添加参数</Button>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label="目标主机">
|
<Form.Item label="目标主机">
|
||||||
{info.host_ids.length > 0 && <span style={{marginRight: 16}}>已选择 {info.host_ids.length} 台</span>}
|
{info.host_ids.length > 0 && <span style={{marginRight: 16}}>已选择 {info.host_ids.length} 台</span>}
|
||||||
<Button type="link" style={{padding: 0}} onClick={() => setVisible(true)}>选择主机</Button>
|
<Button type="link" style={{padding: 0}} onClick={() => setVisible(true)}>选择主机</Button>
|
||||||
|
@ -107,6 +148,13 @@ export default observer(function () {
|
||||||
selectedRowKeys={[...info.host_ids]}
|
selectedRowKeys={[...info.host_ids]}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
onOk={(_, ids) => info.host_ids = ids}/>
|
onOk={(_, ids) => info.host_ids = ids}/>
|
||||||
|
{parameter ? (
|
||||||
|
<Parameter
|
||||||
|
parameter={parameter}
|
||||||
|
parameters={parameters}
|
||||||
|
onCancel={() => setParameter(null)}
|
||||||
|
onOk={updateParameter}/>
|
||||||
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* 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 { Modal, Form, Input, Radio, Switch, message } from 'antd';
|
||||||
|
import S from './store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
|
export default function Parameter(props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const formData = form.getFieldsValue();
|
||||||
|
console.log(formData)
|
||||||
|
formData.id = props.parameter.id
|
||||||
|
if (!formData.name) return message.error('请输入参数名')
|
||||||
|
if (!formData.variable) return message.error('请输入变量名')
|
||||||
|
if (!formData.type) return message.error('请选择参数类型')
|
||||||
|
if (formData.type === 'select' && !formData.options) return message.error('请输入可选项')
|
||||||
|
const tmp = lds.find(props.parameters, {variable: formData.variable})
|
||||||
|
if (tmp && tmp.id !== formData.id) return message.error('变量名重复')
|
||||||
|
props.onOk(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={600}
|
||||||
|
maskClosable={false}
|
||||||
|
title="编辑参数"
|
||||||
|
onCancel={props.onCancel}
|
||||||
|
onOk={handleSubmit}>
|
||||||
|
<Form form={form} initialValues={props.parameter} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
|
<Form.Item required name="name" label="参数名" tooltip="参数的简短名称。">
|
||||||
|
<Input placeholder="请输入参数名称"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name="variable" label="变量名"
|
||||||
|
tooltip="在脚本使用的变量名称,固定前缀_SPUG_ + 输入的变量名,例如变量名name,则最终生成环境变量为 _SPUG_name">
|
||||||
|
<Input placeholder="请输入变量名"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name="type" label="参数类型" tooltip="不同类型展示的形式不同。">
|
||||||
|
<Radio.Group style={{width: '100%'}}>
|
||||||
|
{Object.entries(S.ParameterTypes).map(([key, val]) => (
|
||||||
|
<Radio.Button key={key} value={key}>{val}</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item noStyle shouldUpdate>
|
||||||
|
{({getFieldValue}) =>
|
||||||
|
['select'].includes(getFieldValue('type')) ? (
|
||||||
|
<Form.Item required name="options" label="可选项" tooltip="每项单独一行,每行可以用英文冒号分割前边是值后边是显示的内容。">
|
||||||
|
<Input.TextArea autoSize={{minRows: 3, maxRows: 5}} placeholder="每行一个选项,例如: test:测试环境"/>
|
||||||
|
</Form.Item>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="required" valuePropName="checked" label="必填" tooltip="该参数是否为必填项">
|
||||||
|
<Switch checkedChildren="是" unCheckedChildren="否"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="default" label="默认值">
|
||||||
|
<Input placeholder="请输入"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="desc" label="提示信息" tooltip="会展示在参数的输入框下方。">
|
||||||
|
<Input placeholder="请输入该参数的帮助提示信息"/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,9 +7,14 @@ import { observable } from "mobx";
|
||||||
import { http, includes } from 'libs';
|
import { http, includes } from 'libs';
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
|
ParameterTypes = {
|
||||||
|
'string': '文本框',
|
||||||
|
'password': '密码框',
|
||||||
|
'select': '下拉选择'
|
||||||
|
}
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable types = [];
|
@observable types = [];
|
||||||
@observable record = {};
|
@observable record = {parameters: []};
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable formVisible = false;
|
@observable formVisible = false;
|
||||||
|
|
||||||
|
@ -33,7 +38,7 @@ class Store {
|
||||||
.finally(() => this.isFetching = false)
|
.finally(() => this.isFetching = false)
|
||||||
};
|
};
|
||||||
|
|
||||||
showForm = (info = {interpreter: 'sh', host_ids: []}) => {
|
showForm = (info = {interpreter: 'sh', host_ids: [], parameters: []}) => {
|
||||||
this.formVisible = true;
|
this.formVisible = true;
|
||||||
this.record = info
|
this.record = info
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue