add module repository

pull/289/head
vapao 2021-03-08 11:30:15 +08:00
parent 73d859aadb
commit 807f870fb2
17 changed files with 836 additions and 1 deletions

View File

@ -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.

View File

@ -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',)

View File

@ -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()),
]

View File

@ -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}')

View File

@ -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)

View File

@ -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)

View File

@ -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),
])
)

View File

@ -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/',

View File

@ -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')),

View File

@ -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>
)
})

View File

@ -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>
)
})

View File

@ -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>
)
})

View File

@ -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)

View File

@ -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>
)
})

View File

@ -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
}

View File

@ -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()

View File

@ -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},