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