mirror of https://github.com/openspug/spug
A web update
parent
61b697cd16
commit
0e57c71896
|
@ -24,7 +24,8 @@ class AddSelect extends React.Component {
|
|||
store.record = {
|
||||
is_audit: false,
|
||||
host_ids: [undefined],
|
||||
actions: [{target: 'server'}]
|
||||
host_actions: [],
|
||||
server_actions: []
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, Input, Button, message, Col, Radio, Icon } from 'antd';
|
||||
import { Form, Input, Button, message, Divider, Icon } from 'antd';
|
||||
import Editor from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-sh';
|
||||
import 'ace-builds/src-noconflict/theme-tomorrow';
|
||||
|
@ -19,9 +19,11 @@ class Ext2Setup3 extends React.Component {
|
|||
|
||||
handleSubmit = () => {
|
||||
this.setState({loading: true});
|
||||
store.record['extend'] = '2';
|
||||
store.record['actions'] = store.record['actions'].filter(x => x.title && x.data);
|
||||
http.post('/api/app/', store.record)
|
||||
const info = store.record;
|
||||
info['extend'] = '2';
|
||||
info['host_actions'] = info['host_actions'].filter(x => x.title && x.data);
|
||||
info['server_actions'] = info['server_actions'].filter(x => x.title && x.data);
|
||||
http.post('/api/app/', info)
|
||||
.then(res => {
|
||||
message.success('保存成功');
|
||||
store.ext2Visible = false;
|
||||
|
@ -30,23 +32,14 @@ class Ext2Setup3 extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const actions = store.record['actions'];
|
||||
const server_actions = store.record['server_actions'];
|
||||
const host_actions = store.record['host_actions'];
|
||||
return (
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} className={styles.ext2Form}>
|
||||
{actions.map((item, index) => (
|
||||
{server_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`动作${index + 1}`}>
|
||||
<Col span={9}>
|
||||
<Input value={item['title']} onChange={e => item['title'] = e.target.value} placeholder="请输入"/>
|
||||
</Col>
|
||||
<Col span={15}>
|
||||
<Form.Item labelCol={{span: 6}} wrapperCol={{span: 18}} label="目标">
|
||||
<Radio.Group value={item['target']} onChange={e => item['target'] = e.target.value}>
|
||||
<Radio value="server">服务本机</Radio>
|
||||
<Radio value="host">目标主机</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item required label={`本地动作${index + 1}`}>
|
||||
<Input value={item['title']} onChange={e => item['title'] = e.target.value} placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item required label="执行内容">
|
||||
|
@ -59,21 +52,47 @@ class Ext2Setup3 extends React.Component {
|
|||
onChange={v => item['data'] = v}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
{actions.length > 1 && (
|
||||
<div className={styles.delAction} onClick={() => actions.splice(index, 1)}><Icon
|
||||
type="minus-circle"/>移除</div>
|
||||
)}
|
||||
<div className={styles.delAction} onClick={() => server_actions.splice(index, 1)}>
|
||||
<Icon type="minus-circle"/>移除
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" block onClick={() => actions.push({target: 'server'})}>
|
||||
<Icon type="plus"/>添加执行动作
|
||||
<Button type="dashed" block onClick={() => server_actions.push({target: 'server'})}>
|
||||
<Icon type="plus"/>添加本地执行动作(在服务端本地执行)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Divider/>
|
||||
{host_actions.map((item, index) => (
|
||||
<div key={index} style={{marginBottom: 30, position: 'relative'}}>
|
||||
<Form.Item required label={`目标主机动作${index + 1}`}>
|
||||
<Input value={item['title']} onChange={e => item['title'] = e.target.value} placeholder="请输入"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item required label="执行内容">
|
||||
<Editor
|
||||
mode="sh"
|
||||
theme="tomorrow"
|
||||
width="100%"
|
||||
height="100px"
|
||||
value={item['data']}
|
||||
onChange={v => item['data'] = v}
|
||||
placeholder="请输入要执行的动作"/>
|
||||
</Form.Item>
|
||||
<div className={styles.delAction} onClick={() => host_actions.splice(index, 1)}>
|
||||
<Icon type="minus-circle"/>移除
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" block onClick={() => host_actions.push({target: 'server'})}>
|
||||
<Icon type="plus"/>添加目标主机执行动作(在部署目标主机执行)
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}} help="Spug 将遵循先本地后目标主机的原则,按照顺序依次执行添加的动作,例如:本地动作1 -> 本地动作2 -> 目标主机动作1 -> 目标主机动作2 ...">
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={actions.filter(x => x.title && x.data).length === 0}
|
||||
disabled={[...host_actions, ...server_actions].filter(x => x.title && x.data).length === 0}
|
||||
loading={this.state.loading}
|
||||
onClick={this.handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
|
|
|
@ -8,7 +8,7 @@ import store from './store';
|
|||
import lds from 'lodash';
|
||||
|
||||
@observer
|
||||
class Index extends React.Component {
|
||||
class Ext1Index extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -105,7 +105,7 @@ class Index extends React.Component {
|
|||
<Steps.Step {...this.getStatus('local', 4)} title="检出后任务"/>
|
||||
<Steps.Step {...this.getStatus('local', 5)} title="执行打包"/>
|
||||
</Steps>}>
|
||||
<pre className={styles.console}>{lds.get(store.outputs, 'local.data')}</pre>
|
||||
<pre className={styles.ext1Console}>{lds.get(store.outputs, 'local.data')}</pre>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
|
@ -124,7 +124,7 @@ class Index extends React.Component {
|
|||
<Steps.Step {...this.getStatus(item.id, 4)} title="发布后任务"/>
|
||||
</Steps>
|
||||
</div>}>
|
||||
<pre className={styles.console}>{lds.get(store.outputs, `${item.id}.data`)}</pre>
|
||||
<pre className={styles.ext1Console}>{lds.get(store.outputs, `${item.id}.data`)}</pre>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
|
@ -133,4 +133,4 @@ class Index extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default Index
|
||||
export default Ext1Index
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Steps, PageHeader, Spin, Tag, Button, Icon } from 'antd';
|
||||
import http from 'libs/http';
|
||||
import history from 'libs/history';
|
||||
import styles from './index.module.css';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
|
||||
@observer
|
||||
class Ext2Index extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fetching: true,
|
||||
loading: false,
|
||||
request: {},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.id = this.props.match.params.id;
|
||||
http.get(`/api/deploy/request/${this.id}/`)
|
||||
.then(res => store.request = res)
|
||||
.finally(() => this.setState({fetching: false}))
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.socket) this.socket.close();
|
||||
store.request = {targets: []};
|
||||
store.outputs = {};
|
||||
}
|
||||
|
||||
handleDeploy = () => {
|
||||
this.setState({loading: true});
|
||||
http.post(`/api/deploy/request/${this.id}/`)
|
||||
.then(({token, outputs}) => {
|
||||
store.request.status = '2';
|
||||
store.outputs = outputs;
|
||||
this.socket = new WebSocket(`ws://localhost:8000/ws/exec/${token}/`);
|
||||
this.socket.onopen = () => {
|
||||
this.socket.send('ok');
|
||||
};
|
||||
this.socket.onmessage = e => {
|
||||
if (e.data === 'pong') {
|
||||
this.socket.send('ping')
|
||||
} else {
|
||||
const {key, data, step, status} = JSON.parse(e.data);
|
||||
if (data !== undefined) store.outputs[key]['data'] += data;
|
||||
if (step !== undefined) store.outputs[key]['step'] = step;
|
||||
if (status !== undefined) store.outputs[key]['status'] = status;
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => this.setState({loading: false}))
|
||||
};
|
||||
|
||||
getStatus = (key, n) => {
|
||||
const step = lds.get(store.outputs, `${key}.step`, -1);
|
||||
const isError = lds.get(store.outputs, `${key}.status`) === 'error';
|
||||
const icon = <Icon type="loading"/>;
|
||||
if (n > step) {
|
||||
return {key: n, status: 'wait'}
|
||||
} else if (n === step) {
|
||||
return isError ? {key: n, status: 'error'} : {key: n, status: 'process', icon}
|
||||
} else {
|
||||
return {key: n, status: 'finish'}
|
||||
}
|
||||
};
|
||||
|
||||
getStatusAlias = () => {
|
||||
if (Object.keys(store.outputs).length !== 0) {
|
||||
for (let item of [{id: 'local'}, ...store.request.targets]) {
|
||||
if (lds.get(store.outputs, `${item.id}.status`) === 'error') {
|
||||
return <Tag color="red">发布异常</Tag>
|
||||
} else if (lds.get(store.outputs, `${item.id}.step`, -1) < 5) {
|
||||
return <Tag color="blue">发布中</Tag>
|
||||
}
|
||||
}
|
||||
return <Tag color="green">发布成功</Tag>
|
||||
} else {
|
||||
return <Tag>{store.request['status_alias'] || '...'}</Tag>
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {app_name, env_name, status} = store.request;
|
||||
return (
|
||||
<Spin spinning={this.state.fetching}>
|
||||
<PageHeader
|
||||
title="应用发布"
|
||||
subTitle={`${app_name} - ${env_name}`}
|
||||
style={{padding: 0}}
|
||||
tags={this.getStatusAlias()}
|
||||
extra={<Button loading={this.state.loading} type="primary" disabled={!['1', '-3'].includes(status)}
|
||||
onClick={this.handleDeploy}>发布</Button>}
|
||||
onBack={() => history.goBack()}/>
|
||||
<div className={styles.ext2Block}>
|
||||
<Steps direction="vertical" className={styles.ext2Step}>
|
||||
<Steps.Step {...this.getStatus('local', 0)} title="建立连接"/>
|
||||
<Steps.Step {...this.getStatus('local', 1)} title="发布准备"/>
|
||||
</Steps>
|
||||
<pre className={styles.ext2Console}>{lds.get(store.outputs, 'local.data')}</pre>
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Ext2Index
|
|
@ -10,12 +10,35 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.console {
|
||||
.ext1Console {
|
||||
min-height: 40px;
|
||||
max-height: 300px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.ext2Block {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
margin-top: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.ext2Console {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.ext2Step {
|
||||
padding: 24px;
|
||||
width: 220px;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.ext2Step :global(.ant-steps-item) {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
|
@ -17,6 +17,12 @@ class Ext2Form extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (hostStore.records.length === 0) {
|
||||
hostStore.fetchRecords()
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.state.host_ids.length === 0) {
|
||||
return message.error('请至少选择一个要发布的目标主机')
|
||||
|
@ -24,11 +30,13 @@ class Ext2Form extends React.Component {
|
|||
this.setState({loading: true});
|
||||
const formData = this.props.form.getFieldsValue();
|
||||
formData['id'] = store.record.id;
|
||||
formData['body'] = this.state.body;
|
||||
http.post('/api/exec/template/', formData)
|
||||
formData['app_id'] = store.record.app_id;
|
||||
formData['extra'] = [formData['extra']];
|
||||
formData['host_ids'] = this.state.host_ids;
|
||||
http.post('/api/deploy/request/', formData)
|
||||
.then(res => {
|
||||
message.success('操作成功');
|
||||
store.formVisible = false;
|
||||
store.ext2Visible = false;
|
||||
store.fetchRecords()
|
||||
}, () => this.setState({loading: false}))
|
||||
};
|
||||
|
@ -63,13 +71,18 @@ class Ext2Form extends React.Component {
|
|||
<Input placeholder="请输入申请标题"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="环境变量(_SPUG_RELEASE)" help="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $_SPUG_RELEASE 来使用该值">
|
||||
{getFieldDecorator('extra', {initialValue: info['extra']})(
|
||||
<Input placeholder="请输入环境变量 _SPUG_RELEASE 的值"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="备注信息">
|
||||
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
||||
<Input placeholder="请输入备注信息"/>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item required label="发布目标主机">
|
||||
{info['host_ids'].map(id => (
|
||||
{info['app_host_ids'].map(id => (
|
||||
<Tag.CheckableTag key={id} checked={host_ids.includes(id)} onChange={v => this.handleChange(id, v)}>
|
||||
{lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)})
|
||||
</Tag.CheckableTag>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal, Button, Menu, Icon } from 'antd';
|
||||
import store from './store';
|
||||
|
@ -45,6 +46,7 @@ class SelectApp extends React.Component {
|
|||
|
||||
render() {
|
||||
const {env_id} = this.state;
|
||||
const records = appStore.records.filter(x => String(x.env_id) === env_id);
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
|
@ -66,13 +68,15 @@ class SelectApp extends React.Component {
|
|||
</div>
|
||||
<div className={styles.right}>
|
||||
<div className={styles.title}>{lds.get(envStore.idMap, `${env_id}.name`)}</div>
|
||||
{appStore.records.map(item => (
|
||||
{records.map(item => (
|
||||
<Button key={item.id} type="primary" className={styles.appBlock} onClick={() => this.handleClick(item)}>
|
||||
<div style={{width: 135, overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
<Icon type={item.extend === '1' ? 'ordered-list' : 'build'} style={{marginRight: 10}}/>{item.name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
{records.length === 0 &&
|
||||
<div className={styles.tips}>该环境下还没有可发布的应用哦,快去<Link to="/deploy/app">应用管理</Link>创建应用吧。</div>}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -44,7 +44,7 @@ class ComTable extends React.Component {
|
|||
}
|
||||
} else {
|
||||
return <React.Fragment>
|
||||
<Icon type="build"/> xxx
|
||||
<Icon type="build"/> {info.extra[0]}
|
||||
</React.Fragment>
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ class ComTable extends React.Component {
|
|||
switch (info.status) {
|
||||
case '-3':
|
||||
return <React.Fragment>
|
||||
<Link to={`/deploy/do/${info.id}`}>发布</Link>
|
||||
<Link to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Link>
|
||||
<Divider type="vertical"/>
|
||||
<LinkButton
|
||||
disabled={info.type === '2'}
|
||||
|
@ -106,10 +106,10 @@ class ComTable extends React.Component {
|
|||
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
|
||||
<Divider type="vertical"/>
|
||||
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
case '1':
|
||||
return <React.Fragment>
|
||||
<Link to={`/deploy/do/${info.id}`}>发布</Link>
|
||||
<Link to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Link>
|
||||
<Divider type="vertical"/>
|
||||
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -27,4 +27,9 @@
|
|||
height: 60px;
|
||||
font-size: 18px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 32px;
|
||||
color: #888;
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { makeRoute } from "../../libs/router";
|
||||
import app from './app';
|
||||
import request from './request';
|
||||
import doIndex from './do';
|
||||
import doExt1Index from './do/Ext1Index';
|
||||
import doExt2Index from './do/Ext2Index';
|
||||
|
||||
|
||||
export default [
|
||||
makeRoute('/app', app),
|
||||
makeRoute('/request', request),
|
||||
makeRoute('/do/:id', doIndex),
|
||||
makeRoute('/do/ext1/:id', doExt1Index),
|
||||
makeRoute('/do/ext2/:id', doExt2Index),
|
||||
]
|
Loading…
Reference in New Issue