mirror of https://github.com/openspug/spug
				
				
				
			A web update
							parent
							
								
									9bf22223c0
								
							
						
					
					
						commit
						9d9470557e
					
				|  | @ -10,6 +10,7 @@ export default [ | |||
|   { | ||||
|     icon: 'flag', title: '应用发布', child: [ | ||||
|       {title: '应用管理', path: '/deploy/app'}, | ||||
|       {title: '发布申请', path: '/deploy/request'}, | ||||
|     ] | ||||
|   }, | ||||
|   {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 app from './app'; | ||||
| import request from './request'; | ||||
| 
 | ||||
| 
 | ||||
| export default [ | ||||
|   makeRoute('/app', app), | ||||
|   makeRoute('/request', request), | ||||
| ] | ||||
|  | @ -5,6 +5,7 @@ class Store { | |||
|   @observable records = []; | ||||
|   @observable zones = []; | ||||
|   @observable record = {}; | ||||
|   @observable idMap = {}; | ||||
|   @observable isFetching = false; | ||||
|   @observable formVisible = false; | ||||
| 
 | ||||
|  | @ -17,6 +18,9 @@ class Store { | |||
|       .then(({hosts, zones}) => { | ||||
|         this.records = hosts; | ||||
|         this.zones = zones; | ||||
|         for (let item of hosts) { | ||||
|           this.idMap[item.id] = item | ||||
|         } | ||||
|       }) | ||||
|       .finally(() => this.isFetching = false) | ||||
|   }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 雷二猛
						雷二猛