mirror of https://github.com/openspug/spug
update
parent
11bf8d1f7d
commit
54cb817fc4
|
@ -8,6 +8,7 @@ from .views import *
|
|||
urlpatterns = [
|
||||
path('request/', RequestView.as_view()),
|
||||
path('request/1/', post_request_1),
|
||||
path('request/2/', post_request_2),
|
||||
path('request/upload/', do_upload),
|
||||
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
||||
]
|
||||
|
|
|
@ -29,7 +29,6 @@ def dispatch(req):
|
|||
api_token = uuid.uuid4().hex
|
||||
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
||||
helper = Helper(rds, rds_key)
|
||||
# helper.send_step('local', 1, f'完成\r\n{human_time()} 发布准备... ')
|
||||
env = AttrDict(
|
||||
SPUG_APP_NAME=req.deploy.app.name,
|
||||
SPUG_APP_ID=str(req.deploy.app_id),
|
||||
|
@ -80,18 +79,16 @@ def _ext1_deploy(req, helper, env):
|
|||
|
||||
|
||||
def _ext2_deploy(req, helper, env):
|
||||
extend = req.deploy.extend_obj
|
||||
extras = json.loads(req.extra)
|
||||
helper.send_info('local', f'完成\r\n')
|
||||
extend, step = req.deploy.extend_obj, 1
|
||||
host_actions = json.loads(extend.host_actions)
|
||||
server_actions = json.loads(extend.server_actions)
|
||||
if extras and extras[0]:
|
||||
env.update({'SPUG_RELEASE': extras[0]})
|
||||
step = 2
|
||||
env.update({'SPUG_RELEASE': req.version})
|
||||
for action in server_actions:
|
||||
helper.send_step('local', step, f'\r\n{human_time()} {action["title"]}...\r\n')
|
||||
helper.send_step('local', step, f'{human_time()} {action["title"]}...\r\n')
|
||||
helper.local(f'cd /tmp && {action["data"]}', env)
|
||||
step += 1
|
||||
helper.send_step('local', 100, '完成\r\n' if step == 2 else '\r\n')
|
||||
helper.send_step('local', 100, '\r\n')
|
||||
|
||||
tmp_transfer_file = None
|
||||
for action in host_actions:
|
||||
|
@ -119,17 +116,18 @@ def _ext2_deploy(req, helper, env):
|
|||
else:
|
||||
excludes.append(f'--exclude={x}')
|
||||
exclude = ' '.join(excludes)
|
||||
tar_gz_file = f'{env.SPUG_VERSION}.tar.gz'
|
||||
tar_gz_file = f'{env.spug_version}.tar.gz'
|
||||
helper.local(f'cd {sp_dir} && tar zcf {tar_gz_file} {exclude} {contain}')
|
||||
helper.send_info('local', '完成\r\n')
|
||||
tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
|
||||
break
|
||||
if host_actions:
|
||||
threads, latest_exception = [], None
|
||||
with futures.ThreadPoolExecutor(max_workers=min(10, os.cpu_count() + 5)) as executor:
|
||||
max_workers = min(10, os.cpu_count() * 4) if req.deploy.is_parallel else 1
|
||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for h_id in json.loads(req.host_ids):
|
||||
env = AttrDict(env.items())
|
||||
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, env)
|
||||
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, env, req.spug_version)
|
||||
t.h_id = h_id
|
||||
threads.append(t)
|
||||
for t in futures.as_completed(threads):
|
||||
|
@ -190,23 +188,22 @@ def _deploy_ext1_host(req, helper, h_id, env):
|
|||
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
||||
helper.remote(host.id, ssh, command, env)
|
||||
|
||||
helper.send_step(h_id, 5, f'\r\n{human_time()} ** 发布成功 **')
|
||||
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
|
||||
|
||||
|
||||
def _deploy_ext2_host(helper, h_id, actions, env):
|
||||
helper.send_step(h_id, 1, f'{human_time()} 数据准备... ')
|
||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||
helper.send_info(h_id, '就绪\r\n')
|
||||
host = Host.objects.filter(pk=h_id).first()
|
||||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
ssh = host.get_ssh()
|
||||
helper.send_step(h_id, 2, '完成\r\n')
|
||||
for index, action in enumerate(actions):
|
||||
helper.send_step(h_id, 2 + index, f'{human_time()} {action["title"]}...\r\n')
|
||||
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
|
||||
if action.get('type') == 'transfer':
|
||||
if action.get('src_mode') == '1':
|
||||
try:
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, env.SPUG_VERSION), action['dst'])
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), action['dst'])
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
helper.send_info(host.id, 'transfer completed\r\n')
|
||||
|
|
|
@ -43,6 +43,7 @@ class RequestView(View):
|
|||
tmp['app_name'] = item.app_name
|
||||
tmp['app_extend'] = item.app_extend
|
||||
tmp['host_ids'] = json.loads(item.host_ids)
|
||||
tmp['extra'] = json.loads(item.extra) if item.extra else None
|
||||
tmp['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
|
||||
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
||||
tmp['status_alias'] = item.get_status_display()
|
||||
|
@ -50,45 +51,6 @@ class RequestView(View):
|
|||
data.append(tmp)
|
||||
return json_response(data)
|
||||
|
||||
def post(self, request):
|
||||
form, error = JsonParser(
|
||||
Argument('id', type=int, required=False),
|
||||
Argument('deploy_id', type=int, help='缺少必要参数'),
|
||||
Argument('name', help='请输申请标题'),
|
||||
Argument('extra', type=list, help='缺少必要参数'),
|
||||
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
|
||||
Argument('desc', required=False),
|
||||
).parse(request.body)
|
||||
if error is None:
|
||||
deploy = Deploy.objects.filter(pk=form.deploy_id).first()
|
||||
if not deploy:
|
||||
return json_response(error='未找到该发布配置')
|
||||
if deploy.extend == '2':
|
||||
if form.extra[0]:
|
||||
form.extra[0] = form.extra[0].replace("'", '')
|
||||
if DeployExtend2.objects.filter(deploy=deploy, host_actions__contains='"src_mode": "1"').exists():
|
||||
if len(form.extra) < 2:
|
||||
return json_response(error='该应用的发布配置中使用了数据传输动作且设置为发布时上传,请上传要传输的数据')
|
||||
form.version = form.extra[1].get('path')
|
||||
form.name = form.name.replace("'", '')
|
||||
form.status = '0' if deploy.is_audit else '1'
|
||||
form.extra = json.dumps(form.extra)
|
||||
form.host_ids = json.dumps(form.host_ids)
|
||||
if form.id:
|
||||
req = DeployRequest.objects.get(pk=form.id)
|
||||
is_required_notify = deploy.is_audit and req.status == '-1'
|
||||
DeployRequest.objects.filter(pk=form.id).update(
|
||||
created_by=request.user,
|
||||
reason=None,
|
||||
**form
|
||||
)
|
||||
else:
|
||||
req = DeployRequest.objects.create(created_by=request.user, **form)
|
||||
is_required_notify = deploy.is_audit
|
||||
if is_required_notify:
|
||||
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_req')).start()
|
||||
return json_response(error=error)
|
||||
|
||||
def put(self, request):
|
||||
form, error = JsonParser(
|
||||
Argument('id', type=int, help='缺少必要参数'),
|
||||
|
@ -162,11 +124,12 @@ class RequestDetailView(View):
|
|||
if not req:
|
||||
return json_response(error='未找到指定发布申请')
|
||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
||||
server_actions, host_actions = [], []
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': []} for x in hosts}
|
||||
response = {'outputs': outputs, 'status': req.status}
|
||||
if req.deploy.extend == '2':
|
||||
server_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||
host_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||
outputs['local'] = {'id': 'local', 'data': []}
|
||||
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
|
||||
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
|
||||
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
|
||||
data = rds.lrange(key, counter, counter + 9)
|
||||
while data:
|
||||
|
@ -180,12 +143,7 @@ class RequestDetailView(View):
|
|||
if 'status' in item:
|
||||
outputs[item['key']]['status'] = item['status']
|
||||
data = rds.lrange(key, counter, counter + 9)
|
||||
return json_response({
|
||||
'server_actions': server_actions,
|
||||
'host_actions': host_actions,
|
||||
'outputs': outputs,
|
||||
'status': req.status
|
||||
})
|
||||
return json_response(response)
|
||||
|
||||
def post(self, request, r_id):
|
||||
query = {'pk': r_id}
|
||||
|
@ -206,7 +164,13 @@ class RequestDetailView(View):
|
|||
req.do_by = request.user
|
||||
req.save()
|
||||
Thread(target=dispatch, args=(req,)).start()
|
||||
return json_response({'type': req.type, 'outputs': outputs})
|
||||
if req.deploy.extend == '2':
|
||||
message = f'{human_time()} 建立连接... '
|
||||
outputs['local'] = {'id': 'local', 'step': 0, 'data': [message]}
|
||||
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||
return json_response({'s_actions': s_actions, 'h_actions': h_actions, 'outputs': outputs})
|
||||
return json_response({'outputs': outputs})
|
||||
|
||||
def patch(self, request, r_id):
|
||||
form, error = JsonParser(
|
||||
|
@ -261,6 +225,43 @@ def post_request_1(request):
|
|||
return json_response(error=error)
|
||||
|
||||
|
||||
def post_request_2(request):
|
||||
form, error = JsonParser(
|
||||
Argument('id', type=int, required=False),
|
||||
Argument('deploy_id', type=int, help='缺少必要参数'),
|
||||
Argument('name', help='请输申请标题'),
|
||||
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
|
||||
Argument('extra', type=dict, required=False),
|
||||
Argument('version', required=False),
|
||||
Argument('desc', required=False),
|
||||
).parse(request.body)
|
||||
if error is None:
|
||||
deploy = Deploy.objects.filter(pk=form.deploy_id).first()
|
||||
if not deploy:
|
||||
return json_response(error='未找到该发布配置')
|
||||
extra = form.pop('extra')
|
||||
if DeployExtend2.objects.filter(deploy=deploy, host_actions__contains='"src_mode": "1"').exists():
|
||||
if not extra:
|
||||
return json_response(error='该应用的发布配置中使用了数据传输动作且设置为发布时上传,请上传要传输的数据')
|
||||
form.spug_version = extra['path']
|
||||
form.extra = json.dumps(extra)
|
||||
else:
|
||||
form.spug_version = Repository.make_spug_version(deploy.id)
|
||||
form.name = form.name.replace("'", '')
|
||||
form.status = '0' if deploy.is_audit else '1'
|
||||
form.host_ids = json.dumps(form.host_ids)
|
||||
if form.id:
|
||||
req = DeployRequest.objects.get(pk=form.id)
|
||||
is_required_notify = deploy.is_audit and req.status == '-1'
|
||||
DeployRequest.objects.filter(pk=form.id).update(created_by=request.user, reason=None, **form)
|
||||
else:
|
||||
req = DeployRequest.objects.create(created_by=request.user, **form)
|
||||
is_required_notify = deploy.is_audit
|
||||
if is_required_notify:
|
||||
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_req')).start()
|
||||
return json_response(error=error)
|
||||
|
||||
|
||||
def do_upload(request):
|
||||
repos_dir = settings.REPOS_DIR
|
||||
file = request.FILES['file']
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Modal, Steps } from 'antd';
|
|||
import styles from './index.module.css';
|
||||
import Setup1 from './Ext2Setup1';
|
||||
import Setup2 from './Ext2Setup2';
|
||||
import Setup3 from './Ext2Setup3';
|
||||
import store from './store';
|
||||
|
||||
export default observer(function Ext2From() {
|
||||
|
@ -30,12 +29,10 @@ export default observer(function Ext2From() {
|
|||
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>
|
||||
{store.page === 0 && <Setup1/>}
|
||||
{store.page === 1 && <Setup2/>}
|
||||
{store.page === 2 && <Setup3/>}
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Form, Switch, Select, Button, Input, Radio } from "antd";
|
||||
import { Form, Switch, Select, Button, Input, Radio } from 'antd';
|
||||
import envStore from 'pages/config/environment/store';
|
||||
import Selector from 'pages/host/Selector';
|
||||
import store from './store';
|
||||
|
||||
export default observer(function Ext2Setup1() {
|
||||
const [envs, setEnvs] = useState([]);
|
||||
const [selectorVisible, setSelectorVisible] = useState(false);
|
||||
|
||||
function updateEnvs() {
|
||||
const ids = store.currentRecord['deploys'].map(x => x.env_id);
|
||||
|
@ -41,6 +43,10 @@ export default observer(function Ext2Setup1() {
|
|||
<Link disabled={store.isReadOnly} to="/config/environment">新建环境</Link>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item required label="目标主机">
|
||||
{info.host_ids.length > 0 && `已选择 ${info.host_ids.length} 台`}
|
||||
<Button type="link" onClick={() => setSelectorVisible(true)}>选择主机</Button>
|
||||
</Form.Item>
|
||||
<Form.Item label="发布模式">
|
||||
<Radio.Group buttonStyle="solid" value={info.is_parallel} onChange={e => info.is_parallel = e.target.value}>
|
||||
<Radio.Button value={true}>并行</Radio.Button>
|
||||
|
@ -82,6 +88,11 @@ export default observer(function Ext2Setup1() {
|
|||
disabled={!info.env_id}
|
||||
onClick={() => store.page += 1}>下一步</Button>
|
||||
</Form.Item>
|
||||
<Selector
|
||||
visible={selectorVisible}
|
||||
selectedRowKeys={[...info.host_ids]}
|
||||
onCancel={() => setSelectorVisible(false)}
|
||||
onOk={(_, ids) => info.host_ids = ids}/>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -6,56 +6,186 @@
|
|||
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 { Form, Input, Button, message, Divider, Alert, Select } from 'antd';
|
||||
import Editor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-sh';
|
||||
import 'ace-builds/src-noconflict/theme-tomorrow';
|
||||
import styles from './index.module.css';
|
||||
import { http, cleanCommand } from 'libs';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
|
||||
@observer
|
||||
class Ext2Setup2 extends React.Component {
|
||||
componentDidMount() {
|
||||
if (hostStore.records.length === 0) {
|
||||
hostStore.fetchRecords()
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.helpMap = {
|
||||
'0': null,
|
||||
'1': '相对于输入的本地路径的文件路径,仅将匹配到文件传输至要发布的目标主机。',
|
||||
'2': '支持模糊匹配,如果路径以 / 开头则基于输入的本地路径匹配,匹配到文件将不会被传输。'
|
||||
}
|
||||
this.state = {
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
handleSubmit = () => {
|
||||
this.setState({loading: true});
|
||||
const info = store.deploy;
|
||||
info['app_id'] = store.app_id;
|
||||
info['extend'] = '2';
|
||||
info['host_actions'] = info['host_actions'].filter(x => (x.title && x.data) || (x.title && (x.src || x.src_mode === '1') && x.dst));
|
||||
info['server_actions'] = info['server_actions'].filter(x => x.title && x.data);
|
||||
http.post('/api/app/deploy/', info)
|
||||
.then(res => {
|
||||
message.success('保存成功');
|
||||
store.ext2Visible = false;
|
||||
store.loadDeploys(store.app_id)
|
||||
}, () => this.setState({loading: false}))
|
||||
};
|
||||
|
||||
render() {
|
||||
const server_actions = store.deploy['server_actions'];
|
||||
const host_actions = store.deploy['host_actions'];
|
||||
return (
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required label="发布目标主机">
|
||||
{info['host_ids'].map((id, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Select
|
||||
value={id}
|
||||
showSearch
|
||||
disabled={store.isReadOnly}
|
||||
placeholder="请选择"
|
||||
optionFilterProp="children"
|
||||
style={{width: '80%', marginRight: 10, marginBottom: 12}}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => store.editHost(index, v)}>
|
||||
{hostStore.records.filter(x => hasHostPermission(x.id)).map(item => (
|
||||
<Select.Option key={item.id} value={item.id} disabled={info['host_ids'].includes(item.id)}>
|
||||
{`${item.name}(${item['hostname']}:${item['port']})`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{!store.isReadOnly && info['host_ids'].length > 1 && (
|
||||
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delHost(index)} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} className={styles.ext2Form}>
|
||||
{store.deploy.id === undefined && (
|
||||
<Alert
|
||||
closable
|
||||
showIcon
|
||||
type="info"
|
||||
message="小提示"
|
||||
style={{margin: '0 80px 20px'}}
|
||||
description={[
|
||||
<p key={1}>Spug 将遵循先本地后目标主机的原则,按照顺序依次执行添加的动作,例如:本地动作1 -> 本地动作2 -> 目标主机动作1 -> 目标主机动作2 ...</p>,
|
||||
<p key={2}>执行的命令内可以使用发布申请中设置的环境变量 SPUG_RELEASE,一般可用于标记一次发布的版本号或提交ID等,在执行的脚本内通过使用 $SPUG_RELEASE
|
||||
获取其值来执行相应操作。</p>
|
||||
]}/>
|
||||
)}
|
||||
{server_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`本地动作${index + 1}`}>
|
||||
<Input disabled={store.isReadOnly} value={item['title']} onChange={e => item['title'] = e.target.value}
|
||||
placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item required label="执行内容">
|
||||
<Editor
|
||||
readOnly={store.isReadOnly}
|
||||
mode="sh"
|
||||
theme="tomorrow"
|
||||
width="100%"
|
||||
height="100px"
|
||||
value={item['data']}
|
||||
onChange={v => item['data'] = cleanCommand(v)}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
{!store.isReadOnly && (
|
||||
<div className={styles.delAction} onClick={() => server_actions.splice(index, 1)}>
|
||||
<MinusCircleOutlined />移除
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!store.isReadOnly && (
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" block onClick={() => server_actions.push({})}>
|
||||
<PlusOutlined />添加本地执行动作(在服务端本地执行)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Divider/>
|
||||
{host_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`目标主机动作${index + 1}`}>
|
||||
<Input disabled={store.isReadOnly} value={item['title']} onChange={e => item['title'] = e.target.value}
|
||||
placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
{item['type'] === 'transfer' ? ([
|
||||
<Form.Item key={0} required label="数据来源">
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={store.isReadOnly || item['src_mode'] === '1'}
|
||||
placeholder="请输入本地(部署spug的容器或主机)路径"
|
||||
value={item['src']}
|
||||
onChange={e => item['src'] = e.target.value}
|
||||
addonBefore={(
|
||||
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['src_mode'] || '0'}
|
||||
onChange={v => item['src_mode'] = v}>
|
||||
<Select.Option value="0">本地路径</Select.Option>
|
||||
<Select.Option value="1">发布时上传</Select.Option>
|
||||
</Select>
|
||||
)}/>
|
||||
</Form.Item>,
|
||||
[undefined, '0'].includes(item['src_mode']) ? (
|
||||
<Form.Item key={1} label="过滤规则" help={this.helpMap[item['mode']]}>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
placeholder="请输入逗号分割的过滤规则"
|
||||
value={item['rule']}
|
||||
onChange={e => item['rule'] = e.target.value.replace(',', ',')}
|
||||
disabled={store.isReadOnly || item['mode'] === '0'}
|
||||
addonBefore={(
|
||||
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['mode']}
|
||||
onChange={v => item['mode'] = v}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">包含</Select.Option>
|
||||
<Select.Option value="2">排除</Select.Option>
|
||||
</Select>
|
||||
)}/>
|
||||
</Form.Item>
|
||||
) : null,
|
||||
<Form.Item key={2} required label="目标路径" extra={<a
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.dev/docs/deploy-config#%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93">使用前请务必阅读官方文档。</a>}>
|
||||
<Input
|
||||
disabled={store.isReadOnly}
|
||||
spellCheck={false}
|
||||
value={item['dst']}
|
||||
placeholder="请输入目标主机路径"
|
||||
onChange={e => item['dst'] = e.target.value}/>
|
||||
</Form.Item>
|
||||
]) : (
|
||||
<Form.Item required label="执行内容">
|
||||
<Editor
|
||||
readOnly={store.isReadOnly}
|
||||
mode="sh"
|
||||
theme="tomorrow"
|
||||
width="100%"
|
||||
height="100px"
|
||||
value={item['data']}
|
||||
onChange={v => item['data'] = cleanCommand(v)}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!store.isReadOnly && (
|
||||
<div className={styles.delAction} onClick={() => host_actions.splice(index, 1)}>
|
||||
<MinusCircleOutlined />移除
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!store.isReadOnly && (
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={store.isReadOnly} type="dashed" block onClick={() => host_actions.push({})}>
|
||||
<PlusOutlined />添加目标主机执行动作(在部署目标主机执行)
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
type="dashed"
|
||||
style={{marginTop: 8}}
|
||||
disabled={store.isReadOnly || lds.findIndex(host_actions, x => x.type === 'transfer') !== -1}
|
||||
onClick={() => host_actions.push({type: 'transfer', title: '数据传输', mode: '0', src_mode: '0'})}>
|
||||
<PlusOutlined />添加数据传输动作(仅能添加一个)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={store.isReadOnly} type="dashed" style={{width: '80%'}} onClick={store.addHost}>
|
||||
<PlusOutlined />添加目标主机
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={info['host_ids'].filter(x => x).length === 0} type="primary"
|
||||
onClick={() => store.page += 1}>下一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={store.isReadOnly || [...host_actions, ...server_actions].filter(x => x.title && x.data).length === 0}
|
||||
loading={this.state.loading}
|
||||
onClick={this.handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
@ -1,196 +0,0 @@
|
|||
/**
|
||||
* 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, Input, Button, message, Divider, Alert, Select } from 'antd';
|
||||
import Editor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-sh';
|
||||
import 'ace-builds/src-noconflict/theme-tomorrow';
|
||||
import styles from './index.module.css';
|
||||
import { http, cleanCommand } from 'libs';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
|
||||
@observer
|
||||
class Ext2Setup3 extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.helpMap = {
|
||||
'0': null,
|
||||
'1': '相对于输入的本地路径的文件路径,仅将匹配到文件传输至要发布的目标主机。',
|
||||
'2': '支持模糊匹配,如果路径以 / 开头则基于输入的本地路径匹配,匹配到文件将不会被传输。'
|
||||
}
|
||||
this.state = {
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.setState({loading: true});
|
||||
const info = store.deploy;
|
||||
info['app_id'] = store.app_id;
|
||||
info['extend'] = '2';
|
||||
info['host_actions'] = info['host_actions'].filter(x => (x.title && x.data) || (x.title && (x.src || x.src_mode === '1') && x.dst));
|
||||
info['server_actions'] = info['server_actions'].filter(x => x.title && x.data);
|
||||
http.post('/api/app/deploy/', info)
|
||||
.then(res => {
|
||||
message.success('保存成功');
|
||||
store.ext2Visible = false;
|
||||
store.loadDeploys(store.app_id)
|
||||
}, () => this.setState({loading: false}))
|
||||
};
|
||||
|
||||
render() {
|
||||
const server_actions = store.deploy['server_actions'];
|
||||
const host_actions = store.deploy['host_actions'];
|
||||
return (
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} className={styles.ext2Form}>
|
||||
{store.deploy.id === undefined && (
|
||||
<Alert
|
||||
closable
|
||||
showIcon
|
||||
type="info"
|
||||
message="小提示"
|
||||
style={{margin: '0 80px 20px'}}
|
||||
description={[
|
||||
<p key={1}>Spug 将遵循先本地后目标主机的原则,按照顺序依次执行添加的动作,例如:本地动作1 -> 本地动作2 -> 目标主机动作1 -> 目标主机动作2 ...</p>,
|
||||
<p key={2}>执行的命令内可以使用发布申请中设置的环境变量 SPUG_RELEASE,一般可用于标记一次发布的版本号或提交ID等,在执行的脚本内通过使用 $SPUG_RELEASE
|
||||
获取其值来执行相应操作。</p>
|
||||
]}/>
|
||||
)}
|
||||
{server_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`本地动作${index + 1}`}>
|
||||
<Input disabled={store.isReadOnly} value={item['title']} onChange={e => item['title'] = e.target.value}
|
||||
placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item required label="执行内容">
|
||||
<Editor
|
||||
readOnly={store.isReadOnly}
|
||||
mode="sh"
|
||||
theme="tomorrow"
|
||||
width="100%"
|
||||
height="100px"
|
||||
value={item['data']}
|
||||
onChange={v => item['data'] = cleanCommand(v)}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
{!store.isReadOnly && (
|
||||
<div className={styles.delAction} onClick={() => server_actions.splice(index, 1)}>
|
||||
<MinusCircleOutlined />移除
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!store.isReadOnly && (
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" block onClick={() => server_actions.push({})}>
|
||||
<PlusOutlined />添加本地执行动作(在服务端本地执行)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Divider/>
|
||||
{host_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`目标主机动作${index + 1}`}>
|
||||
<Input disabled={store.isReadOnly} value={item['title']} onChange={e => item['title'] = e.target.value}
|
||||
placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
{item['type'] === 'transfer' ? ([
|
||||
<Form.Item key={0} required label="数据来源">
|
||||
<Input
|
||||
spellCheck={false}
|
||||
disabled={store.isReadOnly || item['src_mode'] === '1'}
|
||||
placeholder="请输入本地(部署spug的容器或主机)路径"
|
||||
value={item['src']}
|
||||
onChange={e => item['src'] = e.target.value}
|
||||
addonBefore={(
|
||||
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['src_mode'] || '0'}
|
||||
onChange={v => item['src_mode'] = v}>
|
||||
<Select.Option value="0">本地路径</Select.Option>
|
||||
<Select.Option value="1">发布时上传</Select.Option>
|
||||
</Select>
|
||||
)}/>
|
||||
</Form.Item>,
|
||||
[undefined, '0'].includes(item['src_mode']) ? (
|
||||
<Form.Item key={1} label="过滤规则" help={this.helpMap[item['mode']]}>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
placeholder="请输入逗号分割的过滤规则"
|
||||
value={item['rule']}
|
||||
onChange={e => item['rule'] = e.target.value.replace(',', ',')}
|
||||
disabled={store.isReadOnly || item['mode'] === '0'}
|
||||
addonBefore={(
|
||||
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['mode']}
|
||||
onChange={v => item['mode'] = v}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">包含</Select.Option>
|
||||
<Select.Option value="2">排除</Select.Option>
|
||||
</Select>
|
||||
)}/>
|
||||
</Form.Item>
|
||||
) : null,
|
||||
<Form.Item key={2} required label="目标路径" extra={<a
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.dev/docs/deploy-config#%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93">使用前请务必阅读官方文档。</a>}>
|
||||
<Input
|
||||
disabled={store.isReadOnly}
|
||||
spellCheck={false}
|
||||
value={item['dst']}
|
||||
placeholder="请输入目标主机路径"
|
||||
onChange={e => item['dst'] = e.target.value}/>
|
||||
</Form.Item>
|
||||
]) : (
|
||||
<Form.Item required label="执行内容">
|
||||
<Editor
|
||||
readOnly={store.isReadOnly}
|
||||
mode="sh"
|
||||
theme="tomorrow"
|
||||
width="100%"
|
||||
height="100px"
|
||||
value={item['data']}
|
||||
onChange={v => item['data'] = cleanCommand(v)}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!store.isReadOnly && (
|
||||
<div className={styles.delAction} onClick={() => host_actions.splice(index, 1)}>
|
||||
<MinusCircleOutlined />移除
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!store.isReadOnly && (
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={store.isReadOnly} type="dashed" block onClick={() => host_actions.push({})}>
|
||||
<PlusOutlined />添加目标主机执行动作(在部署目标主机执行)
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
type="dashed"
|
||||
style={{marginTop: 8}}
|
||||
disabled={store.isReadOnly || lds.findIndex(host_actions, x => x.type === 'transfer') !== -1}
|
||||
onClick={() => host_actions.push({type: 'transfer', title: '数据传输', mode: '0', src_mode: '0'})}>
|
||||
<PlusOutlined />添加数据传输动作(仅能添加一个)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={store.isReadOnly || [...host_actions, ...server_actions].filter(x => x.title && x.data).length === 0}
|
||||
loading={this.state.loading}
|
||||
onClick={this.handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Ext2Setup3
|
|
@ -85,7 +85,7 @@ function Ext1Console(props) {
|
|||
<Progress
|
||||
key={item.id}
|
||||
percent={(item.step + 1) * 18}
|
||||
status={item.status === 'error' ? 'exception' : 'active'}/>
|
||||
status={item.step === 100 ? 'success' : item.status === 'error' ? 'exception' : 'active'}/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
|
|
|
@ -72,8 +72,8 @@ export default observer(function () {
|
|||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item required label="目标主机" help="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台`}
|
||||
<Form.Item required label="目标主机" tooltip="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台(可选${app_host_ids.length})`}
|
||||
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer, useLocalStore } from 'mobx-react';
|
||||
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
|
||||
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import OutView from './OutView';
|
||||
import { http, X_TOKEN } from 'libs';
|
||||
import styles from './index.module.less';
|
||||
import store from './store';
|
||||
|
||||
function Ext2Console(props) {
|
||||
const outputs = useLocalStore(() => ({local: {id: 'local'}}));
|
||||
const [sActions, setSActions] = useState([]);
|
||||
const [hActions, setHActions] = useState([]);
|
||||
|
||||
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
|
||||
|
||||
function readDeploy() {
|
||||
let socket;
|
||||
http.get(`/api/deploy/request/${props.request.id}/`)
|
||||
.then(res => {
|
||||
setSActions(res.s_actions);
|
||||
setHActions(res.h_actions);
|
||||
Object.assign(outputs, res.outputs)
|
||||
if (res.status === '2') {
|
||||
socket = _makeSocket()
|
||||
}
|
||||
})
|
||||
return () => socket && socket.close()
|
||||
}
|
||||
|
||||
function doDeploy() {
|
||||
let socket;
|
||||
http.post(`/api/deploy/request/${props.request.id}/`)
|
||||
.then(res => {
|
||||
setSActions(res.s_actions);
|
||||
setHActions(res.h_actions);
|
||||
Object.assign(outputs, res.outputs)
|
||||
socket = _makeSocket()
|
||||
})
|
||||
return () => socket && socket.close()
|
||||
}
|
||||
|
||||
function _makeSocket() {
|
||||
let index = 0;
|
||||
const token = props.request.id;
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/request/${token}/?x-token=${X_TOKEN}`);
|
||||
socket.onopen = () => socket.send(String(index));
|
||||
socket.onmessage = e => {
|
||||
if (e.data === 'pong') {
|
||||
socket.send(String(index))
|
||||
} else {
|
||||
index += 1;
|
||||
const {key, data, step, status} = JSON.parse(e.data);
|
||||
if (data !== undefined) outputs[key].data.push(data);
|
||||
if (step !== undefined) outputs[key].step = step;
|
||||
if (status !== undefined) outputs[key].status = status;
|
||||
}
|
||||
}
|
||||
return socket
|
||||
}
|
||||
|
||||
function StepItem(props) {
|
||||
let icon = null;
|
||||
if (props.step === props.item.step && props.item.status !== 'error') {
|
||||
if (props.item.id === 'local' || outputs.local.step === 100) {
|
||||
icon = <LoadingOutlined/>
|
||||
}
|
||||
}
|
||||
return <Steps.Step {...props} icon={icon}/>
|
||||
}
|
||||
|
||||
function switchMiniMode() {
|
||||
const value = store.tabModes[props.request.id];
|
||||
store.tabModes[props.request.id] = !value
|
||||
}
|
||||
|
||||
return store.tabModes[props.request.id] ? (
|
||||
<Card
|
||||
className={styles.item}
|
||||
bodyStyle={{padding: '8px 12px'}}
|
||||
onClick={switchMiniMode}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{props.request.name}</div>
|
||||
<CloseOutlined onClick={() => store.showConsole(props.request, true)}/>
|
||||
</div>
|
||||
<Progress percent={(outputs.local.step + 1) * (90 / (1 + sActions.length)).toFixed(0)}
|
||||
status={outputs.local.step === 100 ? 'success' : outputs.local.status === 'error' ? 'exception' : 'active'}/>
|
||||
{Object.values(outputs).filter(x => x.id !== 'local').map(item => (
|
||||
<Progress
|
||||
key={item.id}
|
||||
percent={item.step * (90 / (hActions.length).toFixed(0))}
|
||||
status={item.step === 100 ? 'success' : item.status === 'error' ? 'exception' : 'active'}/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Modal
|
||||
visible
|
||||
width={1000}
|
||||
footer={null}
|
||||
maskClosable={false}
|
||||
className={styles.console}
|
||||
onCancel={() => store.showConsole(props.request, true)}
|
||||
title={[
|
||||
<span key="1">{props.request.name}</span>,
|
||||
<div key="2" className={styles.miniIcon} onClick={switchMiniMode}>
|
||||
<ShrinkOutlined/>
|
||||
</div>
|
||||
]}>
|
||||
<Collapse defaultActiveKey="0" className={styles.collapse}>
|
||||
<Collapse.Panel header={(
|
||||
<Steps size="small" className={styles.step} current={outputs.local.step} status={outputs.local.status}>
|
||||
<StepItem style={{width: 200}} title="建立连接" item={outputs.local} step={0}/>
|
||||
{sActions.map((item, index) => (
|
||||
<StepItem style={{width: 200}} key={index} title={item.title} item={outputs.local} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
)}>
|
||||
<OutView records={outputs.local.data}/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<Collapse
|
||||
defaultActiveKey="0"
|
||||
className={styles.collapse}
|
||||
style={{marginTop: 24}}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{Object.values(outputs).filter(x => x.id !== 'local').map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}{item.step}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
{hActions.map((action, index) => (
|
||||
<StepItem key={index} title={action.title} item={item} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView records={item.data}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Ext2Console)
|
|
@ -8,10 +8,11 @@ import { observer } from 'mobx-react';
|
|||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Modal, Form, Input, Upload, message, Button } from 'antd';
|
||||
import hostStore from 'pages/host/store';
|
||||
import HostSelector from './HostSelector';
|
||||
import { http, X_TOKEN } from 'libs';
|
||||
import styles from './index.module.less';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
import HostSelector from "./HostSelector";
|
||||
|
||||
export default observer(function () {
|
||||
const [form] = Form.useForm();
|
||||
|
@ -19,17 +20,13 @@ export default observer(function () {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const [host_ids, setHostIds] = useState(lds.clone(store.record.app_host_ids));
|
||||
const [host_ids, setHostIds] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hostStore.records.length === 0) {
|
||||
hostStore.fetchRecords()
|
||||
}
|
||||
const file = lds.get(store, 'record.extra.1');
|
||||
if (file) {
|
||||
file.uid = '0';
|
||||
setFileList([file])
|
||||
}
|
||||
const {app_host_ids, host_ids, extra} = store.record;
|
||||
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||
if (hostStore.records.length === 0) hostStore.fetchRecords();
|
||||
if (store.record.extra) setFileList([{...extra, uid: '0'}])
|
||||
}, [])
|
||||
|
||||
function handleSubmit() {
|
||||
|
@ -39,13 +36,10 @@ export default observer(function () {
|
|||
setLoading(true);
|
||||
const formData = form.getFieldsValue();
|
||||
formData['id'] = store.record.id;
|
||||
formData['deploy_id'] = store.record.deploy_id;
|
||||
formData['extra'] = [formData['extra']];
|
||||
if (fileList.length > 0) {
|
||||
formData['extra'].push(lds.pick(fileList[0], ['path', 'name']))
|
||||
}
|
||||
formData['host_ids'] = host_ids;
|
||||
http.post('/api/deploy/request/', formData)
|
||||
formData['deploy_id'] = store.record.deploy_id;
|
||||
if (fileList.length > 0) formData['extra'] = lds.pick(fileList[0], ['path', 'name']);
|
||||
http.post('/api/deploy/request/2/', formData)
|
||||
.then(res => {
|
||||
message.success('操作成功');
|
||||
store.ext2Visible = false;
|
||||
|
@ -73,6 +67,7 @@ export default observer(function () {
|
|||
return false
|
||||
}
|
||||
|
||||
const {app_host_ids, deploy_id} = store.record;
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
|
@ -82,31 +77,33 @@ export default observer(function () {
|
|||
onCancel={() => store.ext2Visible = false}
|
||||
confirmLoading={loading}
|
||||
onOk={handleSubmit}>
|
||||
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required name="name" initialValue={store.record.name} label="申请标题">
|
||||
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required name="name" label="申请标题">
|
||||
<Input placeholder="请输入申请标题"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="extra"
|
||||
initialValue={lds.get(store.record, 'extra.0')}
|
||||
label="环境变量(SPUG_RELEASE)"
|
||||
help="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $SPUG_RELEASE 来使用该值。">
|
||||
name="version"
|
||||
label="SPUG_RELEASE"
|
||||
tooltip="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $SPUG_RELEASE 来使用该值。">
|
||||
<Input placeholder="请输入环境变量 SPUG_RELEASE 的值"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="上传数据" help="通过数据传输动作来使用上传的文件。">
|
||||
<Form.Item label="上传数据" tooltip="通过数据传输动作来使用上传的文件。" className={styles.upload}>
|
||||
<Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={handleUpload}
|
||||
data={{deploy_id: store.record.deploy_id}} onChange={handleUploadChange}>
|
||||
data={{deploy_id}} onChange={handleUploadChange}>
|
||||
{fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
<Form.Item required label="目标主机" help="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台`}
|
||||
<Form.Item required label="目标主机" tooltip="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台(可选${app_host_ids.length})`}
|
||||
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
<Input placeholder="请输入备注信息"/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{visible && <HostSelector
|
||||
host_ids={host_ids}
|
||||
app_host_ids={store.record.app_host_ids}
|
||||
app_host_ids={app_host_ids}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={ids => setHostIds(ids)}/>}
|
||||
</Modal>
|
||||
|
|
|
@ -40,7 +40,7 @@ function ComTable() {
|
|||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BuildOutlined/> {info.extra[0]}
|
||||
<BuildOutlined/> {info.version}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
@ -96,9 +96,7 @@ function ComTable() {
|
|||
</Action>;
|
||||
case '3':
|
||||
return <Action>
|
||||
<Action.Link
|
||||
auth="deploy.request.do"
|
||||
to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</Action.Link>
|
||||
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||
<Action.Button
|
||||
auth="deploy.request.do"
|
||||
disabled={info.type === '2'}
|
||||
|
@ -130,23 +128,6 @@ function ComTable() {
|
|||
}
|
||||
}];
|
||||
|
||||
function handleRollback(info) {
|
||||
http.put('/api/deploy/request/', {id: info.id, action: 'check'})
|
||||
.then(res => {
|
||||
Modal.confirm({
|
||||
title: '回滚确认',
|
||||
content: `确定要回滚至 ${res['date']} 创建的名称为【${res['name']}】的发布申请版本?`,
|
||||
onOk: () => {
|
||||
return http.put('/api/deploy/request/', {id: info.id, action: 'do'})
|
||||
.then(() => {
|
||||
message.success('回滚申请创建成功');
|
||||
store.fetchRecords()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(info) {
|
||||
Modal.confirm({
|
||||
title: '删除确认',
|
||||
|
|
|
@ -13,6 +13,7 @@ import Ext2Form from './Ext2Form';
|
|||
import Approve from './Approve';
|
||||
import ComTable from './Table';
|
||||
import Ext1Console from './Ext1Console';
|
||||
import Ext2Console from './Ext2Console';
|
||||
import { http, includes } from 'libs';
|
||||
import envStore from 'pages/config/environment/store';
|
||||
import appStore from 'pages/config/app/store';
|
||||
|
@ -115,7 +116,11 @@ function Index() {
|
|||
<Row gutter={12} className={styles.miniConsole}>
|
||||
{store.tabs.map(item => (
|
||||
<Col key={item.id}>
|
||||
<Ext1Console request={item}/>
|
||||
{item.app_extend === '1' ? (
|
||||
<Ext1Console request={item}/>
|
||||
) : (
|
||||
<Ext2Console request={item}/>
|
||||
)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
box-shadow: 0 0 4px rgba(0, 0, 0, .3);
|
||||
border-radius: 5px;
|
||||
|
||||
:global(.ant-progress-text) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -100,6 +104,10 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.upload :global(.ant-upload-list-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.floatBox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
|
|
Loading…
Reference in New Issue