A web update

pull/22/head
雷二猛 2019-12-24 23:50:34 +08:00
parent 61b697cd16
commit 0e57c71896
10 changed files with 219 additions and 42 deletions

View File

@ -24,7 +24,8 @@ class AddSelect extends React.Component {
store.record = { store.record = {
is_audit: false, is_audit: false,
host_ids: [undefined], host_ids: [undefined],
actions: [{target: 'server'}] host_actions: [],
server_actions: []
} }
}; };

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-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 Editor from 'react-ace';
import 'ace-builds/src-noconflict/mode-sh'; import 'ace-builds/src-noconflict/mode-sh';
import 'ace-builds/src-noconflict/theme-tomorrow'; import 'ace-builds/src-noconflict/theme-tomorrow';
@ -19,9 +19,11 @@ class Ext2Setup3 extends React.Component {
handleSubmit = () => { handleSubmit = () => {
this.setState({loading: true}); this.setState({loading: true});
store.record['extend'] = '2'; const info = store.record;
store.record['actions'] = store.record['actions'].filter(x => x.title && x.data); info['extend'] = '2';
http.post('/api/app/', store.record) 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 => { .then(res => {
message.success('保存成功'); message.success('保存成功');
store.ext2Visible = false; store.ext2Visible = false;
@ -30,23 +32,14 @@ class Ext2Setup3 extends React.Component {
}; };
render() { render() {
const actions = store.record['actions']; const server_actions = store.record['server_actions'];
const host_actions = store.record['host_actions'];
return ( return (
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} className={styles.ext2Form}> <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'}}> <div key={index} style={{marginBottom: 30, position: 'relative'}}>
<Form.Item required label={`动作${index + 1}`}> <Form.Item required label={`本地动作${index + 1}`}>
<Col span={9}>
<Input value={item['title']} onChange={e => item['title'] = e.target.value} placeholder="请输入"/> <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> </Form.Item>
<Form.Item required label="执行内容"> <Form.Item required label="执行内容">
@ -59,21 +52,47 @@ class Ext2Setup3 extends React.Component {
onChange={v => item['data'] = v} onChange={v => item['data'] = v}
placeholder="请输入要执行的动作"/> placeholder="请输入要执行的动作"/>
</Form.Item> </Form.Item>
{actions.length > 1 && ( <div className={styles.delAction} onClick={() => server_actions.splice(index, 1)}>
<div className={styles.delAction} onClick={() => actions.splice(index, 1)}><Icon <Icon type="minus-circle"/>移除
type="minus-circle"/>移除</div> </div>
)}
</div> </div>
))} ))}
<Form.Item wrapperCol={{span: 14, offset: 6}}> <Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button type="dashed" block onClick={() => actions.push({target: 'server'})}> <Button type="dashed" block onClick={() => server_actions.push({target: 'server'})}>
<Icon type="plus"/>添加执行动作 <Icon type="plus"/>添加本地执行动作在服务端本地执行
</Button> </Button>
</Form.Item> </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}}> <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 <Button
type="primary" 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} loading={this.state.loading}
onClick={this.handleSubmit}>提交</Button> onClick={this.handleSubmit}>提交</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button> <Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>

View File

@ -8,7 +8,7 @@ import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@observer @observer
class Index extends React.Component { class Ext1Index extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -105,7 +105,7 @@ class Index extends React.Component {
<Steps.Step {...this.getStatus('local', 4)} title="检出后任务"/> <Steps.Step {...this.getStatus('local', 4)} title="检出后任务"/>
<Steps.Step {...this.getStatus('local', 5)} title="执行打包"/> <Steps.Step {...this.getStatus('local', 5)} title="执行打包"/>
</Steps>}> </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.Panel>
</Collapse> </Collapse>
@ -124,7 +124,7 @@ class Index extends React.Component {
<Steps.Step {...this.getStatus(item.id, 4)} title="发布后任务"/> <Steps.Step {...this.getStatus(item.id, 4)} title="发布后任务"/>
</Steps> </Steps>
</div>}> </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.Panel>
))} ))}
</Collapse> </Collapse>
@ -133,4 +133,4 @@ class Index extends React.Component {
} }
} }
export default Index export default Ext1Index

View File

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

View File

@ -10,12 +10,35 @@
padding: 0; padding: 0;
} }
.console { .ext1Console {
min-height: 40px; min-height: 40px;
max-height: 300px; max-height: 300px;
padding: 10px 15px; 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 { pre {
margin: 0; margin: 0;
} }

View File

@ -17,6 +17,12 @@ class Ext2Form extends React.Component {
} }
} }
componentDidMount() {
if (hostStore.records.length === 0) {
hostStore.fetchRecords()
}
}
handleSubmit = () => { handleSubmit = () => {
if (this.state.host_ids.length === 0) { if (this.state.host_ids.length === 0) {
return message.error('请至少选择一个要发布的目标主机') return message.error('请至少选择一个要发布的目标主机')
@ -24,11 +30,13 @@ class Ext2Form extends React.Component {
this.setState({loading: true}); this.setState({loading: true});
const formData = this.props.form.getFieldsValue(); const formData = this.props.form.getFieldsValue();
formData['id'] = store.record.id; formData['id'] = store.record.id;
formData['body'] = this.state.body; formData['app_id'] = store.record.app_id;
http.post('/api/exec/template/', formData) formData['extra'] = [formData['extra']];
formData['host_ids'] = this.state.host_ids;
http.post('/api/deploy/request/', formData)
.then(res => { .then(res => {
message.success('操作成功'); message.success('操作成功');
store.formVisible = false; store.ext2Visible = false;
store.fetchRecords() store.fetchRecords()
}, () => this.setState({loading: false})) }, () => this.setState({loading: false}))
}; };
@ -63,13 +71,18 @@ class Ext2Form extends React.Component {
<Input placeholder="请输入申请标题"/> <Input placeholder="请输入申请标题"/>
)} )}
</Form.Item> </Form.Item>
<Form.Item label="环境变量_SPUG_RELEASE" help="可以在自定义脚本中引用该变量,用于设置本次发布相关的动态变量,在脚本中通过 $_SPUG_RELEASE 来使用该值">
{getFieldDecorator('extra', {initialValue: info['extra']})(
<Input placeholder="请输入环境变量 _SPUG_RELEASE 的值"/>
)}
</Form.Item>
<Form.Item label="备注信息"> <Form.Item label="备注信息">
{getFieldDecorator('desc', {initialValue: info['desc']})( {getFieldDecorator('desc', {initialValue: info['desc']})(
<Input placeholder="请输入备注信息"/> <Input placeholder="请输入备注信息"/>
)} )}
</Form.Item> </Form.Item>
<Form.Item required label="发布目标主机"> <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)}> <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`)}) {lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)})
</Tag.CheckableTag> </Tag.CheckableTag>

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Button, Menu, Icon } from 'antd'; import { Modal, Button, Menu, Icon } from 'antd';
import store from './store'; import store from './store';
@ -45,6 +46,7 @@ class SelectApp extends React.Component {
render() { render() {
const {env_id} = this.state; const {env_id} = this.state;
const records = appStore.records.filter(x => String(x.env_id) === env_id);
return ( return (
<Modal <Modal
visible visible
@ -66,13 +68,15 @@ class SelectApp extends React.Component {
</div> </div>
<div className={styles.right}> <div className={styles.right}>
<div className={styles.title}>{lds.get(envStore.idMap, `${env_id}.name`)}</div> <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)}> <Button key={item.id} type="primary" className={styles.appBlock} onClick={() => this.handleClick(item)}>
<div style={{width: 135, overflow: 'hidden', textOverflow: 'ellipsis'}}> <div style={{width: 135, overflow: 'hidden', textOverflow: 'ellipsis'}}>
<Icon type={item.extend === '1' ? 'ordered-list' : 'build'} style={{marginRight: 10}}/>{item.name} <Icon type={item.extend === '1' ? 'ordered-list' : 'build'} style={{marginRight: 10}}/>{item.name}
</div> </div>
</Button> </Button>
))} ))}
{records.length === 0 &&
<div className={styles.tips}>该环境下还没有可发布的应用哦快去<Link to="/deploy/app">应用管理</Link></div>}
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@ -44,7 +44,7 @@ class ComTable extends React.Component {
} }
} else { } else {
return <React.Fragment> return <React.Fragment>
<Icon type="build"/> xxx <Icon type="build"/> {info.extra[0]}
</React.Fragment> </React.Fragment>
} }
} }
@ -81,7 +81,7 @@ class ComTable extends React.Component {
switch (info.status) { switch (info.status) {
case '-3': case '-3':
return <React.Fragment> return <React.Fragment>
<Link to={`/deploy/do/${info.id}`}>发布</Link> <Link to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Link>
<Divider type="vertical"/> <Divider type="vertical"/>
<LinkButton <LinkButton
disabled={info.type === '2'} disabled={info.type === '2'}
@ -106,10 +106,10 @@ class ComTable extends React.Component {
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton> <LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
<Divider type="vertical"/> <Divider type="vertical"/>
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton> <LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
</React.Fragment> </React.Fragment>;
case '1': case '1':
return <React.Fragment> return <React.Fragment>
<Link to={`/deploy/do/${info.id}`}>发布</Link> <Link to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Link>
<Divider type="vertical"/> <Divider type="vertical"/>
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton> <LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
</React.Fragment>; </React.Fragment>;

View File

@ -28,3 +28,8 @@
font-size: 18px; font-size: 18px;
margin-right: 20px; margin-right: 20px;
} }
.tips {
margin-top: 32px;
color: #888;
}

View File

@ -1,11 +1,13 @@
import { makeRoute } from "../../libs/router"; import { makeRoute } from "../../libs/router";
import app from './app'; import app from './app';
import request from './request'; import request from './request';
import doIndex from './do'; import doExt1Index from './do/Ext1Index';
import doExt2Index from './do/Ext2Index';
export default [ export default [
makeRoute('/app', app), makeRoute('/app', app),
makeRoute('/request', request), makeRoute('/request', request),
makeRoute('/do/:id', doIndex), makeRoute('/do/ext1/:id', doExt1Index),
makeRoute('/do/ext2/:id', doExt2Index),
] ]