mirror of https://github.com/openspug/spug
				
				
				
			add module repository
							parent
							
								
									73d859aadb
								
							
						
					
					
						commit
						807f870fb2
					
				| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django.db import models
 | 
			
		||||
from libs.mixins import ModelMixin
 | 
			
		||||
from apps.app.models import App, Environment, Deploy
 | 
			
		||||
from apps.account.models import User
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Repository(models.Model, ModelMixin):
 | 
			
		||||
    STATUS = (
 | 
			
		||||
        ('0', '未开始'),
 | 
			
		||||
        ('1', '构建中'),
 | 
			
		||||
        ('2', '失败'),
 | 
			
		||||
        ('5', '成功'),
 | 
			
		||||
    )
 | 
			
		||||
    app = models.ForeignKey(App, on_delete=models.PROTECT)
 | 
			
		||||
    env = models.ForeignKey(Environment, on_delete=models.PROTECT)
 | 
			
		||||
    deploy = models.ForeignKey(Deploy, on_delete=models.PROTECT)
 | 
			
		||||
    version = models.CharField(max_length=50)
 | 
			
		||||
    spug_version = models.CharField(max_length=50)
 | 
			
		||||
    remarks = models.CharField(max_length=255, null=True)
 | 
			
		||||
    extra = models.TextField()
 | 
			
		||||
    status = models.CharField(max_length=2, choices=STATUS, default='0')
 | 
			
		||||
    created_at = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def make_spug_version(deploy_id):
 | 
			
		||||
        return f'{deploy_id}_{datetime.now().strftime("%Y%m%d%H%M%S")}'
 | 
			
		||||
 | 
			
		||||
    def to_view(self):
 | 
			
		||||
        tmp = self.to_dict()
 | 
			
		||||
        tmp['extra'] = json.loads(self.extra)
 | 
			
		||||
        tmp['status_alias'] = self.get_status_display()
 | 
			
		||||
        if hasattr(self, 'app_name'):
 | 
			
		||||
            tmp['app_name'] = self.app_name
 | 
			
		||||
        if hasattr(self, 'env_name'):
 | 
			
		||||
            tmp['env_name'] = self.env_name
 | 
			
		||||
        if hasattr(self, 'created_by_user'):
 | 
			
		||||
            tmp['created_by_user'] = self.created_by_user
 | 
			
		||||
        return tmp
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        db_table = 'repositories'
 | 
			
		||||
        ordering = ('-id',)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from .views import *
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('', RepositoryView.as_view()),
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,145 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import close_old_connections
 | 
			
		||||
from libs.utils import AttrDict, human_time
 | 
			
		||||
from apps.repository.models import Repository
 | 
			
		||||
import subprocess
 | 
			
		||||
import json
 | 
			
		||||
import uuid
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
REPOS_DIR = settings.REPOS_DIR
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpugError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dispatch(rep: Repository):
 | 
			
		||||
    rds = get_redis_connection()
 | 
			
		||||
    rds_key = f'{settings.BUILD_KEY}:{rep.spug_version}'
 | 
			
		||||
    rep.status = '1'
 | 
			
		||||
    rep.save()
 | 
			
		||||
    helper = Helper(rds, rds_key)
 | 
			
		||||
    try:
 | 
			
		||||
        api_token = uuid.uuid4().hex
 | 
			
		||||
        rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
 | 
			
		||||
        helper.send_info('local', f'完成\r\n{human_time()} 构建准备...        ')
 | 
			
		||||
        env = AttrDict(
 | 
			
		||||
            SPUG_APP_NAME=rep.app.name,
 | 
			
		||||
            SPUG_APP_ID=str(rep.app_id),
 | 
			
		||||
            SPUG_DEPLOY_ID=str(rep.deploy_id),
 | 
			
		||||
            SPUG_BUILD_ID=str(rep.id),
 | 
			
		||||
            SPUG_ENV_ID=str(rep.env_id),
 | 
			
		||||
            SPUG_ENV_KEY=rep.env.key,
 | 
			
		||||
            SPUG_VERSION=rep.version,
 | 
			
		||||
            SPUG_API_TOKEN=api_token,
 | 
			
		||||
            SPUG_REPOS_DIR=REPOS_DIR,
 | 
			
		||||
        )
 | 
			
		||||
        _build(rep, helper, env)
 | 
			
		||||
        rep.status = '5'
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        rep.status = '2'
 | 
			
		||||
        raise e
 | 
			
		||||
    finally:
 | 
			
		||||
        helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
 | 
			
		||||
        close_old_connections()
 | 
			
		||||
        # save the build log for two weeks
 | 
			
		||||
        rds.expire(rds_key, 14 * 24 * 60 * 60)
 | 
			
		||||
        rds.close()
 | 
			
		||||
        rep.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _build(rep: Repository, helper, env):
 | 
			
		||||
    extend = rep.deploy.extend_obj
 | 
			
		||||
    extras = json.loads(rep.extra)
 | 
			
		||||
    git_dir = os.path.join(REPOS_DIR, str(rep.deploy_id))
 | 
			
		||||
    build_dir = os.path.join(REPOS_DIR, rep.spug_version)
 | 
			
		||||
    tar_file = os.path.join(REPOS_DIR, 'build', f'{rep.spug_version}.tar.gz')
 | 
			
		||||
    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])
 | 
			
		||||
    helper.send_info('local', '完成\r\n')
 | 
			
		||||
 | 
			
		||||
    if extend.hook_pre_server:
 | 
			
		||||
        helper.send_step('local', 1, f'{human_time()} 检出前任务...\r\n')
 | 
			
		||||
        helper.local(f'cd {git_dir} && {extend.hook_pre_server}', env)
 | 
			
		||||
 | 
			
		||||
    helper.send_step('local', 2, f'{human_time()} 执行检出...        ')
 | 
			
		||||
    command = f'cd {git_dir} && git archive --prefix={rep.spug_version}/ {tree_ish} | (cd .. && tar xf -)'
 | 
			
		||||
    helper.local(command)
 | 
			
		||||
    helper.send_info('local', '完成\r\n')
 | 
			
		||||
 | 
			
		||||
    if extend.hook_post_server:
 | 
			
		||||
        helper.send_step('local', 3, f'{human_time()} 检出后任务...\r\n')
 | 
			
		||||
        helper.local(f'cd {build_dir} && {extend.hook_post_server}', env)
 | 
			
		||||
 | 
			
		||||
    helper.send_step('local', 4, f'\r\n{human_time()} 执行打包...        ')
 | 
			
		||||
    filter_rule, exclude, contain = json.loads(extend.filter_rule), '', rep.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={rep.spug_version}{x}')
 | 
			
		||||
                else:
 | 
			
		||||
                    excludes.append(f'--exclude={x}')
 | 
			
		||||
            exclude = ' '.join(excludes)
 | 
			
		||||
        else:
 | 
			
		||||
            contain = ' '.join(f'{rep.spug_version}/{x}' for x in files)
 | 
			
		||||
    helper.local(f'cd {REPOS_DIR} && tar zcf {tar_file} {exclude} {contain}')
 | 
			
		||||
    helper.send_step('local', 5, f'完成')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Helper:
 | 
			
		||||
    def __init__(self, rds, key):
 | 
			
		||||
        self.rds = rds
 | 
			
		||||
        self.key = key
 | 
			
		||||
        self.rds.delete(self.key)
 | 
			
		||||
 | 
			
		||||
    def parse_filter_rule(self, data: str, sep='\n'):
 | 
			
		||||
        data, files = data.strip(), []
 | 
			
		||||
        if data:
 | 
			
		||||
            for line in data.split(sep):
 | 
			
		||||
                line = line.strip()
 | 
			
		||||
                if line and not line.startswith('#'):
 | 
			
		||||
                    files.append(line)
 | 
			
		||||
        return files
 | 
			
		||||
 | 
			
		||||
    def _send(self, message):
 | 
			
		||||
        print(message)
 | 
			
		||||
        self.rds.rpush(self.key, json.dumps(message))
 | 
			
		||||
 | 
			
		||||
    def send_info(self, key, message):
 | 
			
		||||
        self._send({'key': key, 'data': message})
 | 
			
		||||
 | 
			
		||||
    def send_error(self, key, message, with_break=True):
 | 
			
		||||
        message = '\r\n' + message
 | 
			
		||||
        self._send({'key': key, 'status': 'error', 'data': message})
 | 
			
		||||
        if with_break:
 | 
			
		||||
            raise SpugError
 | 
			
		||||
 | 
			
		||||
    def send_step(self, key, step, data):
 | 
			
		||||
        self._send({'key': key, 'step': step, 'data': data})
 | 
			
		||||
 | 
			
		||||
    def local(self, command, env=None):
 | 
			
		||||
        if env:
 | 
			
		||||
            env = dict(env.items())
 | 
			
		||||
            env.update(os.environ)
 | 
			
		||||
        command = 'set -e\n' + command
 | 
			
		||||
        task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 | 
			
		||||
        while True:
 | 
			
		||||
            message = task.stdout.readline()
 | 
			
		||||
            if not message:
 | 
			
		||||
                break
 | 
			
		||||
            self.send_info('local', message.decode())
 | 
			
		||||
        if task.wait() != 0:
 | 
			
		||||
            self.send_error('local', f'exit code: {task.returncode}')
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django.views.generic import View
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from libs import json_response, JsonParser, Argument
 | 
			
		||||
from apps.repository.models import Repository
 | 
			
		||||
from apps.repository.utils import dispatch
 | 
			
		||||
from apps.app.models import Deploy
 | 
			
		||||
from threading import Thread
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RepositoryView(View):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        data = Repository.objects.annotate(
 | 
			
		||||
            app_name=F('app__name'),
 | 
			
		||||
            env_name=F('env__name'),
 | 
			
		||||
            created_by_user=F('created_by__nickname'))
 | 
			
		||||
        return json_response([x.to_view() for x in data])
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        form, error = JsonParser(
 | 
			
		||||
            Argument('deploy_id', type=int, help='参数错误'),
 | 
			
		||||
            Argument('version', help='请输入构建版本'),
 | 
			
		||||
            Argument('extra', type=list, help='参数错误'),
 | 
			
		||||
            Argument('remarks', 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='未找到指定发布配置')
 | 
			
		||||
            form.extra = json.dumps(form.extra)
 | 
			
		||||
            form.spug_version = Repository.make_spug_version(deploy.id)
 | 
			
		||||
            rep = Repository.objects.create(
 | 
			
		||||
                app_id=deploy.app_id,
 | 
			
		||||
                env_id=deploy.env_id,
 | 
			
		||||
                created_by=request.user,
 | 
			
		||||
                **form)
 | 
			
		||||
            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='请指定操作对象')
 | 
			
		||||
        ).parse(request.GET)
 | 
			
		||||
        if error is None:
 | 
			
		||||
            repository = Repository.objects.filter(pk=form.id).first()
 | 
			
		||||
            if not repository:
 | 
			
		||||
                return json_response(error='未找到指定构建记录')
 | 
			
		||||
 | 
			
		||||
        return json_response(error=error)
 | 
			
		||||
| 
						 | 
				
			
			@ -2,10 +2,12 @@
 | 
			
		|||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from channels.generic.websocket import WebsocketConsumer
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from asgiref.sync import async_to_sync
 | 
			
		||||
from apps.host.models import Host
 | 
			
		||||
from threading import Thread
 | 
			
		||||
import time
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +36,44 @@ class ExecConsumer(WebsocketConsumer):
 | 
			
		|||
        self.send(text_data='pong')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ComConsumer(WebsocketConsumer):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        token = self.scope['url_route']['kwargs']['token']
 | 
			
		||||
        module = self.scope['url_route']['kwargs']['module']
 | 
			
		||||
        if module == 'build':
 | 
			
		||||
            self.key = f'{settings.BUILD_KEY}:{token}'
 | 
			
		||||
        else:
 | 
			
		||||
            raise TypeError(f'unknown module for {module}')
 | 
			
		||||
        self.rds = get_redis_connection()
 | 
			
		||||
 | 
			
		||||
    def connect(self):
 | 
			
		||||
        self.accept()
 | 
			
		||||
 | 
			
		||||
    def disconnect(self, code):
 | 
			
		||||
        self.rds.close()
 | 
			
		||||
 | 
			
		||||
    def get_response(self, index):
 | 
			
		||||
        counter = 0
 | 
			
		||||
        while counter < 30:
 | 
			
		||||
            response = self.rds.lindex(self.key, index)
 | 
			
		||||
            if response:
 | 
			
		||||
                return response.decode()
 | 
			
		||||
            counter += 1
 | 
			
		||||
            time.sleep(0.2)
 | 
			
		||||
 | 
			
		||||
    def receive(self, text_data='', **kwargs):
 | 
			
		||||
        if text_data.isdigit():
 | 
			
		||||
            index = int(text_data)
 | 
			
		||||
            response = self.get_response(index)
 | 
			
		||||
            while response:
 | 
			
		||||
                index += 1
 | 
			
		||||
                self.send(text_data=response)
 | 
			
		||||
                time.sleep(1)
 | 
			
		||||
                response = self.get_response(index)
 | 
			
		||||
        self.send(text_data='pong')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SSHConsumer(WebsocketConsumer):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ ws_router = AuthMiddleware(
 | 
			
		|||
    URLRouter([
 | 
			
		||||
        path('ws/exec/<str:token>/', ExecConsumer),
 | 
			
		||||
        path('ws/ssh/<int:id>/', SSHConsumer),
 | 
			
		||||
        path('ws/<str:module>/<str:token>/', ComConsumer),
 | 
			
		||||
        path('ws/notify/', NotifyConsumer),
 | 
			
		||||
    ])
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,6 +46,7 @@ INSTALLED_APPS = [
 | 
			
		|||
    'apps.app',
 | 
			
		||||
    'apps.deploy',
 | 
			
		||||
    'apps.notify',
 | 
			
		||||
    'apps.repository',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +104,7 @@ TEMPLATES = [
 | 
			
		|||
SCHEDULE_KEY = 'spug:schedule'
 | 
			
		||||
MONITOR_KEY = 'spug:monitor'
 | 
			
		||||
REQUEST_KEY = 'spug:request'
 | 
			
		||||
BUILD_KEY = 'spug:build'
 | 
			
		||||
REPOS_DIR = os.path.join(BASE_DIR, 'repos')
 | 
			
		||||
 | 
			
		||||
# Internationalization
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +118,7 @@ USE_I18N = True
 | 
			
		|||
 | 
			
		||||
USE_L10N = True
 | 
			
		||||
 | 
			
		||||
USE_TZ = True
 | 
			
		||||
USE_TZ = False
 | 
			
		||||
 | 
			
		||||
AUTHENTICATION_EXCLUDES = (
 | 
			
		||||
    '/account/login/',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,7 @@ urlpatterns = [
 | 
			
		|||
    path('config/', include('apps.config.urls')),
 | 
			
		||||
    path('app/', include('apps.app.urls')),
 | 
			
		||||
    path('deploy/', include('apps.deploy.urls')),
 | 
			
		||||
    path('repository/', include('apps.repository.urls')),
 | 
			
		||||
    path('home/', include('apps.home.urls')),
 | 
			
		||||
    path('notify/', include('apps.notify.urls')),
 | 
			
		||||
    path('file/', include('apps.file.urls')),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,80 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
 * Copyright (c) <spug.dev@gmail.com>
 | 
			
		||||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { observer } from 'mobx-react';
 | 
			
		||||
import { FullscreenOutlined, FullscreenExitOutlined, LoadingOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Modal, Steps } from 'antd';
 | 
			
		||||
import { X_TOKEN, human_time } from 'libs';
 | 
			
		||||
import styles from './index.module.less';
 | 
			
		||||
import store from './store';
 | 
			
		||||
 | 
			
		||||
export default observer(function Console() {
 | 
			
		||||
  const [fullscreen, setFullscreen] = useState(false);
 | 
			
		||||
  const [step, setStep] = useState(0);
 | 
			
		||||
  const [status, setStatus] = useState('process')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.outputs = [`${human_time()} 建立连接...        `]
 | 
			
		||||
    let index = 0;
 | 
			
		||||
    const token = store.record.spug_version;
 | 
			
		||||
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
 | 
			
		||||
    const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/build/${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 {data, step, status} = JSON.parse(e.data);
 | 
			
		||||
        if (data !== undefined) store.outputs.push(data);
 | 
			
		||||
        if (step !== undefined) setStep(step);
 | 
			
		||||
        if (status !== undefined) setStatus(status);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.close();
 | 
			
		||||
      store.outputs = []
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  function handleClose() {
 | 
			
		||||
    store.fetchRecords();
 | 
			
		||||
    store.logVisible = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function StepItem(props) {
 | 
			
		||||
    let icon = null;
 | 
			
		||||
    if (props.step === step && status === 'process') {
 | 
			
		||||
      icon = <LoadingOutlined style={{fontSize: 32}}/>
 | 
			
		||||
    }
 | 
			
		||||
    return <Steps.Step {...props} icon={icon}/>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      visible
 | 
			
		||||
      width={fullscreen ? '100%' : 1000}
 | 
			
		||||
      title={[
 | 
			
		||||
        <span key="1">构建控制台</span>,
 | 
			
		||||
        <div key="2" className={styles.fullscreen} onClick={() => setFullscreen(!fullscreen)}>
 | 
			
		||||
          {fullscreen ? <FullscreenExitOutlined/> : <FullscreenOutlined/>}
 | 
			
		||||
        </div>
 | 
			
		||||
      ]}
 | 
			
		||||
      footer={null}
 | 
			
		||||
      onCancel={handleClose}
 | 
			
		||||
      className={styles.console}
 | 
			
		||||
      maskClosable={false}>
 | 
			
		||||
      <Steps current={step} status={status}>
 | 
			
		||||
        <StepItem title="构建准备" step={0}/>
 | 
			
		||||
        <StepItem title="检出前任务" step={1}/>
 | 
			
		||||
        <StepItem title="执行检出" step={2}/>
 | 
			
		||||
        <StepItem title="检出后任务" step={3}/>
 | 
			
		||||
        <StepItem title="执行打包" step={4}/>
 | 
			
		||||
      </Steps>
 | 
			
		||||
      <pre className={styles.out}>{store.outputs}</pre>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { observer } from 'mobx-react';
 | 
			
		||||
import { Drawer, Descriptions, Table } from 'antd';
 | 
			
		||||
import { http } from 'libs';
 | 
			
		||||
import store from './store';
 | 
			
		||||
import styles from './index.module.less';
 | 
			
		||||
 | 
			
		||||
export default observer(function (props) {
 | 
			
		||||
  const [fetching, setFetching] = useState(true);
 | 
			
		||||
  const [order, setOrder] = useState({});
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (store.record.id && props.visible) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }, [props.visible])
 | 
			
		||||
 | 
			
		||||
  const columns = [{
 | 
			
		||||
    title: '应用名称',
 | 
			
		||||
    key: 'name',
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '商品主图',
 | 
			
		||||
    dataIndex: 'pic',
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '单价',
 | 
			
		||||
    dataIndex: 'price',
 | 
			
		||||
    align: 'right',
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '数量',
 | 
			
		||||
    key: 'number',
 | 
			
		||||
    align: 'right',
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '金额',
 | 
			
		||||
    key: 'money',
 | 
			
		||||
    align: 'right',
 | 
			
		||||
  }]
 | 
			
		||||
 | 
			
		||||
  const record = store.record;
 | 
			
		||||
  const [extra1, extra2, extra3] = record.extra || [];
 | 
			
		||||
  return (
 | 
			
		||||
    <Drawer width={550} visible={props.visible} onClose={() => store.detailVisible = false}>
 | 
			
		||||
      <Descriptions column={1} title={<span style={{fontSize: 22}}>基本信息</span>}>
 | 
			
		||||
        <Descriptions.Item label="应用">{record.app_name}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="环境">{record.env_name}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="版本">{record.version}</Descriptions.Item>
 | 
			
		||||
        {extra1 === 'branch' ? ([
 | 
			
		||||
          <Descriptions.Item key="1" label="Git分支">{extra2}</Descriptions.Item>,
 | 
			
		||||
          <Descriptions.Item key="2" label="CommitID">{extra3}</Descriptions.Item>,
 | 
			
		||||
        ]) : (
 | 
			
		||||
          <Descriptions.Item label="Git标签">{extra2}</Descriptions.Item>
 | 
			
		||||
        )}
 | 
			
		||||
        <Descriptions.Item label="内部版本">{record.spug_version}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="构建时间">{record.created_at}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="备注信息">{record.remarks}</Descriptions.Item>
 | 
			
		||||
        <Descriptions.Item label="构建人">{record.created_by_user}</Descriptions.Item>
 | 
			
		||||
      </Descriptions>
 | 
			
		||||
      <Descriptions title={<span style={{fontSize: 22}}>发布记录</span>} style={{marginTop: 24}}/>
 | 
			
		||||
      <Table rowKey="id" loading={fetching} columns={columns} dataSource={[]} pagination={false}/>
 | 
			
		||||
    </Drawer>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,158 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
 * Copyright (c) <spug.dev@gmail.com>
 | 
			
		||||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { observer } from 'mobx-react';
 | 
			
		||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Modal, Form, Input, Select, Button, message } from 'antd';
 | 
			
		||||
import http from 'libs/http';
 | 
			
		||||
import store from './store';
 | 
			
		||||
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.deploy, 'extra.0', 'branch'));
 | 
			
		||||
  const [extra1, setExtra1] = useState(lds.get(store.deploy, 'extra.1'));
 | 
			
		||||
  const [extra2, setExtra2] = useState(lds.get(store.deploy, 'extra.2'));
 | 
			
		||||
  const [versions, setVersions] = useState({});
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchVersions();
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  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.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() {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const formData = form.getFieldsValue();
 | 
			
		||||
    formData['deploy_id'] = store.deploy.id;
 | 
			
		||||
    formData['extra'] = [git_type, extra1, extra2];
 | 
			
		||||
    http.post('/api/repository/', formData)
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        message.success('操作成功');
 | 
			
		||||
        store.record = res;
 | 
			
		||||
        store.formVisible = false;
 | 
			
		||||
        store.fetchRecords()
 | 
			
		||||
      }, () => setLoading(false))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const {branches, tags} = versions;
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      visible
 | 
			
		||||
      width={800}
 | 
			
		||||
      maskClosable={false}
 | 
			
		||||
      title="新建构建"
 | 
			
		||||
      onCancel={() => store.formVisible = false}
 | 
			
		||||
      confirmLoading={loading}
 | 
			
		||||
      onOk={handleSubmit}>
 | 
			
		||||
      <Form form={form} initialValues={store.record} labelCol={{span: 5}} wrapperCol={{span: 17}}>
 | 
			
		||||
        <Form.Item required name="version" 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>
 | 
			
		||||
        {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="remarks" label="备注信息">
 | 
			
		||||
          <Input placeholder="请输入备注信息"/>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
      </Form>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 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 { Table, Modal, Tag, message } from 'antd';
 | 
			
		||||
import { PlusOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Action, TableCard, AuthButton } from 'components';
 | 
			
		||||
import { http, hasPermission } from 'libs';
 | 
			
		||||
import store from './store';
 | 
			
		||||
 | 
			
		||||
function ComTable() {
 | 
			
		||||
  function handleDelete(info) {
 | 
			
		||||
    Modal.confirm({
 | 
			
		||||
      title: '删除确认',
 | 
			
		||||
      content: `确定要删除【${info['name']}】?`,
 | 
			
		||||
      onOk: () => {
 | 
			
		||||
        return http.delete('/api/config/environment/', {params: {id: info.id}})
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            message.success('删除成功');
 | 
			
		||||
            store.fetchRecords()
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const statusColorMap = {'0': 'cyan', '1': 'blue', '2': 'red', '5': 'green'};
 | 
			
		||||
  return (
 | 
			
		||||
    <TableCard
 | 
			
		||||
      rowKey="id"
 | 
			
		||||
      title="构建版本列表"
 | 
			
		||||
      loading={store.isFetching}
 | 
			
		||||
      dataSource={store.dataSource}
 | 
			
		||||
      onReload={store.fetchRecords}
 | 
			
		||||
      actions={[
 | 
			
		||||
        <AuthButton
 | 
			
		||||
          auth="config.env.add"
 | 
			
		||||
          type="primary"
 | 
			
		||||
          icon={<PlusOutlined/>}
 | 
			
		||||
          onClick={() => store.addVisible = true}>新建</AuthButton>
 | 
			
		||||
      ]}
 | 
			
		||||
      pagination={{
 | 
			
		||||
        showSizeChanger: true,
 | 
			
		||||
        showLessItems: true,
 | 
			
		||||
        hideOnSinglePage: true,
 | 
			
		||||
        showTotal: total => `共 ${total} 条`,
 | 
			
		||||
        pageSizeOptions: ['10', '20', '50', '100']
 | 
			
		||||
      }}>
 | 
			
		||||
      <Table.Column ellipsis title="应用" dataIndex="app_name"/>
 | 
			
		||||
      <Table.Column title="环境" dataIndex="env_name"/>
 | 
			
		||||
      <Table.Column title="版本" dataIndex="version"/>
 | 
			
		||||
      <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>}/>
 | 
			
		||||
      {hasPermission('config.env.edit|config.env.del') && (
 | 
			
		||||
        <Table.Column width={150} title="操作" render={info => (
 | 
			
		||||
          <Action>
 | 
			
		||||
            <Action.Button auth="config.env.edit" onClick={() => store.showDetail(info)}>详情</Action.Button>
 | 
			
		||||
            <Action.Button auth="config.env.del" onClick={() => store.showConsole(info)}>日志</Action.Button>
 | 
			
		||||
          </Action>
 | 
			
		||||
        )}/>
 | 
			
		||||
      )}
 | 
			
		||||
    </TableCard>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default observer(ComTable)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 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 } from 'mobx-react';
 | 
			
		||||
import { Select } from 'antd';
 | 
			
		||||
import { SearchForm, AuthDiv, Breadcrumb, AppSelector } from 'components';
 | 
			
		||||
import { includes } from 'libs';
 | 
			
		||||
import ComTable from './Table';
 | 
			
		||||
import ComForm from './Form';
 | 
			
		||||
import Console from './Console';
 | 
			
		||||
import Detail from './Detail';
 | 
			
		||||
import store from './store';
 | 
			
		||||
import envStore from 'pages/config/environment/store';
 | 
			
		||||
import appStore from 'pages/config/app/store';
 | 
			
		||||
 | 
			
		||||
export default observer(function () {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.fetchRecords();
 | 
			
		||||
    if (!appStore.records.length) appStore.fetchRecords()
 | 
			
		||||
  }, [])
 | 
			
		||||
  return (
 | 
			
		||||
    <AuthDiv auth="config.env.view">
 | 
			
		||||
      <Breadcrumb>
 | 
			
		||||
        <Breadcrumb.Item>首页</Breadcrumb.Item>
 | 
			
		||||
        <Breadcrumb.Item>应用发布</Breadcrumb.Item>
 | 
			
		||||
        <Breadcrumb.Item>构建仓库</Breadcrumb.Item>
 | 
			
		||||
      </Breadcrumb>
 | 
			
		||||
      <SearchForm>
 | 
			
		||||
        <SearchForm.Item span={6} title="应用">
 | 
			
		||||
          <Select
 | 
			
		||||
            allowClear
 | 
			
		||||
            showSearch
 | 
			
		||||
            value={store.f_app_id}
 | 
			
		||||
            onChange={v => store.f_app_id = v}
 | 
			
		||||
            filterOption={(i, o) => includes(o.children, i)}
 | 
			
		||||
            placeholder="请选择">
 | 
			
		||||
            {appStore.records.map(item => (
 | 
			
		||||
              <Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Select>
 | 
			
		||||
        </SearchForm.Item>
 | 
			
		||||
        <SearchForm.Item span={6} title="环境">
 | 
			
		||||
          <Select
 | 
			
		||||
            allowClear
 | 
			
		||||
            showSearch
 | 
			
		||||
            value={store.f_env_id}
 | 
			
		||||
            onChange={v => store.f_env_id = v}
 | 
			
		||||
            filterOption={(i, o) => includes(o.children, i)}
 | 
			
		||||
            placeholder="请选择">
 | 
			
		||||
            {envStore.records.map(item => (
 | 
			
		||||
              <Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Select>
 | 
			
		||||
        </SearchForm.Item>
 | 
			
		||||
      </SearchForm>
 | 
			
		||||
      <ComTable/>
 | 
			
		||||
      <AppSelector
 | 
			
		||||
        visible={store.addVisible}
 | 
			
		||||
        filter={item => item.extend === '1'}
 | 
			
		||||
        onCancel={() => store.addVisible = false}
 | 
			
		||||
        onSelect={store.confirmAdd}/>
 | 
			
		||||
      <Detail visible={store.detailVisible}/>
 | 
			
		||||
      {store.formVisible && <ComForm/>}
 | 
			
		||||
      {store.logVisible && <Console/>}
 | 
			
		||||
    </AuthDiv>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
.console {
 | 
			
		||||
  .fullscreen {
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fullscreen:hover {
 | 
			
		||||
    color: #000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .out {
 | 
			
		||||
    margin-top: 24px;
 | 
			
		||||
    min-height: 40px;
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
    padding: 10px 15px;
 | 
			
		||||
    border: 1px solid #d9d9d9;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: #fafafa;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.split {
 | 
			
		||||
  height: 1px;
 | 
			
		||||
  background-color: #eee;
 | 
			
		||||
  margin: 16px 0 24px 0;
 | 
			
		||||
  clear: both
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
 * Copyright (c) <spug.dev@gmail.com>
 | 
			
		||||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import { observable, computed } from "mobx";
 | 
			
		||||
import http from 'libs/http';
 | 
			
		||||
 | 
			
		||||
class Store {
 | 
			
		||||
  @observable records = [];
 | 
			
		||||
  @observable record = {};
 | 
			
		||||
  @observable idMap = {};
 | 
			
		||||
  @observable outputs = [];
 | 
			
		||||
  @observable isFetching = false;
 | 
			
		||||
  @observable formVisible = false;
 | 
			
		||||
  @observable addVisible = false;
 | 
			
		||||
  @observable logVisible = false;
 | 
			
		||||
  @observable detailVisible = false;
 | 
			
		||||
 | 
			
		||||
  @observable f_app_id;
 | 
			
		||||
  @observable f_env_id;
 | 
			
		||||
 | 
			
		||||
  @computed get dataSource() {
 | 
			
		||||
    let records = this.records;
 | 
			
		||||
    if (this.f_app_id) records = records.filter(x => x.app_id === this.f_app_id);
 | 
			
		||||
    if (this.f_env_id) records = records.filter(x => x.env_id === this.f_env_id);
 | 
			
		||||
    return records
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetchRecords = () => {
 | 
			
		||||
    this.isFetching = true;
 | 
			
		||||
    return http.get('/api/repository/')
 | 
			
		||||
      .then(res => this.records = res)
 | 
			
		||||
      .finally(() => this.isFetching = false)
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  confirmAdd = (deploy) => {
 | 
			
		||||
    this.deploy = deploy;
 | 
			
		||||
    this.formVisible = true;
 | 
			
		||||
    this.addVisible = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  showConsole = (info) => {
 | 
			
		||||
    this.record = info;
 | 
			
		||||
    this.logVisible = true
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  showDetail = (info) => {
 | 
			
		||||
    this.record = info;
 | 
			
		||||
    this.detailVisible = true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Store()
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ import ExecTask from './pages/exec/task';
 | 
			
		|||
import ExecTemplate from './pages/exec/template';
 | 
			
		||||
 | 
			
		||||
import DeployApp from './pages/deploy/app';
 | 
			
		||||
import DeployRepository from './pages/deploy/repository';
 | 
			
		||||
import DeployRequest from './pages/deploy/request';
 | 
			
		||||
import DoExt1Index from './pages/deploy/do/Ext1Index';
 | 
			
		||||
import DoExt2Index from './pages/deploy/do/Ext2Index';
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +60,7 @@ export default [
 | 
			
		|||
  {
 | 
			
		||||
    icon: <FlagOutlined/>, title: '应用发布', auth: 'deploy.app.view|deploy.request.view', child: [
 | 
			
		||||
      {title: '应用管理', auth: 'deploy.app.view', path: '/deploy/app', component: DeployApp},
 | 
			
		||||
      {title: '构建仓库', auth: 'deploy.repository.view', path: '/deploy/repository', component: DeployRepository},
 | 
			
		||||
      {title: '发布申请', auth: 'deploy.request.view', path: '/deploy/request', component: DeployRequest},
 | 
			
		||||
      {path: '/deploy/do/ext1/:id', component: DoExt1Index},
 | 
			
		||||
      {path: '/deploy/do/ext2/:id', component: DoExt2Index},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue