mirror of https://github.com/openspug/spug
improve app deploy
parent
402250f7b1
commit
b81b1f66ac
|
@ -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='+')
|
||||
|
|
|
@ -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/<int:r_id>/', RequestDetailView.as_view()),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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='请指定操作对象')
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ export default function () {
|
|||
rel="noopener noreferrer">文档</a>
|
||||
</div>
|
||||
<div style={{color: 'rgba(0, 0, 0, .45)'}}>
|
||||
Copyright <CopyrightOutlined/> 2020 By OpenSpug
|
||||
Copyright <CopyrightOutlined/> 2021 By OpenSpug
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Footer>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* 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 (
|
||||
<TableCard
|
||||
|
@ -54,11 +77,16 @@ function ComTable() {
|
|||
<Table.Column ellipsis title="备注" dataIndex="remarks"/>
|
||||
<Table.Column hide title="构建时间" dataIndex="created_at"/>
|
||||
<Table.Column hide title="构建人" dataIndex="created_by_user"/>
|
||||
<Table.Column width={100} title="状态" render={info => <Tag color={statusColorMap[info.status]}>{info.status_alias}</Tag>}/>
|
||||
<Table.Column width={100} title="状态"
|
||||
render={info => <Tag color={statusColorMap[info.status]}>{info.status_alias}</Tag>}/>
|
||||
{hasPermission('config.env.edit|config.env.del') && (
|
||||
<Table.Column width={150} title="操作" render={info => (
|
||||
<Table.Column width={180} title="操作" render={info => (
|
||||
<Action>
|
||||
<Action.Button auth="config.env.edit" onClick={() => store.showDetail(info)}>详情</Action.Button>
|
||||
<Action.Button
|
||||
auth="config.env.del"
|
||||
loading={loading === info.id}
|
||||
onClick={() => handleRebuild(info)}>构建</Action.Button>
|
||||
<Action.Button auth="config.env.del" onClick={() => store.showConsole(info)}>日志</Action.Button>
|
||||
</Action>
|
||||
)}/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}>
|
||||
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}} onValuesChange={handleChange}>
|
||||
<Form.Item required name="is_pass" initialValue={true} valuePropName="checked" label="审批结果">
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* 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 } 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 = <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>
|
||||
</div>
|
||||
{Object.values(outputs).map(item => (
|
||||
<Progress
|
||||
key={item.id}
|
||||
percent={(item.step + 1) * 18}
|
||||
status={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}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{Object.values(outputs).map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
<StepItem title="数据准备" item={item} step={1}/>
|
||||
<StepItem title="发布前任务" item={item} step={2}/>
|
||||
<StepItem title="执行发布" item={item} step={3}/>
|
||||
<StepItem title="发布后任务" item={item} step={4}/>
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView records={item.data}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
|
||||
</Collapse>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Ext1Console)
|
|
@ -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 (
|
||||
<Modal
|
||||
visible
|
||||
width={800}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
title="新建发布申请"
|
||||
onCancel={() => store.ext1Visible = false}
|
||||
|
@ -125,55 +68,22 @@ export default observer(function () {
|
|||
<Form.Item required name="name" label="申请标题">
|
||||
<Input placeholder="请输入申请标题"/>
|
||||
</Form.Item>
|
||||
<Form.Item required label="选择分支/标签/版本" style={{marginBottom: 12}} extra={<span>
|
||||
根据网络情况,首次刷新可能会很慢,请耐心等待。
|
||||
<a target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.dev/docs/install-error/#%E6%96%B0%E5%BB%BA%E5%B8%B8%E8%A7%84%E5%8F%91%E5%B8%83%E7%94%B3%E8%AF%B7-git-clone-%E9%94%99%E8%AF%AF">clone 失败?</a>
|
||||
</span>}>
|
||||
<Form.Item style={{display: 'inline-block', marginBottom: 0, width: '450px'}}>
|
||||
<Input.Group compact>
|
||||
<Select value={git_type} onChange={switchType} style={{width: 100}}>
|
||||
<Select.Option value="branch">Branch</Select.Option>
|
||||
<Select.Option value="tag">Tag</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 350}}
|
||||
value={extra1}
|
||||
placeholder="请稍等"
|
||||
onChange={switchExtra1}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}>
|
||||
{git_type === 'branch' ? (
|
||||
Object.keys(branches || {}).map(b => <Select.Option key={b} value={b}>{b}</Select.Option>)
|
||||
) : (
|
||||
Object.entries(tags || {}).map(([tag, info]) => (
|
||||
<Select.Option key={tag} value={tag}>{`${tag} ${info.author} ${info.message}`}</Select.Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item style={{display: 'inline-block', width: 82, textAlign: 'center', marginBottom: 0}}>
|
||||
{fetching ? <LoadingOutlined style={{fontSize: 18, color: '#1890ff'}}/> :
|
||||
<Button type="link" icon={<SyncOutlined/>} disabled={fetching} onClick={fetchVersions}>刷新</Button>
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item required name="repository_id" label="发布版本">
|
||||
<Select placeholder="请选择">
|
||||
{versions.map(item => (
|
||||
<Select.Option
|
||||
key={item.id}
|
||||
value={item.id}>
|
||||
{item.remarks ? `${item.version} (${item.remarks})` : item.version}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{git_type === 'branch' && (
|
||||
<Form.Item required label="选择Commit ID">
|
||||
<Select value={extra2} placeholder="请选择" onChange={v => setExtra2(v)}>
|
||||
{extra1 && branches ? branches[extra1].map(item => (
|
||||
<Select.Option
|
||||
key={item.id}>{item.id.substr(0, 6)} {item['date']} {item['author']} {item['message']}</Select.Option>
|
||||
)) : null}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
<Input placeholder="请输入备注信息"/>
|
||||
</Form.Item>
|
||||
<Form.Item required label="发布目标主机" help="通过点击主机名称自由选择本次发布的主机。">
|
||||
{store.record['app_host_ids'].map(id => (
|
||||
<Form.Item required label="发布主机" help="通过点击主机名称自由选择本次发布的主机。">
|
||||
{store.record.app_host_ids.map(id => (
|
||||
<Tag.CheckableTag key={id} checked={host_ids.includes(id)} onChange={() => handleChange(id)}>
|
||||
{lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)})
|
||||
</Tag.CheckableTag>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* 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 (
|
||||
<pre ref={el} className={styles.out}>{props.records}</pre>
|
||||
)
|
||||
}
|
||||
|
||||
export default OutView
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<BranchesOutlined/> {ext1}#{ext2.substr(0, 6)}
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TagOutlined/> {ext1}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
const [ext1] = info.rep_extra;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ext1 === 'branch' ? <BranchesOutlined/> : <TagOutlined/>} {info.version}
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -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 <Action>
|
||||
<Action.Link
|
||||
auth="deploy.request.do"
|
||||
to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</Action.Link>
|
||||
<Action.Link auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Action.Link>
|
||||
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||
<Popconfirm title="确认要执行该发布申请?" onConfirm={() => store.showConsole(info)}>
|
||||
<Action.Button auth="deploy.request.do">发布</Action.Button>
|
||||
</Popconfirm>
|
||||
<Action.Button
|
||||
auth="deploy.request.do"
|
||||
disabled={info.type === '2'}
|
||||
|
@ -138,7 +130,7 @@ class ComTable extends React.Component {
|
|||
</Action>;
|
||||
case '1':
|
||||
return <Action>
|
||||
<Action.Link auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Action.Link>
|
||||
<Action.Button auth="deploy.request.do" onClick={() => store.showConsole(info)}>发布</Action.Button>
|
||||
<Action.Button auth="deploy.request.del" onClick={() => this.handleDelete(info)}>删除</Action.Button>
|
||||
</Action>;
|
||||
case '2':
|
||||
|
|
|
@ -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 && <Ext1Form/>}
|
||||
{store.ext2Visible && <Ext2Form/>}
|
||||
{store.approveVisible && <Approve/>}
|
||||
{store.tabs.length > 0 && (
|
||||
<Row gutter={12} className={styles.miniConsole}>
|
||||
{store.tabs.map(item => (
|
||||
<Col key={item.id}>
|
||||
<Ext1Console request={item}/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</AuthDiv>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ export default function () {
|
|||
<a title="文档" href="https://www.spug.dev/docs/about-spug/" target="_blank"
|
||||
rel="noopener noreferrer">文档</a>
|
||||
</div>
|
||||
<div style={{color: 'rgba(0, 0, 0, .45)'}}>Copyright <CopyrightOutlined/> 2020 By OpenSpug</div>
|
||||
<div style={{color: 'rgba(0, 0, 0, .45)'}}>Copyright <CopyrightOutlined/> 2021 By OpenSpug</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue