From b81b1f66ac8770d4769665f1e09fc31b3ece3fea Mon Sep 17 00:00:00 2001 From: vapao Date: Wed, 17 Mar 2021 16:01:15 +0800 Subject: [PATCH] improve app deploy --- spug_api/apps/deploy/models.py | 3 + spug_api/apps/deploy/urls.py | 1 + spug_api/apps/deploy/utils.py | 106 ++++---------- spug_api/apps/deploy/views.py | 83 +++++++---- spug_api/apps/repository/views.py | 17 +++ spug_api/consumer/consumers.py | 3 +- spug_web/src/layout/Footer.js | 2 +- spug_web/src/pages/deploy/repository/Table.js | 34 ++++- spug_web/src/pages/deploy/repository/store.js | 2 +- spug_web/src/pages/deploy/request/Approve.js | 2 + .../src/pages/deploy/request/Ext1Console.js | 128 +++++++++++++++++ spug_web/src/pages/deploy/request/Ext1Form.js | 136 +++--------------- spug_web/src/pages/deploy/request/OutView.js | 21 +++ spug_web/src/pages/deploy/request/Table.js | 38 ++--- spug_web/src/pages/deploy/request/index.js | 13 +- .../pages/deploy/request/index.module.less | 101 +++++++++++++ spug_web/src/pages/deploy/request/store.js | 25 ++++ spug_web/src/pages/login/index.js | 2 +- 18 files changed, 469 insertions(+), 248 deletions(-) create mode 100644 spug_web/src/pages/deploy/request/Ext1Console.js create mode 100644 spug_web/src/pages/deploy/request/OutView.js create mode 100644 spug_web/src/pages/deploy/request/index.module.less diff --git a/spug_api/apps/deploy/models.py b/spug_api/apps/deploy/models.py index 2f639a7..6df7992 100644 --- a/spug_api/apps/deploy/models.py +++ b/spug_api/apps/deploy/models.py @@ -5,6 +5,7 @@ from django.db import models from libs import ModelMixin, human_datetime from apps.account.models import User from apps.app.models import Deploy +from apps.repository.models import Repository class DeployRequest(models.Model, ModelMixin): @@ -21,6 +22,7 @@ class DeployRequest(models.Model, ModelMixin): ('2', '回滚') ) deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE) + repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL) name = models.CharField(max_length=50) type = models.CharField(max_length=2, choices=TYPES, default='1') extra = models.TextField() @@ -29,6 +31,7 @@ class DeployRequest(models.Model, ModelMixin): status = models.CharField(max_length=2, choices=STATUS) reason = models.CharField(max_length=255, null=True) version = models.CharField(max_length=50, null=True) + spug_version = models.CharField(max_length=50, null=True) created_at = models.CharField(max_length=20, default=human_datetime) created_by = models.ForeignKey(User, models.PROTECT, related_name='+') diff --git a/spug_api/apps/deploy/urls.py b/spug_api/apps/deploy/urls.py index c7e95eb..abf4245 100644 --- a/spug_api/apps/deploy/urls.py +++ b/spug_api/apps/deploy/urls.py @@ -7,6 +7,7 @@ from .views import * urlpatterns = [ path('request/', RequestView.as_view()), + path('request/1/', post_request_1), path('request/upload/', do_upload), path('request//', RequestDetailView.as_view()), ] diff --git a/spug_api/apps/deploy/utils.py b/spug_api/apps/deploy/utils.py index 163cf30..954e917 100644 --- a/spug_api/apps/deploy/utils.py +++ b/spug_api/apps/deploy/utils.py @@ -22,13 +22,14 @@ class SpugError(Exception): pass -def deploy_dispatch(request, req, token): +def dispatch(req): rds = get_redis_connection() + rds_key = f'{settings.REQUEST_KEY}:{req.id}' try: api_token = uuid.uuid4().hex rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}') - helper = Helper(rds, token, req.id) - helper.send_step('local', 1, f'完成\r\n{human_time()} 发布准备... ') + 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), @@ -43,7 +44,6 @@ def deploy_dispatch(request, req, token): SPUG_REPOS_DIR=REPOS_DIR, ) if req.deploy.extend == '1': - env.update(json.loads(req.deploy.extend_obj.custom_envs)) _ext1_deploy(req, helper, env) else: _ext2_deploy(req, helper, env) @@ -52,7 +52,7 @@ def deploy_dispatch(request, req, token): req.status = '-3' raise e finally: - rds.expire(token, 5 * 60) + rds.expire(rds_key, 14 * 24 * 60 * 60) rds.close() req.save() Helper.send_deploy_notify(req) @@ -60,55 +60,12 @@ def deploy_dispatch(request, req, token): def _ext1_deploy(req, helper, env): extend = req.deploy.extend_obj - extras = json.loads(req.extra) env.update(SPUG_DST_DIR=extend.dst_dir) - if extras[0] == 'branch': - tree_ish = extras[2] - env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2]) - else: - tree_ish = extras[1] - env.update(SPUG_GIT_TAG=extras[1]) - if req.type == '2': - helper.send_step('local', 6, f'完成\r\n{human_time()} 回滚发布... 跳过') - else: - helper.local(f'cd {REPOS_DIR} && rm -rf {req.deploy_id}_*') - helper.send_step('local', 1, '完成\r\n') - - if extend.hook_pre_server: - helper.send_step('local', 2, f'{human_time()} 检出前任务...\r\n') - helper.local(f'cd /tmp && {extend.hook_pre_server}', env) - - helper.send_step('local', 3, f'{human_time()} 执行检出... ') - git_dir = os.path.join(REPOS_DIR, str(req.deploy.id)) - command = f'cd {git_dir} && git archive --prefix={env.SPUG_VERSION}/ {tree_ish} | (cd .. && tar xf -)' - helper.local(command) - helper.send_step('local', 3, '完成\r\n') - - if extend.hook_post_server: - helper.send_step('local', 4, f'{human_time()} 检出后任务...\r\n') - helper.local(f'cd {os.path.join(REPOS_DIR, env.SPUG_VERSION)} && {extend.hook_post_server}', env) - - helper.send_step('local', 5, f'\r\n{human_time()} 执行打包... ') - filter_rule, exclude, contain = json.loads(extend.filter_rule), '', env.SPUG_VERSION - files = helper.parse_filter_rule(filter_rule['data']) - if files: - if filter_rule['type'] == 'exclude': - excludes = [] - for x in files: - if x.startswith('/'): - excludes.append(f'--exclude={env.SPUG_VERSION}{x}') - else: - excludes.append(f'--exclude={x}') - exclude = ' '.join(excludes) - else: - contain = ' '.join(f'{env.SPUG_VERSION}/{x}' for x in files) - helper.local(f'cd {REPOS_DIR} && tar zcf {env.SPUG_VERSION}.tar.gz {exclude} {contain}') - helper.send_step('local', 6, f'完成') threads, latest_exception = [], None with futures.ThreadPoolExecutor(max_workers=min(10, os.cpu_count() + 5)) as executor: for h_id in json.loads(req.host_ids): env = AttrDict(env.items()) - t = executor.submit(_deploy_ext1_host, helper, h_id, extend, env) + t = executor.submit(_deploy_ext1_host, req, helper, h_id, env) t.h_id = h_id threads.append(t) for t in futures.as_completed(threads): @@ -188,34 +145,34 @@ def _ext2_deploy(req, helper, env): helper.send_step('local', 100, f'\r\n{human_time()} ** 发布成功 **') -def _deploy_ext1_host(helper, h_id, extend, env): - helper.send_step(h_id, 1, f'{human_time()} 数据准备... ') +def _deploy_ext1_host(req, helper, h_id, env): + extend = req.deploy.extend_obj + helper.send_step(h_id, 1, f'就绪\r\n{human_time()} 数据准备... ') 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() - if env.SPUG_DEPLOY_TYPE != '2': - code, _ = ssh.exec_command( - f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]') - if code == 0: - helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。') - # clean - clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf' - helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {env.SPUG_VERSION} && {clean_command}') - # transfer files - tar_gz_file = f'{env.SPUG_VERSION}.tar.gz' - try: - ssh.put_file(os.path.join(REPOS_DIR, tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file)) - except Exception as e: - helper.send_error(host.id, f'exception: {e}') + code, _ = ssh.exec_command( + f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]') + if code == 0: + helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。') + # clean + clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf' + helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}') + # transfer files + tar_gz_file = f'{req.spug_version}.tar.gz' + try: + ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file)) + except Exception as e: + helper.send_error(host.id, f'exception: {e}') - command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {env.SPUG_APP_ID}_*.tar.gz' - helper.remote(host.id, ssh, command) + command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz' + helper.remote(host.id, ssh, command) helper.send_step(h_id, 1, '完成\r\n') # pre host - repo_dir = os.path.join(extend.dst_repo, env.SPUG_VERSION) + repo_dir = os.path.join(extend.dst_repo, req.spug_version) if extend.hook_pre_host: helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n') command = f'cd {repo_dir} ; {extend.hook_pre_host}' @@ -271,11 +228,10 @@ def _deploy_ext2_host(helper, h_id, actions, env): class Helper: - def __init__(self, rds, token, r_id): + def __init__(self, rds, key): self.rds = rds - self.token = token - self.log_key = f'{settings.REQUEST_KEY}:{r_id}' - self.rds.delete(self.log_key) + self.key = key + self.rds.delete(self.key) @classmethod def _make_dd_notify(cls, action, req, version, host_str): @@ -425,11 +381,11 @@ class Helper: return files def _send(self, message): - self.rds.lpush(self.token, json.dumps(message)) - self.rds.lpush(self.log_key, json.dumps(message)) + self.rds.rpush(self.key, json.dumps(message)) def send_info(self, key, message): - self._send({'key': key, 'status': 'info', 'data': message}) + if message: + self._send({'key': key, 'data': message}) def send_error(self, key, message, with_break=True): message = '\r\n' + message diff --git a/spug_api/apps/deploy/views.py b/spug_api/apps/deploy/views.py index 10941da..7c53d6b 100644 --- a/spug_api/apps/deploy/views.py +++ b/spug_api/apps/deploy/views.py @@ -9,14 +9,14 @@ from django_redis import get_redis_connection from libs import json_response, JsonParser, Argument, human_datetime, human_time from apps.deploy.models import DeployRequest from apps.app.models import Deploy, DeployExtend2 -from apps.deploy.utils import deploy_dispatch, Helper +from apps.repository.models import Repository +from apps.deploy.utils import dispatch, Helper from apps.host.models import Host from collections import defaultdict from threading import Thread from datetime import datetime import subprocess import json -import uuid import os @@ -34,6 +34,7 @@ class RequestView(View): app_name=F('deploy__app__name'), app_host_ids=F('deploy__host_ids'), app_extend=F('deploy__extend'), + rep_extra=F('repository__extra'), created_by_user=F('created_by__nickname')): tmp = item.to_dict() tmp['env_id'] = item.env_id @@ -41,8 +42,8 @@ class RequestView(View): tmp['app_id'] = item.app_id tmp['app_name'] = item.app_name tmp['app_extend'] = item.app_extend - tmp['extra'] = json.loads(item.extra) tmp['host_ids'] = json.loads(item.host_ids) + 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() tmp['created_by_user'] = item.created_by_user @@ -62,10 +63,6 @@ class RequestView(View): deploy = Deploy.objects.filter(pk=form.deploy_id).first() if not deploy: return json_response(error='未找到该发布配置') - if form.extra[0] == 'tag' and not form.extra[1]: - return json_response(error='请选择要发布的Tag') - if form.extra[0] == 'branch' and not form.extra[2]: - return json_response(error='请选择要发布的分支及Commit ID') if deploy.extend == '2': if form.extra[0]: form.extra[0] = form.extra[0].replace("'", '') @@ -165,28 +162,29 @@ class RequestDetailView(View): if not req: return json_response(error='未找到指定发布申请') hosts = Host.objects.filter(id__in=json.loads(req.host_ids)) - targets = [{'id': x.id, 'title': f'{x.name}({x.hostname}:{x.port})'} for x in hosts] - server_actions, host_actions, outputs = [], [], [] + server_actions, host_actions = [], [] + outputs = {x.id: {'id': x.id, 'title': x.name, 'data': []} for x in hosts} 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) - if request.GET.get('log'): - 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) + while data: + counter += 10 + for item in data: + item = json.loads(item.decode()) + if 'data' in item: + outputs[item['key']]['data'].append(item['data']) + if 'step' in item: + outputs[item['key']]['step'] = item['step'] + if 'status' in item: + outputs[item['key']]['status'] = item['status'] data = rds.lrange(key, counter, counter + 9) - while data: - counter += 10 - outputs.extend(x.decode() for x in data) - data = rds.lrange(key, counter, counter + 9) return json_response({ - 'app_name': req.deploy.app.name, - 'env_name': req.deploy.env.name, - 'status': req.status, - 'type': req.type, - 'status_alias': req.get_status_display(), - 'targets': targets, 'server_actions': server_actions, 'host_actions': host_actions, - 'outputs': outputs + 'outputs': outputs, + 'status': req.status }) def post(self, request, r_id): @@ -201,17 +199,14 @@ class RequestDetailView(View): if req.status not in ('1', '-3'): return json_response(error='该申请单当前状态还不能执行发布') hosts = Host.objects.filter(id__in=json.loads(req.host_ids)) - token = uuid.uuid4().hex - outputs = {str(x.id): {'data': []} for x in hosts} - outputs.update(local={'data': [f'{human_time()} 建立接连... ']}) + message = f'{human_time()} 等待调度... ' + outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts} req.status = '2' req.do_at = human_datetime() req.do_by = request.user - if not req.version: - req.version = f'{req.deploy_id}_{req.id}_{datetime.now().strftime("%Y%m%d%H%M%S")}' req.save() - Thread(target=deploy_dispatch, args=(request, req, token)).start() - return json_response({'token': token, 'type': req.type, 'outputs': outputs}) + Thread(target=dispatch, args=(req,)).start() + return json_response({'type': req.type, 'outputs': outputs}) def patch(self, request, r_id): form, error = JsonParser( @@ -235,6 +230,36 @@ class RequestDetailView(View): return json_response(error=error) +def post_request_1(request): + form, error = JsonParser( + Argument('id', type=int, required=False), + Argument('name', help='请输申请标题'), + Argument('repository_id', type=int, help='请选择发布版本'), + Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'), + Argument('desc', required=False), + ).parse(request.body) + if error is None: + repository = Repository.objects.filter(pk=form.repository_id).first() + if not repository: + return json_response(error='未找到指定构建版本记录') + form.name = form.name.replace("'", '') + form.status = '0' if repository.deploy.is_audit else '1' + form.version = repository.version + form.spug_version = repository.spug_version + form.deploy_id = repository.deploy_id + form.host_ids = json.dumps(form.host_ids) + if form.id: + req = DeployRequest.objects.get(pk=form.id) + is_required_notify = repository.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 = repository.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'] diff --git a/spug_api/apps/repository/views.py b/spug_api/apps/repository/views.py index acf5231..ec77176 100644 --- a/spug_api/apps/repository/views.py +++ b/spug_api/apps/repository/views.py @@ -13,10 +13,13 @@ import json class RepositoryView(View): def get(self, request): + deploy_id = request.GET.get('deploy_id') data = Repository.objects.annotate( app_name=F('app__name'), env_name=F('env__name'), created_by_user=F('created_by__nickname')) + if deploy_id: + data = data.filter(deploy_id=deploy_id, status='5') return json_response([x.to_view() for x in data]) def post(self, request): @@ -41,6 +44,20 @@ class RepositoryView(View): return json_response(rep.to_view()) return json_response(error=error) + def patch(self, request): + form, error = JsonParser( + Argument('id', type=int, help='参数错误'), + Argument('action', help='参数错误') + ).parse(request.body) + if error is None: + rep = Repository.objects.filter(pk=form.id).first() + if not rep: + return json_response(error='未找到指定构建记录') + if form.action == 'rebuild': + Thread(target=dispatch, args=(rep,)).start() + return json_response(rep.to_view()) + return json_response(error=error) + def delete(self, request): form, error = JsonParser( Argument('id', type=int, help='请指定操作对象') diff --git a/spug_api/consumer/consumers.py b/spug_api/consumer/consumers.py index 470618c..5ff518c 100644 --- a/spug_api/consumer/consumers.py +++ b/spug_api/consumer/consumers.py @@ -43,6 +43,8 @@ class ComConsumer(WebsocketConsumer): module = self.scope['url_route']['kwargs']['module'] if module == 'build': self.key = f'{settings.BUILD_KEY}:{token}' + elif module == 'request': + self.key = f'{settings.REQUEST_KEY}:{token}' else: raise TypeError(f'unknown module for {module}') self.rds = get_redis_connection() @@ -69,7 +71,6 @@ class ComConsumer(WebsocketConsumer): while response: index += 1 self.send(text_data=response) - time.sleep(1) response = self.get_response(index) self.send(text_data='pong') diff --git a/spug_web/src/layout/Footer.js b/spug_web/src/layout/Footer.js index a965536..4b880cc 100644 --- a/spug_web/src/layout/Footer.js +++ b/spug_web/src/layout/Footer.js @@ -22,7 +22,7 @@ export default function () { rel="noopener noreferrer">文档
- Copyright 2020 By OpenSpug + Copyright 2021 By OpenSpug
diff --git a/spug_web/src/pages/deploy/repository/Table.js b/spug_web/src/pages/deploy/repository/Table.js index 341c08c..ea3c40f 100644 --- a/spug_web/src/pages/deploy/repository/Table.js +++ b/spug_web/src/pages/deploy/repository/Table.js @@ -3,7 +3,7 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { Table, Modal, Tag, message } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; @@ -12,6 +12,8 @@ import { http, hasPermission } from 'libs'; import store from './store'; function ComTable() { + const [loading, setLoading] = useState(); + function handleDelete(info) { Modal.confirm({ title: '删除确认', @@ -26,6 +28,27 @@ function ComTable() { }) } + function handleRebuild(info) { + if (info.status === '5') { + Modal.confirm({ + title: '重新构建提示', + content: `当前选择版本 ${info.version} 已完成构建,再次构建将覆盖已有的数据,要再次重新构建吗?`, + onOk: () => _rebuild(info) + }) + } else if (info.status === '1') { + return message.error('已在构建中,请点击日志查看详情') + } else { + _rebuild(info) + } + } + + function _rebuild(info) { + setLoading(info.id); + http.patch('/api/repository/', {id: info.id, action: 'rebuild'}) + .then(() => store.showConsole(info)) + .finally(() => setLoading(null)) + } + const statusColorMap = {'0': 'cyan', '1': 'blue', '2': 'red', '5': 'green'}; return ( - {info.status_alias}}/> + {info.status_alias}}/> {hasPermission('config.env.edit|config.env.del') && ( - ( + ( store.showDetail(info)}>详情 + handleRebuild(info)}>构建 store.showConsole(info)}>日志 )}/> diff --git a/spug_web/src/pages/deploy/repository/store.js b/spug_web/src/pages/deploy/repository/store.js index 2e77ce3..1ae8bfa 100644 --- a/spug_web/src/pages/deploy/repository/store.js +++ b/spug_web/src/pages/deploy/repository/store.js @@ -9,7 +9,7 @@ import http from 'libs/http'; class Store { @observable records = []; @observable record = {}; - @observable idMap = {}; + @observable deploy = {}; @observable outputs = []; @observable isFetching = false; @observable formVisible = false; diff --git a/spug_web/src/pages/deploy/request/Approve.js b/spug_web/src/pages/deploy/request/Approve.js index 57d773e..11b422c 100644 --- a/spug_web/src/pages/deploy/request/Approve.js +++ b/spug_web/src/pages/deploy/request/Approve.js @@ -8,6 +8,7 @@ import { observer } from 'mobx-react'; import { Modal, Form, Input, Switch, message } from 'antd'; import http from 'libs/http'; import store from './store'; +import styles from './index.module.less'; export default observer(function () { const [form] = Form.useForm(); @@ -38,6 +39,7 @@ export default observer(function () { title="审核发布申请" onCancel={() => store.approveVisible = false} confirmLoading={loading} + className={styles.approve} onOk={handleSubmit}>
diff --git a/spug_web/src/pages/deploy/request/Ext1Console.js b/spug_web/src/pages/deploy/request/Ext1Console.js new file mode 100644 index 0000000..3f8ca1b --- /dev/null +++ b/spug_web/src/pages/deploy/request/Ext1Console.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, { useEffect } from 'react'; +import { observer, useLocalStore } from 'mobx-react'; +import { Card, Progress, Modal, Collapse, Steps } from 'antd'; +import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, } 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 Ext1Console(props) { + const outputs = useLocalStore(() => ({})); + + useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, []) + + function readDeploy() { + let socket; + http.get(`/api/deploy/request/${props.request.id}/`) + .then(res => { + 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 => { + 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') { + icon = + } + return + } + + function switchMiniMode() { + const value = store.tabModes[props.request.id]; + store.tabModes[props.request.id] = !value + } + + return store.tabModes[props.request.id] ? ( + +
+
{props.request.name}
+
+ {Object.values(outputs).map(item => ( + + ))} +
+ ) : ( + store.showConsole(props.request, true)} + title={[ + {props.request.name}, +
+ +
+ ]}> + }> + {Object.values(outputs).map((item, index) => ( + + {item.title} + + + + + + + + }> + + + ))} + + +
+ ) +} + +export default observer(Ext1Console) \ No newline at end of file diff --git a/spug_web/src/pages/deploy/request/Ext1Form.js b/spug_web/src/pages/deploy/request/Ext1Form.js index 8f9c8d6..9e9d638 100644 --- a/spug_web/src/pages/deploy/request/Ext1Form.js +++ b/spug_web/src/pages/deploy/request/Ext1Form.js @@ -5,8 +5,7 @@ */ import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; -import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'; -import { Modal, Form, Input, Select, Button, Tag, message } from 'antd'; +import { Modal, Form, Input, Select, Tag, message } from 'antd'; import hostStore from 'pages/host/store'; import http from 'libs/http'; import store from './store'; @@ -15,84 +14,29 @@ import lds from 'lodash'; export default observer(function () { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); - const [fetching, setFetching] = useState(true); - const [git_type, setGitType] = useState(lds.get(store.record, 'extra.0', 'branch')); - const [extra1, setExtra1] = useState(lds.get(store.record, 'extra.1')); - const [extra2, setExtra2] = useState(lds.get(store.record, 'extra.2')); - const [versions, setVersions] = useState({}); - const [host_ids, setHostIds] = useState(lds.clone(store.record.app_host_ids)); + const [versions, setVersions] = useState([]); + const [host_ids, setHostIds] = useState([]); useEffect(() => { - fetchVersions(); + const {deploy_id, app_host_ids, host_ids} = store.record; + setHostIds(lds.clone(host_ids || app_host_ids)); + http.get('/api/repository/', {params: {deploy_id}}) + .then(res => setVersions(res)) if (hostStore.records.length === 0) { hostStore.fetchRecords() } }, []) - useEffect(() => { - if (extra1 === undefined) { - const {branches, tags} = versions; - let [extra1, extra2] = [undefined, undefined]; - if (git_type === 'branch') { - if (branches) { - extra1 = _getDefaultBranch(branches); - extra2 = lds.get(branches[extra1], '0.id') - } - } else { - if (tags) { - extra1 = lds.get(Object.keys(tags), 0) - } - } - setExtra1(extra1) - setExtra2(extra2) - } - }, [versions, git_type, extra1]) - - function fetchVersions() { - setFetching(true); - http.get(`/api/app/deploy/${store.record.deploy_id}/versions/`, {timeout: 120000}) - .then(res => setVersions(res)) - .finally(() => setFetching(false)) - } - - function _getDefaultBranch(branches) { - branches = Object.keys(branches); - let branch = branches[0]; - for (let item of store.records) { - if (item['deploy_id'] === store.record['deploy_id']) { - const b = lds.get(item, 'extra.1'); - if (branches.includes(b)) { - branch = b - } - break - } - } - return branch - } - - function switchType(v) { - setExtra1(undefined); - setGitType(v) - } - - function switchExtra1(v) { - setExtra1(v) - if (git_type === 'branch') { - setExtra2(lds.get(versions.branches[v], '0.id')) - } - } - function handleSubmit() { if (host_ids.length === 0) { - return message.error('请至少选择一个要发布的目标主机') + return message.error('请至少选择一个要发布的主机') } setLoading(true); const formData = form.getFieldsValue(); formData['id'] = store.record.id; - formData['deploy_id'] = store.record.deploy_id; formData['host_ids'] = host_ids; - formData['extra'] = [git_type, extra1, extra2]; - http.post('/api/deploy/request/', formData) + formData['deploy_id'] = store.record.deploy_id; + http.post('/api/deploy/request/1/', formData) .then(res => { message.success('操作成功'); store.ext1Visible = false; @@ -111,11 +55,10 @@ export default observer(function () { } } - const {branches, tags} = versions; return ( store.ext1Visible = false} @@ -125,55 +68,22 @@ export default observer(function () { - - 根据网络情况,首次刷新可能会很慢,请耐心等待。 - clone 失败? - }> - - - - - - - - {fetching ? : - - } - + + - {git_type === 'branch' && ( - - - - )} - - {store.record['app_host_ids'].map(id => ( + + {store.record.app_host_ids.map(id => ( handleChange(id)}> {lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)}) diff --git a/spug_web/src/pages/deploy/request/OutView.js b/spug_web/src/pages/deploy/request/OutView.js new file mode 100644 index 0000000..8213f80 --- /dev/null +++ b/spug_web/src/pages/deploy/request/OutView.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, { useRef, useEffect } from 'react'; +import styles from './index.module.less'; + +function OutView(props) { + const el = useRef() + + useEffect(() => { + if (el) el.current.scrollTop = el.current.scrollHeight + }) + + return ( +
{props.records}
+ ) +} + +export default OutView \ No newline at end of file diff --git a/spug_web/src/pages/deploy/request/Table.js b/spug_web/src/pages/deploy/request/Table.js index efa684e..380de7b 100644 --- a/spug_web/src/pages/deploy/request/Table.js +++ b/spug_web/src/pages/deploy/request/Table.js @@ -6,7 +6,7 @@ import React from 'react'; import { observer } from 'mobx-react'; import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined } from '@ant-design/icons'; -import { Radio, Modal, Popover, Tag, message } from 'antd'; +import { Radio, Modal, Popover, Tag, Popconfirm, message } from 'antd'; import { http, hasPermission } from 'libs'; import { Action, AuthButton, TableCard } from 'components'; import store from './store'; @@ -42,20 +42,12 @@ class ComTable extends React.Component { title: '版本', render: info => { if (info['app_extend'] === '1') { - const [type, ext1, ext2] = info.extra; - if (type === 'branch') { - return ( - - {ext1}#{ext2.substr(0, 6)} - - ) - } else { - return ( - - {ext1} - - ) - } + const [ext1] = info.rep_extra; + return ( + + {ext1 === 'branch' ? : } {info.version} + + ) } else { return ( @@ -88,15 +80,15 @@ class ComTable extends React.Component { }, { title: '申请人', dataIndex: 'created_by_user', + hide: true }, { title: '申请时间', dataIndex: 'created_at', - sorter: (a, b) => a['created_at'].localeCompare(b['created_at']) + sorter: (a, b) => a['created_at'].localeCompare(b['created_at']), + hide: true }, { title: '备注', dataIndex: 'desc', - ellipsis: true, - hide: true }, { title: '操作', className: hasPermission('deploy.request.do|deploy.request.edit|deploy.request.approve|deploy.request.del') ? null : 'none', @@ -104,10 +96,10 @@ class ComTable extends React.Component { switch (info.status) { case '-3': return - 查看 - 发布 + store.readConsole(info)}>查看 + store.showConsole(info)}> + 发布 + ; case '1': return - 发布 + store.showConsole(info)}>发布 this.handleDelete(info)}>删除 ; case '2': diff --git a/spug_web/src/pages/deploy/request/index.js b/spug_web/src/pages/deploy/request/index.js index dedb87c..bec183e 100644 --- a/spug_web/src/pages/deploy/request/index.js +++ b/spug_web/src/pages/deploy/request/index.js @@ -6,17 +6,19 @@ import React from 'react'; import { observer } from 'mobx-react'; import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons'; -import { Form, Select, DatePicker, Modal, Input, message } from 'antd'; +import { Form, Select, DatePicker, Modal, Input, Row, Col, message } from 'antd'; import { SearchForm, AuthDiv, AuthButton, Breadcrumb, AppSelector } from 'components'; import Ext1Form from './Ext1Form'; import Ext2Form from './Ext2Form'; import Approve from './Approve'; import ComTable from './Table'; +import Ext1Console from './Ext1Console'; import { http, includes } from 'libs'; import envStore from 'pages/config/environment/store'; import appStore from 'pages/config/app/store'; import store from './store'; import moment from 'moment'; +import styles from './index.module.less'; @observer class Index extends React.Component { @@ -120,6 +122,15 @@ class Index extends React.Component { {store.ext1Visible && } {store.ext2Visible && } {store.approveVisible && } + {store.tabs.length > 0 && ( + + {store.tabs.map(item => ( + + + + ))} + + )} ) } diff --git a/spug_web/src/pages/deploy/request/index.module.less b/spug_web/src/pages/deploy/request/index.module.less new file mode 100644 index 0000000..d5d266e --- /dev/null +++ b/spug_web/src/pages/deploy/request/index.module.less @@ -0,0 +1,101 @@ +.approve { + :global(.ant-switch) { + background: #faad14; + } + + :global(.ant-switch-checked) { + background: #389e0d; + } +} + +.miniConsole { + position: fixed; + bottom: 12px; + right: 24px; + align-items: flex-end; + + .item { + width: 180px; + box-shadow: 0 0 4px rgba(0, 0, 0, .3); + border-radius: 5px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + margin-bottom: 4px; + + .title { + width: 120px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .icon { + font-size: 16px; + color: rgba(0, 0, 0, .45); + } + + .icon:hover { + color: #000; + } + } + } +} + +.console { + .miniIcon { + position: absolute; + top: 0; + right: 0; + display: block; + width: 56px; + height: 56px; + line-height: 56px; + text-align: center; + cursor: pointer; + color: rgba(0, 0, 0, .45); + margin-right: 56px; + + :hover { + color: #000; + } + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + + .title { + width: 200px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 600; + } + + .step { + width: 600px; + margin-right: 16px; + } + + .icon { + font-size: 22px; + font-weight: 300; + color: #1890ff; + } + } + + .out { + min-height: 40px; + max-height: 400px; + padding: 10px 15px; + } +} + +.collapse :global(.ant-collapse-content-box) { + padding: 0; +} \ No newline at end of file diff --git a/spug_web/src/pages/deploy/request/store.js b/spug_web/src/pages/deploy/request/store.js index 2df3303..760dff8 100644 --- a/spug_web/src/pages/deploy/request/store.js +++ b/spug_web/src/pages/deploy/request/store.js @@ -5,11 +5,14 @@ */ import { observable } from "mobx"; import http from 'libs/http'; +import lds from 'lodash'; class Store { @observable records = []; @observable record = {}; @observable counter = {}; + @observable tabs = []; + @observable tabModes = {}; @observable isFetching = false; @observable addVisible = false; @observable ext1Visible = false; @@ -82,6 +85,28 @@ class Store { showApprove = (info) => { this.record = info; this.approveVisible = true; + }; + + showConsole = (info, isClose) => { + const index = lds.findIndex(this.tabs, x => x.id === info.id); + if (isClose) { + if (index !== -1) { + this.tabs.splice(index, 1) + delete this.tabModes[info.id] + } + } else if (index === -1) { + this.tabModes[info.id] = true + this.tabs.push(info) + } + }; + + readConsole = (info) => { + this.tabModes[info.id] = false + const index = lds.findIndex(this.tabs, x => x.id === info.id); + if (index === -1) { + info = Object.assign({}, info, {mode: 'read'}) + this.tabs.push(info) + } } } diff --git a/spug_web/src/pages/login/index.js b/spug_web/src/pages/login/index.js index 0f50012..9d10af5 100644 --- a/spug_web/src/pages/login/index.js +++ b/spug_web/src/pages/login/index.js @@ -145,7 +145,7 @@ export default function () { 文档 -
Copyright 2020 By OpenSpug
+
Copyright 2021 By OpenSpug
)