upgrade deploy

pull/289/head
vapao 2021-02-07 23:52:46 +08:00
parent d076c51c3d
commit 4d86288ab5
9 changed files with 171 additions and 284 deletions

View File

@ -74,10 +74,7 @@ class DeployExtend1(models.Model, ModelMixin):
deploy = models.OneToOneField(Deploy, primary_key=True, on_delete=models.CASCADE) deploy = models.OneToOneField(Deploy, primary_key=True, on_delete=models.CASCADE)
git_repo = models.CharField(max_length=255) git_repo = models.CharField(max_length=255)
dst_dir = models.CharField(max_length=255) dst_dir = models.CharField(max_length=255)
dst_repo = models.CharField(max_length=255)
versions = models.IntegerField()
filter_rule = models.TextField() filter_rule = models.TextField()
custom_envs = models.TextField()
hook_pre_server = models.TextField(null=True) hook_pre_server = models.TextField(null=True)
hook_post_server = models.TextField(null=True) hook_post_server = models.TextField(null=True)
hook_pre_host = models.TextField(null=True) hook_pre_host = models.TextField(null=True)
@ -86,7 +83,6 @@ class DeployExtend1(models.Model, ModelMixin):
def to_dict(self, *args, **kwargs): def to_dict(self, *args, **kwargs):
tmp = super().to_dict(*args, **kwargs) tmp = super().to_dict(*args, **kwargs)
tmp['filter_rule'] = json.loads(self.filter_rule) tmp['filter_rule'] = json.loads(self.filter_rule)
tmp['custom_envs'] = '\n'.join(f'{k}={v}' for k, v in json.loads(self.custom_envs).items())
return tmp return tmp
def __repr__(self): def __repr__(self):

View File

@ -120,10 +120,7 @@ class DeployView(View):
extend_form, error = JsonParser( extend_form, error = JsonParser(
Argument('git_repo', handler=str.strip, help='请输入git仓库地址'), Argument('git_repo', handler=str.strip, help='请输入git仓库地址'),
Argument('dst_dir', handler=str.strip, help='请输入发布目标路径'), Argument('dst_dir', handler=str.strip, help='请输入发布目标路径'),
Argument('dst_repo', handler=str.strip, help='请输入目标仓库路径'),
Argument('versions', type=int, help='请输入保留历史版本数量'),
Argument('filter_rule', type=dict, help='参数错误'), Argument('filter_rule', type=dict, help='参数错误'),
Argument('custom_envs', handler=str.strip, required=False),
Argument('hook_pre_server', handler=str.strip, default=''), Argument('hook_pre_server', handler=str.strip, default=''),
Argument('hook_post_server', handler=str.strip, default=''), Argument('hook_post_server', handler=str.strip, default=''),
Argument('hook_pre_host', handler=str.strip, default=''), Argument('hook_pre_host', handler=str.strip, default=''),
@ -133,7 +130,6 @@ class DeployView(View):
return json_response(error=error) return json_response(error=error)
extend_form.dst_dir = extend_form.dst_dir.rstrip('/') extend_form.dst_dir = extend_form.dst_dir.rstrip('/')
extend_form.filter_rule = json.dumps(extend_form.filter_rule) extend_form.filter_rule = json.dumps(extend_form.filter_rule)
extend_form.custom_envs = json.dumps(parse_envs(extend_form.custom_envs))
if form.id: if form.id:
extend = DeployExtend1.objects.filter(deploy_id=form.id).first() extend = DeployExtend1.objects.filter(deploy_id=form.id).first()
if extend.git_repo != extend_form.git_repo: if extend.git_repo != extend_form.git_repo:

View File

@ -19,8 +19,7 @@ class AddSelect extends React.Component {
git_type: 'branch', git_type: 'branch',
is_audit: false, is_audit: false,
rst_notify: {mode: '0'}, rst_notify: {mode: '0'},
versions: 10, host_ids: [],
host_ids: [undefined],
filter_rule: {type: 'contain', data: ''} filter_rule: {type: 'contain', data: ''}
} }
}; };

View File

@ -23,15 +23,15 @@ export default observer(function Ext1From() {
return ( return (
<Modal <Modal
visible visible
width={900} width={800}
maskClosable={false} maskClosable={false}
title={title} title={title}
onCancel={() => store.ext1Visible = false} onCancel={() => store.ext1Visible = false}
footer={null}> footer={null}>
<Steps current={store.page} className={styles.steps}> <Steps current={store.page} className={styles.steps}>
<Steps.Step key={0} title="基本配置"/> <Steps.Step key={0} title="基本配置"/>
<Steps.Step key={1} title="发布主机"/> <Steps.Step key={1} title="构建配置"/>
<Steps.Step key={2} title="任务配置"/> <Steps.Step key={2} title="发布配置"/>
</Steps> </Steps>
{store.page === 0 && <Setup1/>} {store.page === 0 && <Setup1/>}
{store.page === 1 && <Setup2/>} {store.page === 1 && <Setup2/>}

View File

@ -8,6 +8,7 @@ import { observer } from 'mobx-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Switch, Form, Input, Select, Button } from 'antd'; import { Switch, Form, Input, Select, Button } from 'antd';
import envStore from 'pages/config/environment/store'; import envStore from 'pages/config/environment/store';
import Selector from 'pages/host/Selector';
import store from './store'; import store from './store';
export default observer(function Ext1Setup1() { export default observer(function Ext1Setup1() {
@ -41,6 +42,10 @@ export default observer(function Ext1Setup1() {
<Link disabled={store.isReadOnly} to="/config/environment">新建环境</Link> <Link disabled={store.isReadOnly} to="/config/environment">新建环境</Link>
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item required label="目标主机">
{info.host_ids.length > 0 && `已选择 ${info.host_ids.length}`}
<Button type="link" onClick={() => store.selectorVisible = true}>选择主机</Button>
</Form.Item>
<Form.Item required label="Git仓库地址"> <Form.Item required label="Git仓库地址">
<Input disabled={store.isReadOnly} value={info['git_repo']} onChange={e => info['git_repo'] = e.target.value} <Input disabled={store.isReadOnly} value={info['git_repo']} onChange={e => info['git_repo'] = e.target.value}
placeholder="请输入Git仓库地址"/> placeholder="请输入Git仓库地址"/>
@ -60,7 +65,8 @@ export default observer(function Ext1Setup1() {
</span>}> </span>}>
<Input <Input
addonBefore={( addonBefore={(
<Select disabled={store.isReadOnly} <Select
disabled={store.isReadOnly}
value={info['rst_notify']['mode']} style={{width: 100}} value={info['rst_notify']['mode']} style={{width: 100}}
onChange={v => info['rst_notify']['mode'] = v}> onChange={v => info['rst_notify']['mode'] = v}>
<Select.Option value="0">关闭</Select.Option> <Select.Option value="0">关闭</Select.Option>
@ -77,9 +83,14 @@ export default observer(function Ext1Setup1() {
<Form.Item wrapperCol={{span: 14, offset: 6}}> <Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button <Button
type="primary" type="primary"
disabled={!(info.env_id && info['git_repo'])} disabled={!(info.env_id && info.git_repo && info.host_ids.length)}
onClick={() => store.page += 1}>下一步</Button> onClick={() => store.page += 1}>下一步</Button>
</Form.Item> </Form.Item>
<Selector
visible={store.selectorVisible}
selectedRowKeys={[...info.host_ids]}
onCancel={() => store.selectorVisible = false}
onOk={(_, ids) => info.host_ids = ids}/>
</Form> </Form>
) )
}) })

View File

@ -5,85 +5,99 @@
*/ */
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { Form, Input, Select, Button, message } from "antd"; import { Form, Radio, Button, Tooltip } from "antd";
import { hasHostPermission } from 'libs'; import { cleanCommand } from 'libs';
import Editor from 'react-ace';
import 'ace-builds/src-noconflict/mode-text';
import 'ace-builds/src-noconflict/mode-sh';
import 'ace-builds/src-noconflict/theme-tomorrow';
import store from './store'; import store from './store';
import hostStore from 'pages/host/store';
import styles from './index.module.css';
@observer export default observer(function () {
class Ext1Setup2 extends React.Component { const Tips = (
componentDidMount() { <a
if (hostStore.records.length === 0) { target="_blank"
hostStore.fetchRecords() rel="noopener noreferrer"
} href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">内置全局变量</a>
} )
checkStatus = () => { function handleNext() {
const info = store.deploy;
return info['dst_dir'] && info['dst_repo'] && info['versions'] && info['host_ids'].filter(x => x).length > 0
};
handleNext = () => {
const {dst_dir, dst_repo} = store.deploy;
if (dst_repo.includes(dst_dir.replace(/\/*$/, '/'))) {
message.error('仓库目录不能位于发布部署目录内')
} else {
store.page += 1 store.page += 1
} }
};
render() { const FilterHead = (
<div style={{width: 512, display: 'flex', justifyContent: 'space-between'}}>
<span>
文件过滤规则 &nbsp;
<Tooltip title="请输入相对于项目根目录的文件路径,根据包含或排除规则进行打包。">
<QuestionCircleOutlined style={{color: 'rgba(0, 0, 0, 0.45)'}}/>
</Tooltip>
</span>
<Radio.Group
size="small"
value={store.deploy.filter_rule.type}
onChange={e => store.deploy.filter_rule.type = e.target.value}>
<Radio.Button value="contain">
<Tooltip title="仅打包匹配到的文件或目录,如果内容为空则打包所有。">包含</Tooltip>
</Radio.Button>
<Radio.Button value="exclude">
<Tooltip title="打包时排除匹配到的文件或目录,如果内容为空则不排除任何文件。">排除</Tooltip>
</Radio.Button>
</Radio.Group>
</div>
)
const info = store.deploy; const info = store.deploy;
return ( return (
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}> <Form layout="vertical" style={{padding: '0 120px'}}>
<Form.Item required label="目标主机部署路径" help="目标主机的应用根目录,例如:/var/www/html"> <Form.Item label={FilterHead} tooltip="xxx">
<Input disabled={store.isReadOnly} value={info['dst_dir']} onChange={e => info['dst_dir'] = e.target.value} <Editor
placeholder="请输入目标主机部署路径"/> readOnly={store.isReadOnly}
mode="text"
theme="tomorrow"
width="100%"
height="80px"
placeholder="每行一条规则"
value={info['filter_rule']['data']}
onChange={v => info['filter_rule']['data'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item> </Form.Item>
<Form.Item required label="目标主机仓库路径" help="此目录用于存储应用的历史版本,例如:/data/spug/repos"> <Form.Item
<Input disabled={store.isReadOnly} value={info['dst_repo']} onChange={e => info['dst_repo'] = e.target.value} placeholder="请输入目标主机仓库路径"/> label="代码检出前执行"
tooltip="在运行 Spug 的服务器(或容器)上执行,当前目录为仓库源代码目录,可以执行任意自定义命令。"
help={<span>可使用 {Tips}请避免在此修改已跟踪的文件防止在检出代码时失败</span>}>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height="120px"
placeholder="输入要执行的命令"
value={info['hook_pre_server']}
onChange={v => info['hook_pre_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item> </Form.Item>
<Form.Item required label="保留历史版本数量" help="早于指定数量的历史版本会被删除,以释放空间"> <Form.Item
<Input disabled={store.isReadOnly} value={info['versions']} onChange={e => info['versions'] = e.target.value} placeholder="请输入保留历史版本数量"/> label="代码检出后执行"
</Form.Item> style={{marginTop: 12, marginBottom: 24}}
<Form.Item required label="发布目标主机"> tooltip="在运行 Spug 的服务器(或容器)上执行,当前目录为检出后的源代码目录,可执行任意自定义命令。"
{info['host_ids'].map((id, index) => ( help={<span>可使用 {Tips}大多数情况下在此进行构建操作</span>}>
<React.Fragment key={index}> <Editor
<Select readOnly={store.isReadOnly}
value={id} mode="sh"
showSearch theme="tomorrow"
placeholder="请选择" width="100%"
disabled={store.isReadOnly} height="120px"
style={{width: '80%', marginRight: 10, marginBottom: 12}} placeholder="输入要执行的命令"
optionFilterProp="children" value={info['hook_post_server']}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0} onChange={v => info['hook_post_server'] = cleanCommand(v)}
onChange={v => store.editHost(index, v)}> style={{border: '1px solid #e8e8e8'}}/>
{hostStore.records.filter(x => hasHostPermission(x.id)).map(item => (
<Select.Option key={item.id} value={item.id} disabled={info['host_ids'].includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option>
))}
</Select>
{!store.isReadOnly && info['host_ids'].length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delHost(index)} />
)}
</React.Fragment>
))}
</Form.Item> </Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}> <Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={store.isReadOnly} type="dashed" style={{width: '80%'}} onClick={store.addHost}> <Button type="primary" onClick={handleNext}>下一步</Button>
<PlusOutlined />添加目标主机
</Button>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={!this.checkStatus()} type="primary" onClick={this.handleNext}>下一步</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button> <Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item> </Form.Item>
</Form> </Form>
) )
} })
}
export default Ext1Setup2

View File

@ -3,210 +3,79 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React from 'react'; import React, {useState} from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { GitlabOutlined, InfoCircleOutlined, SettingOutlined, SwapOutlined } from '@ant-design/icons'; import { Form, Button, Input, message } from 'antd';
import { Form, Row, Col, Button, Radio, Tooltip, message } from 'antd';
import { LinkButton } from 'components';
import Editor from 'react-ace'; import Editor from 'react-ace';
import 'ace-builds/src-noconflict/mode-text'; import 'ace-builds/src-noconflict/mode-text';
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';
import { http, cleanCommand } from 'libs';
import store from './store'; import store from './store';
import http from 'libs/http';
import styles from './index.module.css';
import { cleanCommand } from "../../../libs";
@observer export default observer(function () {
class Ext1Setup3 extends React.Component { const [loading, setLoading] = useState(false);
constructor(props) { const Tips = (
super(props); <a
this.helpMap = { target="_blank"
'2': <span> rel="noopener noreferrer"
Spug 内置了一些全局变量这些变量可以直接使用请参考官方文档 href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">内置全局变量</a>
<a target="_blank" rel="noopener noreferrer" )
href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">全局变量</a>
</span>,
'3': '在部署 Spug 的服务器上运行,可以执行任意自定义命令。',
'4': '在部署 Spug 的服务器上运行,当前目录为检出后待发布的源代码目录,可执行任意自定义命令。',
'5': '在发布的目标主机上运行,当前目录为目标主机上待发布的源代码目录,可执行任意自定义命令。',
'6': '在发布的目标主机上运行,当前目录为已发布的应用目录,可执行任意自定义命令。'
};
this.state = {
loading: false,
full: ''
}
}
handleSubmit = () => { function handleSubmit() {
this.setState({loading: true}); setLoading(true);
const info = store.deploy; const info = store.deploy;
info['app_id'] = store.app_id; info['app_id'] = store.app_id;
info['extend'] = '1'; info['extend'] = '1';
info['host_ids'] = info['host_ids'].filter(x => x);
http.post('/api/app/deploy/', info) http.post('/api/app/deploy/', info)
.then(() => { .then(() => {
message.success('保存成功'); message.success('保存成功');
store.loadDeploys(store.app_id); store.loadDeploys(store.app_id);
store.ext1Visible = false store.ext1Visible = false
}, () => this.setState({loading: false})) }, () => setLoading(false))
};
handleFullscreen = (id) => {
if (this.state.full) {
this.setState({full: ''})
} else {
this.setState({full: id})
}
} }
FilterLabel = (props) => (
<div style={{display: 'flex', alignItems: 'center', height: 40}}>
<div>文件过滤 :</div>
<Radio.Group
disabled={store.isReadOnly}
style={{marginLeft: 20, float: 'left'}}
value={props.type}
onChange={e => store.deploy['filter_rule']['type'] = e.target.value}>
<Radio value="contain">包含
<Tooltip title="请输入相对于项目根目录的文件路径,仅将匹配到文件传输至要发布的目标主机。">
<InfoCircleOutlined style={{color: '#515151', marginLeft: 8}}/>
</Tooltip>
</Radio>
<Radio value="exclude">排除
<Tooltip title="支持模糊匹配,如果路径以 / 开头则基于项目根目录匹配,匹配到文件将不会被传输。">
<InfoCircleOutlined style={{color: '#515151', marginLeft: 8}}/>
</Tooltip>
</Radio>
</Radio.Group>
<div style={{flex: 1, textAlign: 'right'}}>
<LinkButton onClick={() => this.handleFullscreen('1')}>{this.state.full ? '退出全屏' : '全屏'}</LinkButton>
</div>
</div>
);
NormalLabel = (props) => (
<div style={{display: 'flex', alignItems: 'center', height: 40}}>
<div style={{marginRight: 8}}>{props.title} :</div>
<Tooltip title={this.helpMap[props.id]}>
<InfoCircleOutlined style={{color: '#515151'}}/>
</Tooltip>
<div style={{flex: 1, textAlign: 'right'}}>
<LinkButton onClick={() => this.handleFullscreen(props.id)}>{this.state.full ? '退出全屏' : '全屏'}</LinkButton>
</div>
</div>
);
render() {
const info = store.deploy; const info = store.deploy;
const {full} = this.state;
return ( return (
<React.Fragment> <Form layout="vertical" style={{padding: '0 120px'}}>
<Row> <Form.Item required label="部署目标路径" tooltip="应用最终在主机上部署路径,构建的结果将会放置于该路径下。">
<Col span={11}> <Input value={info['dst_dir']} onChange={e => info['dst_dir'] = e.target.value} placeholder="请输入部署目标路径" />
<div className={full === '1' ? styles.fullScreen : null} style={{marginBottom: 24}}> </Form.Item>
<this.FilterLabel type={info['filter_rule']['type']}/> <Form.Item
<Editor label="应用发布前执行"
readOnly={store.isReadOnly} tooltip="在发布的目标主机上运行,当前目录为目标主机上待发布的源代码目录,可执行任意自定义命令。"
mode="text" help={<span>可使用 {Tips}此时还未进行文件变更可进行一些发布前置操作</span>}>
theme="tomorrow"
width="100%"
height={full === '1' ? '100vh' : '100px'}
placeholder="每行一条规则"
value={info['filter_rule']['data']}
onChange={v => info['filter_rule']['data'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '3' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="代码检出前执行" id="3"/>
<Editor <Editor
readOnly={store.isReadOnly} readOnly={store.isReadOnly}
mode="sh" mode="sh"
theme="tomorrow" theme="tomorrow"
width="100%" width="100%"
height={full === '3' ? '100vh' : '100px'} height="150px"
placeholder="输入要执行的命令"
value={info['hook_pre_server']}
onChange={v => info['hook_pre_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '5' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="应用发布前执行" id="5"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '5' ? '100vh' : '100px'}
placeholder="输入要执行的命令" placeholder="输入要执行的命令"
value={info['hook_pre_host']} value={info['hook_pre_host']}
onChange={v => info['hook_pre_host'] = cleanCommand(v)} onChange={v => info['hook_pre_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/> style={{border: '1px solid #e8e8e8'}}/>
</div> </Form.Item>
</Col> <Form.Item
<Col span={2}> label="应用发布后执行"
<div className={styles.deployBlock} style={{marginTop: 39}}> style={{marginTop: 12, marginBottom: 24}}
<SettingOutlined style={{fontSize: 32}}/> tooltip="在发布的目标主机上运行,当前目录为已发布的应用目录,可执行任意自定义命令。"
<span style={{fontSize: 12, marginTop: 5}}>基础设置</span> help={<span>可使用 {Tips}可以在发布后进行重启服务等操作</span>}>
</div>
<div className={styles.deployBlock}>
<GitlabOutlined style={{fontSize: 32}}/>
<span style={{fontSize: 12, marginTop: 5}}>检出代码</span>
</div>
<div className={styles.deployBlock}>
<SwapOutlined style={{fontSize: 32}}/>
<span style={{fontSize: 12, marginTop: 5}}>版本切换</span>
</div>
</Col>
<Col span={11}>
<div className={full === '2' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="自定义全局变量" id="2"/>
<Editor
readOnly={store.isReadOnly}
mode="text"
theme="tomorrow"
width="100%"
height={full === '2' ? '100vh' : '100px'}
placeholder="每行一个例如HOME=/data/spug"
value={info['custom_envs']}
onChange={v => info['custom_envs'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '4' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="代码检出后执行" id="4"/>
<Editor <Editor
readOnly={store.isReadOnly} readOnly={store.isReadOnly}
mode="sh" mode="sh"
theme="tomorrow" theme="tomorrow"
width="100%" width="100%"
height={full === '4' ? '100vh' : '100px'} height="150px"
placeholder="输入要执行的命令"
value={info['hook_post_server']}
onChange={v => info['hook_post_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '6' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="应用发布后执行" id="6"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '6' ? '100vh' : '100px'}
placeholder="输入要执行的命令" placeholder="输入要执行的命令"
value={info['hook_post_host']} value={info['hook_post_host']}
onChange={v => info['hook_post_host'] = cleanCommand(v)} onChange={v => info['hook_post_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/> style={{border: '1px solid #e8e8e8'}}/>
</div> </Form.Item>
</Col>
</Row>
<Form.Item wrapperCol={{span: 14, offset: 6}}> <Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={store.isReadOnly} type="primary" onClick={this.handleSubmit}>提交</Button> <Button disabled={store.isReadOnly} loading={loading} type="primary" onClick={handleSubmit}>提交</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button> <Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item> </Form.Item>
</React.Fragment> </Form>
) )
} })
}
export default Ext1Setup3

View File

@ -91,7 +91,7 @@ class ComTable extends React.Component {
}; };
expandedRowRender = (record) => { expandedRowRender = (record) => {
if (record['deploys'] === undefined) { if (!record.isLoaded) {
store.loadDeploys(record.id) store.loadDeploys(record.id)
} }

View File

@ -18,6 +18,7 @@ class Store {
@observable addVisible = false; @observable addVisible = false;
@observable ext1Visible = false; @observable ext1Visible = false;
@observable ext2Visible = false; @observable ext2Visible = false;
@observable selectorVisible = false;
@observable f_name; @observable f_name;
@observable f_desc; @observable f_desc;
@ -47,6 +48,7 @@ class Store {
}; };
loadDeploys = (app_id) => { loadDeploys = (app_id) => {
this.records[`a${app_id}`].isLoaded = true;
return http.get('/api/app/deploy/', {params: {app_id}}) return http.get('/api/app/deploy/', {params: {app_id}})
.then(res => this.records[`a${app_id}`]['deploys'] = res) .then(res => this.records[`a${app_id}`]['deploys'] = res)
}; };