mirror of https://github.com/openspug/spug
update
parent
11bf8d1f7d
commit
54cb817fc4
|
@ -8,6 +8,7 @@ from .views import *
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('request/', RequestView.as_view()),
|
path('request/', RequestView.as_view()),
|
||||||
path('request/1/', post_request_1),
|
path('request/1/', post_request_1),
|
||||||
|
path('request/2/', post_request_2),
|
||||||
path('request/upload/', do_upload),
|
path('request/upload/', do_upload),
|
||||||
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -29,7 +29,6 @@ def dispatch(req):
|
||||||
api_token = uuid.uuid4().hex
|
api_token = uuid.uuid4().hex
|
||||||
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
||||||
helper = Helper(rds, rds_key)
|
helper = Helper(rds, rds_key)
|
||||||
# helper.send_step('local', 1, f'完成\r\n{human_time()} 发布准备... ')
|
|
||||||
env = AttrDict(
|
env = AttrDict(
|
||||||
SPUG_APP_NAME=req.deploy.app.name,
|
SPUG_APP_NAME=req.deploy.app.name,
|
||||||
SPUG_APP_ID=str(req.deploy.app_id),
|
SPUG_APP_ID=str(req.deploy.app_id),
|
||||||
|
@ -80,18 +79,16 @@ def _ext1_deploy(req, helper, env):
|
||||||
|
|
||||||
|
|
||||||
def _ext2_deploy(req, helper, env):
|
def _ext2_deploy(req, helper, env):
|
||||||
extend = req.deploy.extend_obj
|
helper.send_info('local', f'完成\r\n')
|
||||||
extras = json.loads(req.extra)
|
extend, step = req.deploy.extend_obj, 1
|
||||||
host_actions = json.loads(extend.host_actions)
|
host_actions = json.loads(extend.host_actions)
|
||||||
server_actions = json.loads(extend.server_actions)
|
server_actions = json.loads(extend.server_actions)
|
||||||
if extras and extras[0]:
|
env.update({'SPUG_RELEASE': req.version})
|
||||||
env.update({'SPUG_RELEASE': extras[0]})
|
|
||||||
step = 2
|
|
||||||
for action in server_actions:
|
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)
|
helper.local(f'cd /tmp && {action["data"]}', env)
|
||||||
step += 1
|
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
|
tmp_transfer_file = None
|
||||||
for action in host_actions:
|
for action in host_actions:
|
||||||
|
@ -119,17 +116,18 @@ def _ext2_deploy(req, helper, env):
|
||||||
else:
|
else:
|
||||||
excludes.append(f'--exclude={x}')
|
excludes.append(f'--exclude={x}')
|
||||||
exclude = ' '.join(excludes)
|
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.local(f'cd {sp_dir} && tar zcf {tar_gz_file} {exclude} {contain}')
|
||||||
helper.send_info('local', '完成\r\n')
|
helper.send_info('local', '完成\r\n')
|
||||||
tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
|
tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
|
||||||
break
|
break
|
||||||
if host_actions:
|
if host_actions:
|
||||||
threads, latest_exception = [], None
|
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):
|
for h_id in json.loads(req.host_ids):
|
||||||
env = AttrDict(env.items())
|
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
|
t.h_id = h_id
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
for t in futures.as_completed(threads):
|
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}'
|
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
||||||
helper.remote(host.id, ssh, command, env)
|
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):
|
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||||
helper.send_step(h_id, 1, f'{human_time()} 数据准备... ')
|
helper.send_info(h_id, '就绪\r\n')
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
if not host:
|
if not host:
|
||||||
helper.send_error(h_id, 'no such host')
|
helper.send_error(h_id, 'no such host')
|
||||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||||
ssh = host.get_ssh()
|
ssh = host.get_ssh()
|
||||||
helper.send_step(h_id, 2, '完成\r\n')
|
|
||||||
for index, action in enumerate(actions):
|
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('type') == 'transfer':
|
||||||
if action.get('src_mode') == '1':
|
if action.get('src_mode') == '1':
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
helper.send_error(host.id, f'exception: {e}')
|
helper.send_error(host.id, f'exception: {e}')
|
||||||
helper.send_info(host.id, 'transfer completed\r\n')
|
helper.send_info(host.id, 'transfer completed\r\n')
|
||||||
|
|
|
@ -43,6 +43,7 @@ class RequestView(View):
|
||||||
tmp['app_name'] = item.app_name
|
tmp['app_name'] = item.app_name
|
||||||
tmp['app_extend'] = item.app_extend
|
tmp['app_extend'] = item.app_extend
|
||||||
tmp['host_ids'] = json.loads(item.host_ids)
|
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['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
|
||||||
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
||||||
tmp['status_alias'] = item.get_status_display()
|
tmp['status_alias'] = item.get_status_display()
|
||||||
|
@ -50,45 +51,6 @@ class RequestView(View):
|
||||||
data.append(tmp)
|
data.append(tmp)
|
||||||
return json_response(data)
|
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):
|
def put(self, request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
Argument('id', type=int, help='缺少必要参数'),
|
Argument('id', type=int, help='缺少必要参数'),
|
||||||
|
@ -162,11 +124,12 @@ class RequestDetailView(View):
|
||||||
if not req:
|
if not req:
|
||||||
return json_response(error='未找到指定发布申请')
|
return json_response(error='未找到指定发布申请')
|
||||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
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}
|
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':
|
if req.deploy.extend == '2':
|
||||||
server_actions = json.loads(req.deploy.extend_obj.server_actions)
|
outputs['local'] = {'id': 'local', 'data': []}
|
||||||
host_actions = json.loads(req.deploy.extend_obj.host_actions)
|
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
|
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
while data:
|
while data:
|
||||||
|
@ -180,12 +143,7 @@ class RequestDetailView(View):
|
||||||
if 'status' in item:
|
if 'status' in item:
|
||||||
outputs[item['key']]['status'] = item['status']
|
outputs[item['key']]['status'] = item['status']
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
return json_response({
|
return json_response(response)
|
||||||
'server_actions': server_actions,
|
|
||||||
'host_actions': host_actions,
|
|
||||||
'outputs': outputs,
|
|
||||||
'status': req.status
|
|
||||||
})
|
|
||||||
|
|
||||||
def post(self, request, r_id):
|
def post(self, request, r_id):
|
||||||
query = {'pk': r_id}
|
query = {'pk': r_id}
|
||||||
|
@ -206,7 +164,13 @@ class RequestDetailView(View):
|
||||||
req.do_by = request.user
|
req.do_by = request.user
|
||||||
req.save()
|
req.save()
|
||||||
Thread(target=dispatch, args=(req,)).start()
|
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):
|
def patch(self, request, r_id):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
|
@ -261,6 +225,43 @@ def post_request_1(request):
|
||||||
return json_response(error=error)
|
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):
|
def do_upload(request):
|
||||||
repos_dir = settings.REPOS_DIR
|
repos_dir = settings.REPOS_DIR
|
||||||
file = request.FILES['file']
|
file = request.FILES['file']
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { Modal, Steps } from 'antd';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
import Setup1 from './Ext2Setup1';
|
import Setup1 from './Ext2Setup1';
|
||||||
import Setup2 from './Ext2Setup2';
|
import Setup2 from './Ext2Setup2';
|
||||||
import Setup3 from './Ext2Setup3';
|
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
export default observer(function Ext2From() {
|
export default observer(function Ext2From() {
|
||||||
|
@ -30,12 +29,10 @@ export default observer(function Ext2From() {
|
||||||
footer={null}>
|
footer={null}>
|
||||||
<Steps current={store.page} className={styles.steps}>
|
<Steps current={store.page} className={styles.steps}>
|
||||||
<Steps.Step key={0} title="基本配置"/>
|
<Steps.Step key={0} title="基本配置"/>
|
||||||
<Steps.Step key={1} title="发布主机"/>
|
<Steps.Step key={1} title="执行动作"/>
|
||||||
<Steps.Step key={2} title="执行动作"/>
|
|
||||||
</Steps>
|
</Steps>
|
||||||
{store.page === 0 && <Setup1/>}
|
{store.page === 0 && <Setup1/>}
|
||||||
{store.page === 1 && <Setup2/>}
|
{store.page === 1 && <Setup2/>}
|
||||||
{store.page === 2 && <Setup3/>}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Link } from 'react-router-dom';
|
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 envStore from 'pages/config/environment/store';
|
||||||
|
import Selector from 'pages/host/Selector';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
export default observer(function Ext2Setup1() {
|
export default observer(function Ext2Setup1() {
|
||||||
const [envs, setEnvs] = useState([]);
|
const [envs, setEnvs] = useState([]);
|
||||||
|
const [selectorVisible, setSelectorVisible] = useState(false);
|
||||||
|
|
||||||
function updateEnvs() {
|
function updateEnvs() {
|
||||||
const ids = store.currentRecord['deploys'].map(x => x.env_id);
|
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>
|
<Link disabled={store.isReadOnly} to="/config/environment">新建环境</Link>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</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="发布模式">
|
<Form.Item label="发布模式">
|
||||||
<Radio.Group buttonStyle="solid" value={info.is_parallel} onChange={e => info.is_parallel = e.target.value}>
|
<Radio.Group buttonStyle="solid" value={info.is_parallel} onChange={e => info.is_parallel = e.target.value}>
|
||||||
<Radio.Button value={true}>并行</Radio.Button>
|
<Radio.Button value={true}>并行</Radio.Button>
|
||||||
|
@ -82,6 +88,11 @@ export default observer(function Ext2Setup1() {
|
||||||
disabled={!info.env_id}
|
disabled={!info.env_id}
|
||||||
onClick={() => store.page += 1}>下一步</Button>
|
onClick={() => store.page += 1}>下一步</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Selector
|
||||||
|
visible={selectorVisible}
|
||||||
|
selectedRowKeys={[...info.host_ids]}
|
||||||
|
onCancel={() => setSelectorVisible(false)}
|
||||||
|
onOk={(_, ids) => info.host_ids = ids}/>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,56 +6,186 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { Form, Select, Button } from 'antd';
|
import { Form, Input, Button, message, Divider, Alert, Select } from 'antd';
|
||||||
import { hasHostPermission } from 'libs';
|
import Editor from 'react-ace';
|
||||||
import store from './store';
|
import 'ace-builds/src-noconflict/mode-sh';
|
||||||
import hostStore from 'pages/host/store';
|
import 'ace-builds/src-noconflict/theme-tomorrow';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
import { http, cleanCommand } from 'libs';
|
||||||
|
import store from './store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Ext2Setup2 extends React.Component {
|
class Ext2Setup2 extends React.Component {
|
||||||
componentDidMount() {
|
constructor(props) {
|
||||||
if (hostStore.records.length === 0) {
|
super(props);
|
||||||
hostStore.fetchRecords()
|
this.helpMap = {
|
||||||
|
'0': null,
|
||||||
|
'1': '相对于输入的本地路径的文件路径,仅将匹配到文件传输至要发布的目标主机。',
|
||||||
|
'2': '支持模糊匹配,如果路径以 / 开头则基于输入的本地路径匹配,匹配到文件将不会被传输。'
|
||||||
|
}
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
handleSubmit = () => {
|
||||||
|
this.setState({loading: true});
|
||||||
const info = store.deploy;
|
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 (
|
return (
|
||||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} className={styles.ext2Form}>
|
||||||
<Form.Item required label="发布目标主机">
|
{store.deploy.id === undefined && (
|
||||||
{info['host_ids'].map((id, index) => (
|
<Alert
|
||||||
<React.Fragment key={index}>
|
closable
|
||||||
<Select
|
showIcon
|
||||||
value={id}
|
type="info"
|
||||||
showSearch
|
message="小提示"
|
||||||
disabled={store.isReadOnly}
|
style={{margin: '0 80px 20px'}}
|
||||||
placeholder="请选择"
|
description={[
|
||||||
optionFilterProp="children"
|
<p key={1}>Spug 将遵循先本地后目标主机的原则,按照顺序依次执行添加的动作,例如:本地动作1 -> 本地动作2 -> 目标主机动作1 -> 目标主机动作2 ...</p>,
|
||||||
style={{width: '80%', marginRight: 10, marginBottom: 12}}
|
<p key={2}>执行的命令内可以使用发布申请中设置的环境变量 SPUG_RELEASE,一般可用于标记一次发布的版本号或提交ID等,在执行的脚本内通过使用 $SPUG_RELEASE
|
||||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
获取其值来执行相应操作。</p>
|
||||||
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>
|
{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>
|
||||||
|
|
||||||
|
<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}}>
|
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||||
<Button disabled={store.isReadOnly} type="dashed" style={{width: '80%'}} onClick={store.addHost}>
|
<Button type="dashed" block onClick={() => server_actions.push({})}>
|
||||||
<PlusOutlined />添加目标主机
|
<PlusOutlined />添加本地执行动作(在服务端本地执行)
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</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}}>
|
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||||
<Button disabled={info['host_ids'].filter(x => x).length === 0} type="primary"
|
<Button disabled={store.isReadOnly} type="dashed" block onClick={() => host_actions.push({})}>
|
||||||
onClick={() => store.page += 1}>下一步</Button>
|
<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>
|
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</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
|
<Progress
|
||||||
key={item.id}
|
key={item.id}
|
||||||
percent={(item.step + 1) * 18}
|
percent={(item.step + 1) * 18}
|
||||||
status={item.status === 'error' ? 'exception' : 'active'}/>
|
status={item.step === 100 ? 'success' : item.status === 'error' ? 'exception' : 'active'}/>
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -72,8 +72,8 @@ export default observer(function () {
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required label="目标主机" help="可以通过创建多个发布申请单,选择主机分批发布。">
|
<Form.Item required label="目标主机" tooltip="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台`}
|
{host_ids.length > 0 && `已选择 ${host_ids.length} 台(可选${app_host_ids.length})`}
|
||||||
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="desc" label="备注信息">
|
<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 { UploadOutlined } from '@ant-design/icons';
|
||||||
import { Modal, Form, Input, Upload, message, Button } from 'antd';
|
import { Modal, Form, Input, Upload, message, Button } from 'antd';
|
||||||
import hostStore from 'pages/host/store';
|
import hostStore from 'pages/host/store';
|
||||||
|
import HostSelector from './HostSelector';
|
||||||
import { http, X_TOKEN } from 'libs';
|
import { http, X_TOKEN } from 'libs';
|
||||||
|
import styles from './index.module.less';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import lds from 'lodash';
|
import lds from 'lodash';
|
||||||
import HostSelector from "./HostSelector";
|
|
||||||
|
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
@ -19,17 +20,13 @@ export default observer(function () {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [fileList, setFileList] = useState([]);
|
const [fileList, setFileList] = useState([]);
|
||||||
const [host_ids, setHostIds] = useState(lds.clone(store.record.app_host_ids));
|
const [host_ids, setHostIds] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hostStore.records.length === 0) {
|
const {app_host_ids, host_ids, extra} = store.record;
|
||||||
hostStore.fetchRecords()
|
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||||
}
|
if (hostStore.records.length === 0) hostStore.fetchRecords();
|
||||||
const file = lds.get(store, 'record.extra.1');
|
if (store.record.extra) setFileList([{...extra, uid: '0'}])
|
||||||
if (file) {
|
|
||||||
file.uid = '0';
|
|
||||||
setFileList([file])
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
@ -39,13 +36,10 @@ export default observer(function () {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const formData = form.getFieldsValue();
|
const formData = form.getFieldsValue();
|
||||||
formData['id'] = store.record.id;
|
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;
|
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 => {
|
.then(res => {
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
store.ext2Visible = false;
|
store.ext2Visible = false;
|
||||||
|
@ -73,6 +67,7 @@ export default observer(function () {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {app_host_ids, deploy_id} = store.record;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
|
@ -82,31 +77,33 @@ export default observer(function () {
|
||||||
onCancel={() => store.ext2Visible = false}
|
onCancel={() => store.ext2Visible = false}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
onOk={handleSubmit}>
|
onOk={handleSubmit}>
|
||||||
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
<Form.Item required name="name" initialValue={store.record.name} label="申请标题">
|
<Form.Item required name="name" label="申请标题">
|
||||||
<Input placeholder="请输入申请标题"/>
|
<Input placeholder="请输入申请标题"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="extra"
|
name="version"
|
||||||
initialValue={lds.get(store.record, 'extra.0')}
|
label="SPUG_RELEASE"
|
||||||
label="环境变量(SPUG_RELEASE)"
|
tooltip="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $SPUG_RELEASE 来使用该值。">
|
||||||
help="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $SPUG_RELEASE 来使用该值。">
|
|
||||||
<Input placeholder="请输入环境变量 SPUG_RELEASE 的值"/>
|
<Input placeholder="请输入环境变量 SPUG_RELEASE 的值"/>
|
||||||
</Form.Item>
|
</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}
|
<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}
|
{fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null}
|
||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required label="目标主机" help="可以通过创建多个发布申请单,选择主机分批发布。">
|
<Form.Item required label="目标主机" tooltip="可以通过创建多个发布申请单,选择主机分批发布。">
|
||||||
{host_ids.length > 0 && `已选择 ${host_ids.length} 台`}
|
{host_ids.length > 0 && `已选择 ${host_ids.length} 台(可选${app_host_ids.length})`}
|
||||||
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
<Button type="link" onClick={() => setVisible(true)}>选择主机</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="desc" label="备注信息">
|
||||||
|
<Input placeholder="请输入备注信息"/>
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
{visible && <HostSelector
|
{visible && <HostSelector
|
||||||
host_ids={host_ids}
|
host_ids={host_ids}
|
||||||
app_host_ids={store.record.app_host_ids}
|
app_host_ids={app_host_ids}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
onOk={ids => setHostIds(ids)}/>}
|
onOk={ids => setHostIds(ids)}/>}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -40,7 +40,7 @@ function ComTable() {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<BuildOutlined/> {info.extra[0]}
|
<BuildOutlined/> {info.version}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -96,9 +96,7 @@ function ComTable() {
|
||||||
</Action>;
|
</Action>;
|
||||||
case '3':
|
case '3':
|
||||||
return <Action>
|
return <Action>
|
||||||
<Action.Link
|
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||||
auth="deploy.request.do"
|
|
||||||
to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</Action.Link>
|
|
||||||
<Action.Button
|
<Action.Button
|
||||||
auth="deploy.request.do"
|
auth="deploy.request.do"
|
||||||
disabled={info.type === '2'}
|
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) {
|
function handleDelete(info) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除确认',
|
title: '删除确认',
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Ext2Form from './Ext2Form';
|
||||||
import Approve from './Approve';
|
import Approve from './Approve';
|
||||||
import ComTable from './Table';
|
import ComTable from './Table';
|
||||||
import Ext1Console from './Ext1Console';
|
import Ext1Console from './Ext1Console';
|
||||||
|
import Ext2Console from './Ext2Console';
|
||||||
import { http, includes } from 'libs';
|
import { http, includes } from 'libs';
|
||||||
import envStore from 'pages/config/environment/store';
|
import envStore from 'pages/config/environment/store';
|
||||||
import appStore from 'pages/config/app/store';
|
import appStore from 'pages/config/app/store';
|
||||||
|
@ -115,7 +116,11 @@ function Index() {
|
||||||
<Row gutter={12} className={styles.miniConsole}>
|
<Row gutter={12} className={styles.miniConsole}>
|
||||||
{store.tabs.map(item => (
|
{store.tabs.map(item => (
|
||||||
<Col key={item.id}>
|
<Col key={item.id}>
|
||||||
|
{item.app_extend === '1' ? (
|
||||||
<Ext1Console request={item}/>
|
<Ext1Console request={item}/>
|
||||||
|
) : (
|
||||||
|
<Ext2Console request={item}/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, .3);
|
box-shadow: 0 0 4px rgba(0, 0, 0, .3);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
:global(.ant-progress-text) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -100,6 +104,10 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload :global(.ant-upload-list-item) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.floatBox {
|
.floatBox {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
Loading…
Reference in New Issue