mirror of https://github.com/openspug/spug
A web update
parent
9bf22223c0
commit
9d9470557e
|
@ -10,6 +10,7 @@ export default [
|
||||||
{
|
{
|
||||||
icon: 'flag', title: '应用发布', child: [
|
icon: 'flag', title: '应用发布', child: [
|
||||||
{title: '应用管理', path: '/deploy/app'},
|
{title: '应用管理', path: '/deploy/app'},
|
||||||
|
{title: '发布申请', path: '/deploy/request'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{icon: 'schedule', title: '任务计划', path: '/schedule'},
|
{icon: 'schedule', title: '任务计划', path: '/schedule'},
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Form, Input, Select, Col, Button, Tag, message } from 'antd';
|
||||||
|
import hostStore from 'pages/host/store';
|
||||||
|
import http from 'libs/http';
|
||||||
|
import store from './store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class Ext1Form extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
type: null,
|
||||||
|
host_ids: store.record['host_ids'].concat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
if (this.state.host_ids.length === 0) {
|
||||||
|
return message.error('请至少选择一个要发布的目标主机')
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
.then(res => {
|
||||||
|
message.success('操作成功');
|
||||||
|
store.formVisible = false;
|
||||||
|
store.fetchRecords()
|
||||||
|
}, () => this.setState({loading: false}))
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (id, v) => {
|
||||||
|
const host_ids = this.state.host_ids;
|
||||||
|
const index = host_ids.indexOf(id);
|
||||||
|
if (index === -1) {
|
||||||
|
this.setState({host_ids: [id, ...host_ids]})
|
||||||
|
} else {
|
||||||
|
host_ids.splice(index, 1);
|
||||||
|
this.setState({host_ids})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const info = store.record;
|
||||||
|
const {host_ids} = this.state;
|
||||||
|
const {getFieldDecorator} = this.props.form;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
title="新建发布申请"
|
||||||
|
onCancel={() => store.ext1Visible = false}
|
||||||
|
confirmLoading={this.state.loading}
|
||||||
|
onOk={this.handleSubmit}>
|
||||||
|
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
|
<Form.Item required label="申请标题">
|
||||||
|
{getFieldDecorator('name', {initialValue: info['name']})(
|
||||||
|
<Input placeholder="请输入申请标题"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required label={`选择${info['git_type'] === 'branch' ? '分支' : '标签/版本'}`}>
|
||||||
|
<Col span={19}>
|
||||||
|
{getFieldDecorator('name', {initialValue: info['name']})(
|
||||||
|
<Select placeholder="请选择">
|
||||||
|
{store.types.map(item => (
|
||||||
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col span={4} offset={1}>
|
||||||
|
<Button type="link" icon="sync" onClick={this.handleAddZone}>刷新</Button>
|
||||||
|
</Col>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注信息">
|
||||||
|
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
||||||
|
<Input placeholder="请输入备注信息"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required label="发布目标主机">
|
||||||
|
{info['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>
|
||||||
|
))}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form.create()(Ext1Form)
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Form, Input, Select, Col, Button, message } from 'antd';
|
||||||
|
import { ACEditor } from 'components';
|
||||||
|
import http from 'libs/http';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class ComForm extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
type: null,
|
||||||
|
body: store.record['body'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
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)
|
||||||
|
.then(res => {
|
||||||
|
message.success('操作成功');
|
||||||
|
store.formVisible = false;
|
||||||
|
store.fetchRecords()
|
||||||
|
}, () => this.setState({loading: false}))
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAddZone = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
icon: 'exclamation-circle',
|
||||||
|
title: '添加模板类型',
|
||||||
|
content: this.addZoneForm,
|
||||||
|
onOk: () => {
|
||||||
|
if (this.state.type) {
|
||||||
|
store.types.push(this.state.type);
|
||||||
|
this.props.form.setFieldsValue({'type': this.state.type})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
addZoneForm = (
|
||||||
|
<Form>
|
||||||
|
<Form.Item required label="模板类型">
|
||||||
|
<Input onChange={val => this.setState({type: val.target.value})}/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const info = store.record;
|
||||||
|
const {getFieldDecorator} = this.props.form;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
title={store.record.id ? '编辑模板' : '新建模板'}
|
||||||
|
onCancel={() => store.formVisible = false}
|
||||||
|
confirmLoading={this.state.loading}
|
||||||
|
onOk={this.handleSubmit}>
|
||||||
|
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
|
<Form.Item required label="模板类型">
|
||||||
|
<Col span={16}>
|
||||||
|
{getFieldDecorator('type', {initialValue: info['type']})(
|
||||||
|
<Select placeholder="请选择模板类型">
|
||||||
|
{store.types.map(item => (
|
||||||
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col span={6} offset={2}>
|
||||||
|
<Button type="link" onClick={this.handleAddZone}>添加类型</Button>
|
||||||
|
</Col>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required label="模板名称">
|
||||||
|
{getFieldDecorator('name', {initialValue: info['name']})(
|
||||||
|
<Input placeholder="请输入模板名称"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required label="模板内容">
|
||||||
|
<ACEditor
|
||||||
|
mode="sh"
|
||||||
|
value={this.state.body}
|
||||||
|
onChange={val => this.setState({body: val})}
|
||||||
|
height="300px"/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注信息">
|
||||||
|
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
||||||
|
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form.create()(ComForm)
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Button, Menu, Icon } from 'antd';
|
||||||
|
import store from './store';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
import envStore from 'pages/config/environment/store';
|
||||||
|
import hostStore from 'pages/host/store';
|
||||||
|
import appStore from '../app/store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class SelectApp extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
env_id: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (envStore.records.length === 0) {
|
||||||
|
envStore.fetchRecords().then(this._initEnv)
|
||||||
|
} else {
|
||||||
|
this._initEnv()
|
||||||
|
}
|
||||||
|
if (appStore.records.length === 0) {
|
||||||
|
appStore.fetchRecords()
|
||||||
|
}
|
||||||
|
if (hostStore.records.length === 0) {
|
||||||
|
hostStore.fetchRecords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initEnv = () => {
|
||||||
|
if (envStore.records.length) {
|
||||||
|
this.setState({env_id: envStore.records[0].id})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = (app) => {
|
||||||
|
store.record = app;
|
||||||
|
if (app.extend === '1') {
|
||||||
|
store.ext1Visible = true
|
||||||
|
} else {
|
||||||
|
store.ext2Visible = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {env_id} = this.state;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={800}
|
||||||
|
maskClosable={false}
|
||||||
|
title="选择应用"
|
||||||
|
bodyStyle={{padding: 0}}
|
||||||
|
onCancel={() => store.addVisible = false}
|
||||||
|
footer={null}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.left}>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
selectedKeys={[String(env_id)]}
|
||||||
|
style={{border: 'none'}}
|
||||||
|
onSelect={({selectedKeys}) => this.setState({env_id: selectedKeys[0]})}>
|
||||||
|
{envStore.records.map(item => <Menu.Item key={item.id}>{item.name}</Menu.Item>)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div className={styles.right}>
|
||||||
|
<div className={styles.title}>{lds.get(envStore.idMap, `${env_id}.name`)}</div>
|
||||||
|
{appStore.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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectApp
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Table, Divider, Modal, message } from 'antd';
|
||||||
|
import http from 'libs/http';
|
||||||
|
import store from './store';
|
||||||
|
import { LinkButton } from "components";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class ComTable extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
store.fetchRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = [{
|
||||||
|
title: '序号',
|
||||||
|
key: 'series',
|
||||||
|
render: (_, __, index) => index + 1,
|
||||||
|
width: 80,
|
||||||
|
}, {
|
||||||
|
title: '模版名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
}, {
|
||||||
|
title: '模版类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
}, {
|
||||||
|
title: '模版内容',
|
||||||
|
render: text => text.body,
|
||||||
|
ellipsis: true
|
||||||
|
}, {
|
||||||
|
title: '描述信息',
|
||||||
|
dataIndex: 'desc',
|
||||||
|
ellipsis: true
|
||||||
|
}, {
|
||||||
|
title: '操作',
|
||||||
|
render: info => (
|
||||||
|
<span>
|
||||||
|
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
|
||||||
|
<Divider type="vertical"/>
|
||||||
|
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}];
|
||||||
|
|
||||||
|
handleDelete = (text) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '删除确认',
|
||||||
|
content: `确定要删除【${text['name']}】?`,
|
||||||
|
onOk: () => {
|
||||||
|
return http.delete('/api/exec/template/', {params: {id: text.id}})
|
||||||
|
.then(() => {
|
||||||
|
message.success('删除成功');
|
||||||
|
store.fetchRecords()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let data = store.records;
|
||||||
|
if (store.f_name) {
|
||||||
|
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
|
||||||
|
}
|
||||||
|
if (store.f_type) {
|
||||||
|
data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase()))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Table rowKey="id" loading={store.isFetching} dataSource={data} columns={this.columns}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComTable
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Card, Input, Select, Button } from 'antd';
|
||||||
|
import { SearchForm } from 'components';
|
||||||
|
import SelectApp from './SelectApp';
|
||||||
|
import Ext1Form from './Ext1Form';
|
||||||
|
import ComTable from './Table';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
export default observer(function () {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SearchForm>
|
||||||
|
<SearchForm.Item span={8} title="应用名称">
|
||||||
|
<Select allowClear onChange={v => store.f_type = v} placeholder="请选择">
|
||||||
|
{store.types.map(item => (
|
||||||
|
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</SearchForm.Item>
|
||||||
|
<SearchForm.Item span={8} title="模版名称">
|
||||||
|
<Input allowClear onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
|
||||||
|
</SearchForm.Item>
|
||||||
|
<SearchForm.Item span={8}>
|
||||||
|
<Button type="primary" icon="sync" onClick={store.fetchRecords}>刷新</Button>
|
||||||
|
</SearchForm.Item>
|
||||||
|
</SearchForm>
|
||||||
|
<div style={{marginBottom: 16}}>
|
||||||
|
<Button type="primary" icon="plus" onClick={() => store.addVisible = true}>新建发布申请</Button>
|
||||||
|
</div>
|
||||||
|
<ComTable/>
|
||||||
|
{store.addVisible && <SelectApp />}
|
||||||
|
{store.ext1Visible && <Ext1Form />}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,30 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 16px 0;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
flex: 2;
|
||||||
|
border-right: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
flex: 7;
|
||||||
|
padding: 8px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appBlock {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 165px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import http from 'libs/http';
|
||||||
|
|
||||||
|
class Store {
|
||||||
|
@observable records = [];
|
||||||
|
@observable types = [];
|
||||||
|
@observable record = {};
|
||||||
|
@observable isFetching = false;
|
||||||
|
@observable addVisible = false;
|
||||||
|
@observable ext1Visible = false;
|
||||||
|
@observable ext2Visible = false;
|
||||||
|
|
||||||
|
@observable f_name;
|
||||||
|
@observable f_type;
|
||||||
|
|
||||||
|
fetchRecords = () => {
|
||||||
|
this.isFetching = true;
|
||||||
|
http.get('/api/app/')
|
||||||
|
.then(res => this.records = res)
|
||||||
|
.finally(() => this.isFetching = false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Store()
|
|
@ -1,7 +1,9 @@
|
||||||
import { makeRoute } from "../../libs/router";
|
import { makeRoute } from "../../libs/router";
|
||||||
import app from './app';
|
import app from './app';
|
||||||
|
import request from './request';
|
||||||
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
makeRoute('/app', app),
|
makeRoute('/app', app),
|
||||||
|
makeRoute('/request', request),
|
||||||
]
|
]
|
|
@ -5,6 +5,7 @@ class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable zones = [];
|
@observable zones = [];
|
||||||
@observable record = {};
|
@observable record = {};
|
||||||
|
@observable idMap = {};
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable formVisible = false;
|
@observable formVisible = false;
|
||||||
|
|
||||||
|
@ -17,6 +18,9 @@ class Store {
|
||||||
.then(({hosts, zones}) => {
|
.then(({hosts, zones}) => {
|
||||||
this.records = hosts;
|
this.records = hosts;
|
||||||
this.zones = zones;
|
this.zones = zones;
|
||||||
|
for (let item of hosts) {
|
||||||
|
this.idMap[item.id] = item
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => this.isFetching = false)
|
.finally(() => this.isFetching = false)
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue