mirror of https://github.com/openspug/spug
优化配置中心移除公共/私有配置概念
parent
b5e83bdd63
commit
36f9f62490
|
@ -48,9 +48,8 @@ class Config(models.Model, ModelMixin):
|
||||||
env = models.ForeignKey(Environment, on_delete=models.PROTECT)
|
env = models.ForeignKey(Environment, on_delete=models.PROTECT)
|
||||||
value = models.TextField(null=True)
|
value = models.TextField(null=True)
|
||||||
desc = models.CharField(max_length=255, null=True)
|
desc = models.CharField(max_length=255, null=True)
|
||||||
is_public = models.BooleanField(default=False)
|
|
||||||
updated_at = models.CharField(max_length=20)
|
updated_at = models.CharField(max_length=20)
|
||||||
updated_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
updated_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Config {self.key!r}>'
|
return f'<Config {self.key!r}>'
|
||||||
|
@ -72,11 +71,10 @@ class ConfigHistory(models.Model, ModelMixin):
|
||||||
env_id = models.IntegerField()
|
env_id = models.IntegerField()
|
||||||
value = models.TextField(null=True)
|
value = models.TextField(null=True)
|
||||||
desc = models.CharField(max_length=255, null=True)
|
desc = models.CharField(max_length=255, null=True)
|
||||||
is_public = models.BooleanField()
|
|
||||||
old_value = models.TextField(null=True)
|
old_value = models.TextField(null=True)
|
||||||
action = models.CharField(max_length=2, choices=ACTIONS)
|
action = models.CharField(max_length=2, choices=ACTIONS)
|
||||||
updated_at = models.CharField(max_length=20)
|
updated_at = models.CharField(max_length=20)
|
||||||
updated_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
updated_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<ConfigHistory {self.key!r}>'
|
return f'<ConfigHistory {self.key!r}>'
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
# 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.
|
||||||
from apps.config.models import Config, Service
|
from apps.config.models import Config, Service, Environment, ConfigHistory
|
||||||
from apps.app.models import App
|
from apps.app.models import App
|
||||||
|
from libs.utils import SpugError, human_datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ def compose_configs(app, env_id, no_prefix=False):
|
||||||
app_ids = json.loads(app.rel_apps)
|
app_ids = json.loads(app.rel_apps)
|
||||||
if app_ids:
|
if app_ids:
|
||||||
id_key_map = {x.id: x.key for x in App.objects.filter(id__in=app_ids)}
|
id_key_map = {x.id: x.key for x in App.objects.filter(id__in=app_ids)}
|
||||||
for item in Config.objects.filter(type='app', o_id__in=app_ids, env_id=env_id, is_public=True) \
|
for item in Config.objects.filter(type='app', o_id__in=app_ids, env_id=env_id) \
|
||||||
.only('key', 'value'):
|
.only('key', 'value'):
|
||||||
key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
|
key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
|
||||||
configs[key] = item.value
|
configs[key] = item.value
|
||||||
|
@ -32,3 +33,44 @@ def compose_configs(app, env_id, no_prefix=False):
|
||||||
key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
|
key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
|
||||||
configs[key] = item.value
|
configs[key] = item.value
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
|
|
||||||
|
def update_config_by_var(val):
|
||||||
|
try:
|
||||||
|
keys, value = val.split('=', 1)
|
||||||
|
key, env_key, var = keys.split(':')
|
||||||
|
except ValueError:
|
||||||
|
raise SpugError('通过SPUG_SET动态更新配置出错,请遵循export SPUG_SET=应用/服务标识符:环境标识符:变量名=变量值')
|
||||||
|
env = Environment.objects.filter(key=env_key).first()
|
||||||
|
if not env:
|
||||||
|
raise SpugError(f'通过SPUG_SET动态更新配置出错,未找到环境标识符{env_key}')
|
||||||
|
app = App.objects.filter(key=key).first()
|
||||||
|
if app:
|
||||||
|
query = dict(key=var, type='app', o_id=app.id, env_id=env.id)
|
||||||
|
else:
|
||||||
|
service = Service.objects.filter(key=key).first()
|
||||||
|
if not service:
|
||||||
|
raise SpugError(f'通过SPUG_SET动态更新配置出错,未找到应用或服务标识符{key}')
|
||||||
|
query = dict(key=var, type='src', o_id=service.id, env_id=env.id)
|
||||||
|
|
||||||
|
config = Config.objects.filter(**query).first()
|
||||||
|
if config:
|
||||||
|
if config.value != value:
|
||||||
|
old_value = config.value
|
||||||
|
config.value = value
|
||||||
|
config.updated_at = human_datetime()
|
||||||
|
config.updated_by_id = None
|
||||||
|
config.desc = '通过SPUG_SET动态更新'
|
||||||
|
ConfigHistory.objects.create(
|
||||||
|
action='2',
|
||||||
|
old_value=old_value,
|
||||||
|
**config.to_dict(excludes=('id',)))
|
||||||
|
else:
|
||||||
|
config = Config.objects.create(
|
||||||
|
value=value,
|
||||||
|
desc='通过SPUG_SET动态创建',
|
||||||
|
updated_at=human_datetime(),
|
||||||
|
updated_by_id=None,
|
||||||
|
**query
|
||||||
|
)
|
||||||
|
ConfigHistory.objects.create(action='1', **config.to_dict(excludes=('id',)))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Modal, Form, Transfer, message, Tabs, Alert } from 'antd';
|
import { Modal, Form, Transfer, message, Tabs } from 'antd';
|
||||||
import { http, hasPermission } from 'libs';
|
import { http, hasPermission } from 'libs';
|
||||||
import serviceStore from '../service/store';
|
import serviceStore from '../service/store';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
import { Modal, Form, Table, Row, Col, Checkbox, Button, Alert } from 'antd';
|
import { Modal, Form, Table, Row, Col, Checkbox, Button, Alert } from 'antd';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
import envStore from '../environment/store';
|
import envStore from '../environment/store';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.less';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
@ -76,9 +76,8 @@ class Record extends React.Component {
|
||||||
onClick={() => this.handleEnvCheck(item)}
|
onClick={() => this.handleEnvCheck(item)}
|
||||||
style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}>
|
style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}>
|
||||||
<Col span={2}><Checkbox checked={envs.map(x => x.id).includes(item.id)}/></Col>
|
<Col span={2}><Checkbox checked={envs.map(x => x.id).includes(item.id)}/></Col>
|
||||||
<Col span={4} className={styles.ellipsis}>{item.key}</Col>
|
<Col span={10} className={styles.ellipsis}>{item.key}</Col>
|
||||||
<Col span={9} className={styles.ellipsis}>{item.name}</Col>
|
<Col span={10} className={styles.ellipsis}>{item.name}</Col>
|
||||||
<Col span={9} className={styles.ellipsis}>{item.desc}</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Modal, Form, Input, Checkbox, Switch, Row, Col, message } from 'antd';
|
import { Modal, Form, Input, Checkbox, Row, Col, message } from 'antd';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import envStore from '../environment/store'
|
import envStore from '../environment/store'
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.less';
|
||||||
import lds from 'lodash';
|
import lds from 'lodash';
|
||||||
|
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
|
@ -21,7 +21,6 @@ export default observer(function () {
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const formData = form.getFieldsValue();
|
const formData = form.getFieldsValue();
|
||||||
formData['is_public'] = store.type === 'src' ? false : formData['is_public'];
|
|
||||||
let request;
|
let request;
|
||||||
if (isModify) {
|
if (isModify) {
|
||||||
formData['id'] = store.record.id;
|
formData['id'] = store.record.id;
|
||||||
|
@ -62,38 +61,29 @@ export default observer(function () {
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
onOk={handleSubmit}>
|
onOk={handleSubmit}>
|
||||||
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||||
<Form.Item required name="key" label="Key">
|
<Form.Item required name="key" label="Key" tooltip="变量前缀由_SPUG_加唯一标识符组成,用于防止变量名冲突。" extra="可以由字母、数字和下划线组成。">
|
||||||
<Input disabled={isModify} placeholder="请输入"/>
|
<Input addonBefore={`_SPUG_${store.obj.key.toUpperCase()}_`} placeholder="请输入变量名"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="value" label="Value">
|
<Form.Item name="value" label="Value">
|
||||||
<Input.TextArea placeholder="请输入"/>
|
<Input.TextArea placeholder="请输入变量值"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="desc" label="备注">
|
<Form.Item name="desc" label="备注">
|
||||||
<Input.TextArea placeholder="请输入备注信息"/>
|
<Input.TextArea placeholder="请输入备注信息"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{store.type === 'app' && (
|
{isModify ? null : (
|
||||||
<Form.Item
|
<Form.Item label="选择环境" style={{lineHeight: '40px'}} tooltip="可多选环境,复制到所有选择的环境内。">
|
||||||
label="类型"
|
{envStore.records.map((item, index) => (
|
||||||
name="is_public"
|
<Row
|
||||||
valuePropName="checked"
|
key={item.id}
|
||||||
initialValue={store.record.is_public === undefined || store.record.is_public}
|
onClick={() => handleEnvCheck(item.id)}
|
||||||
tooltip={<a target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/conf-app">什么是公共/私有配置?</a>}>
|
style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}>
|
||||||
<Switch checkedChildren="公共" unCheckedChildren="私有"/>
|
<Col span={2}><Checkbox checked={envs.includes(item.id)}/></Col>
|
||||||
|
<Col span={10} className={styles.ellipsis}>{item.key}</Col>
|
||||||
|
<Col span={10} className={styles.ellipsis}>{item.name}</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
<Form.Item label="选择环境" style={{lineHeight: '40px'}}>
|
|
||||||
{envStore.records.map((item, index) => (
|
|
||||||
<Row
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => handleEnvCheck(item.id)}
|
|
||||||
style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}>
|
|
||||||
<Col span={2}><Checkbox disabled={isModify} checked={envs.includes(item.id)}/></Col>
|
|
||||||
<Col span={4} className={styles.ellipsis}>{item.key}</Col>
|
|
||||||
<Col span={9} className={styles.ellipsis}>{item.name}</Col>
|
|
||||||
<Col span={9} className={styles.ellipsis}>{item.desc}</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { SaveOutlined, EditOutlined } from '@ant-design/icons';
|
import { message } from 'antd';
|
||||||
import { Button, message } from 'antd';
|
import { ACEditor } from 'components';
|
||||||
import { AuthButton, ACEditor } from 'components';
|
|
||||||
import { http } from 'libs';
|
import { http } from 'libs';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
|
@ -16,8 +15,6 @@ class JSONView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
|
||||||
readOnly: true,
|
|
||||||
body: ''
|
body: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,53 +28,35 @@ class JSONView extends React.Component {
|
||||||
for (let item of store.records) {
|
for (let item of store.records) {
|
||||||
body[item.key] = item.value
|
body[item.key] = item.value
|
||||||
}
|
}
|
||||||
this.setState({readOnly: true, body: JSON.stringify(body, null, 2)})
|
this.setState({body: JSON.stringify(body, null, 2)})
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(this.state.body);
|
const data = JSON.parse(this.state.body);
|
||||||
this.setState({loading: true});
|
|
||||||
const formData = {type: store.type, o_id: store.id, env_id: store.env.id, data};
|
const formData = {type: store.type, o_id: store.id, env_id: store.env.id, data};
|
||||||
http.post('/api/config/parse/json/', formData)
|
return http.post('/api/config/parse/json/', formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
store.fetchRecords().then(this.updateValue)
|
store.fetchRecords().then(this.updateValue)
|
||||||
})
|
})
|
||||||
.finally(() => this.setState({loading: false}))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('解析JSON失败,请检查输入内容')
|
message.error('解析JSON失败,请检查输入内容')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {body, readOnly, loading} = this.state;
|
const {body} = this.state;
|
||||||
return (
|
return (
|
||||||
<div style={{position: 'relative'}}>
|
<ACEditor
|
||||||
<ACEditor
|
mode="json"
|
||||||
mode="json"
|
theme="tomorrow"
|
||||||
theme="tomorrow"
|
height="500px"
|
||||||
height="500px"
|
width="100%"
|
||||||
width="100%"
|
readOnly={!this.props.editable}
|
||||||
readOnly={readOnly}
|
setOptions={{useWorker: false}}
|
||||||
setOptions={{useWorker: false}}
|
value={body}
|
||||||
value={body}
|
onChange={v => this.setState({body: v})}/>
|
||||||
onChange={v => this.setState({body: v})}/>
|
|
||||||
{readOnly && <AuthButton
|
|
||||||
icon={<EditOutlined/>}
|
|
||||||
type="link"
|
|
||||||
size="large"
|
|
||||||
auth={`config.${store.type}.edit_config`}
|
|
||||||
style={{position: 'absolute', top: 0, right: 0}}
|
|
||||||
onClick={() => this.setState({readOnly: false})}>编辑</AuthButton>}
|
|
||||||
{readOnly || <Button
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
type="link"
|
|
||||||
size="large"
|
|
||||||
loading={loading}
|
|
||||||
style={{position: 'absolute', top: 0, right: 0}}
|
|
||||||
onClick={this.handleSubmit}>保存</Button>}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,30 +5,25 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { LockOutlined } from '@ant-design/icons';
|
|
||||||
import { Table, Modal, Tooltip, message } from 'antd';
|
import { Table, Modal, Tooltip, message } from 'antd';
|
||||||
import { Action } from 'components';
|
import { Action } from 'components';
|
||||||
import ComForm from './Form';
|
import ComForm from './Form';
|
||||||
import { http, hasPermission } from 'libs';
|
import { http, hasPermission, includes } from 'libs';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class TableView extends React.Component {
|
class TableView extends React.Component {
|
||||||
lockIcon = <Tooltip title="私有配置应用专用,不会被其他应用获取到">
|
|
||||||
<LockOutlined style={{marginRight: 5}}/>
|
|
||||||
</Tooltip>;
|
|
||||||
|
|
||||||
columns = [{
|
columns = [{
|
||||||
title: 'Key',
|
title: 'Key',
|
||||||
key: 'key',
|
key: 'key',
|
||||||
render: info => {
|
render: info => {
|
||||||
let prefix = (store.type === 'app' && info.is_public === false) ? this.lockIcon : null;
|
const value = `_SPUG_${store.obj.key}_${info.key}`.toUpperCase()
|
||||||
let content = info.desc ? <span style={{color: '#1890ff'}}>{info.key}</span> : info.key;
|
return info.desc ? (
|
||||||
return <React.Fragment>
|
<Tooltip title={info.desc}>
|
||||||
{prefix}
|
<span style={{color: '#2563fc'}}>{value}</span>
|
||||||
<Tooltip title={info.desc}>{content}</Tooltip>
|
</Tooltip>
|
||||||
</React.Fragment>
|
) : value
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
title: 'Value',
|
title: 'Value',
|
||||||
|
@ -48,7 +43,8 @@ class TableView extends React.Component {
|
||||||
className: hasPermission(`config.${store.type}.edit_config`) ? null : 'none',
|
className: hasPermission(`config.${store.type}.edit_config`) ? null : 'none',
|
||||||
render: info => (
|
render: info => (
|
||||||
<Action>
|
<Action>
|
||||||
<Action.Button auth={`config.${store.type}.edit_config`} onClick={() => store.showForm(info)}>编辑</Action.Button>
|
<Action.Button auth={`config.${store.type}.edit_config`}
|
||||||
|
onClick={() => store.showForm(info)}>编辑</Action.Button>
|
||||||
<Action.Button
|
<Action.Button
|
||||||
danger
|
danger
|
||||||
auth={`config.${store.type}.edit_config`}
|
auth={`config.${store.type}.edit_config`}
|
||||||
|
@ -73,9 +69,7 @@ class TableView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let data = store.records;
|
let data = store.records;
|
||||||
if (store.f_name) {
|
if (store.f_name) data = data.filter(x => includes(x.key, store.f_name))
|
||||||
data = data.filter(item => item['key'].toLowerCase().includes(store.f_name.toLowerCase()))
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Table
|
<Table
|
||||||
|
|
|
@ -5,22 +5,16 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Button, message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { SaveOutlined, EditOutlined } from '@ant-design/icons';
|
|
||||||
import { ACEditor } from 'components';
|
import { ACEditor } from 'components';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import { http } from 'libs';
|
import { http } from 'libs';
|
||||||
|
|
||||||
|
|
||||||
import { AuthButton } from 'components';
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class TextView extends React.Component {
|
class TextView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
|
||||||
readOnly: true,
|
|
||||||
body: ''
|
body: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,47 +28,29 @@ class TextView extends React.Component {
|
||||||
for (let item of store.records) {
|
for (let item of store.records) {
|
||||||
body += `${item.key} = ${item.value}\n`
|
body += `${item.key} = ${item.value}\n`
|
||||||
}
|
}
|
||||||
this.setState({readOnly: true, body})
|
this.setState({body})
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
this.setState({loading: true});
|
|
||||||
const formData = {type: store.type, o_id: store.id, env_id: store.env.id, data: this.state.body};
|
const formData = {type: store.type, o_id: store.id, env_id: store.env.id, data: this.state.body};
|
||||||
http.post('/api/config/parse/text/', formData)
|
return http.post('/api/config/parse/text/', formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
store.fetchRecords().then(this.updateValue)
|
store.fetchRecords().then(this.updateValue)
|
||||||
})
|
})
|
||||||
.finally(() => this.setState({loading: false}))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {body, loading, readOnly} = this.state;
|
const {body} = this.state;
|
||||||
return (
|
return (
|
||||||
<div style={{position: 'relative'}}>
|
<ACEditor
|
||||||
<ACEditor
|
mode="space"
|
||||||
mode="space"
|
width="100%"
|
||||||
width="100%"
|
height="500px"
|
||||||
height="500px"
|
theme="tomorrow"
|
||||||
theme="tomorrow"
|
value={body}
|
||||||
value={body}
|
readOnly={!this.props.editable}
|
||||||
readOnly={readOnly}
|
onChange={v => this.setState({body: v})}/>
|
||||||
onChange={v => this.setState({body: v})}/>
|
|
||||||
{readOnly && <AuthButton
|
|
||||||
icon={<EditOutlined/>}
|
|
||||||
type="link"
|
|
||||||
size="large"
|
|
||||||
auth={`config.${store.type}.edit_config`}
|
|
||||||
style={{position: 'absolute', top: 0, right: 0}}
|
|
||||||
onClick={() => this.setState({readOnly: false})}>编辑</AuthButton>}
|
|
||||||
{readOnly || <Button
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
type="link"
|
|
||||||
size="large"
|
|
||||||
loading={loading}
|
|
||||||
style={{position: 'absolute', top: 0, right: 0}}
|
|
||||||
onClick={this.handleSubmit}>保存</Button>}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,12 @@ import {
|
||||||
NumberOutlined,
|
NumberOutlined,
|
||||||
TableOutlined,
|
TableOutlined,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
PlusOutlined
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
SaveOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import envStore from '../environment/store';
|
import envStore from '../environment/store';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.less';
|
||||||
import history from 'libs/history';
|
import history from 'libs/history';
|
||||||
import { AuthDiv, AuthButton, Breadcrumb } from 'components';
|
import { AuthDiv, AuthButton, Breadcrumb } from 'components';
|
||||||
import DiffConfig from './DiffConfig';
|
import DiffConfig from './DiffConfig';
|
||||||
|
@ -24,8 +26,6 @@ import TextView from './TextView';
|
||||||
import JSONView from './JSONView';
|
import JSONView from './JSONView';
|
||||||
import Record from './Record';
|
import Record from './Record';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import appStore from '../app/store';
|
|
||||||
import srcStore from '../service/store';
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Index extends React.Component {
|
class Index extends React.Component {
|
||||||
|
@ -34,28 +34,36 @@ class Index extends React.Component {
|
||||||
this.textView = null;
|
this.textView = null;
|
||||||
this.JSONView = null;
|
this.JSONView = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
view: '1'
|
view: '1',
|
||||||
|
editable: false,
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {type, id} = this.props.match.params;
|
const {type, id} = this.props.match.params;
|
||||||
store.type = type;
|
store.initial(type, id)
|
||||||
store.id = id;
|
.then(() => {
|
||||||
if (envStore.records.length === 0) {
|
|
||||||
envStore.fetchRecords().then(() => {
|
|
||||||
if (envStore.records.length === 0) {
|
if (envStore.records.length === 0) {
|
||||||
Modal.error({
|
envStore.fetchRecords().then(() => {
|
||||||
title: '无可用环境',
|
if (envStore.records.length === 0) {
|
||||||
content: <div>配置依赖应用的运行环境,请在 <a href="/config/environment">环境管理</a> 中创建环境。</div>
|
Modal.error({
|
||||||
|
title: '无可用环境',
|
||||||
|
content: <div>配置依赖应用的运行环境,请在 <a href="/config/environment">环境管理</a> 中创建环境。</div>
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.updateEnv()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.updateEnv()
|
this.updateEnv()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
this.updateEnv()
|
|
||||||
}
|
componentWillUnmount() {
|
||||||
|
store.obj = {}
|
||||||
|
store.records = []
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEnv = (env) => {
|
updateEnv = (env) => {
|
||||||
|
@ -70,16 +78,25 @@ class Index extends React.Component {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
this.setState({loading: true})
|
||||||
|
const ref = this.state.view === '2' ? this.textView : this.JSONView
|
||||||
|
ref.handleSubmit()
|
||||||
|
.then(() => this.setState({editable: false}))
|
||||||
|
.finally(() => this.setState({loading: false}))
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {view} = this.state;
|
const {view, editable, loading} = this.state;
|
||||||
const isApp = store.type === 'app';
|
const isApp = store.type === 'app';
|
||||||
const record = isApp ? appStore.record : srcStore.record;
|
|
||||||
return (
|
return (
|
||||||
<AuthDiv auth={`config.${store.type}.view_config`}>
|
<AuthDiv auth={`config.${store.type}.view_config`}>
|
||||||
<Breadcrumb>
|
<Breadcrumb extra={(<Button type="primary" className={styles.historyBtn} icon={<HistoryOutlined/>}
|
||||||
|
onClick={store.showRecord}>更改历史</Button>)}>
|
||||||
|
<Breadcrumb.Item>首页</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>配置中心</Breadcrumb.Item>
|
<Breadcrumb.Item>配置中心</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item onClick={() => history.goBack()}>{isApp ? '应用配置' : '服务配置'}</Breadcrumb.Item>
|
<Breadcrumb.Item onClick={() => history.goBack()}>{isApp ? '应用配置' : '服务配置'}</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>{record.name}</Breadcrumb.Item>
|
<Breadcrumb.Item>{store.obj.name}</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
|
@ -108,26 +125,37 @@ class Index extends React.Component {
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Key">
|
<Form.Item label="Key">
|
||||||
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
|
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value}
|
||||||
|
placeholder="请输入"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space style={{flex: 1, justifyContent: 'flex-end'}}>
|
<Space style={{flex: 1, justifyContent: 'flex-end'}}>
|
||||||
<AuthButton
|
{['2', '3'].includes(view) ? editable ? (
|
||||||
auth="config.app.edit_config|config.service.edit_config"
|
<Button
|
||||||
disabled={view !== '1'}
|
icon={<SaveOutlined/>}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined/>}
|
loading={loading}
|
||||||
onClick={() => store.showForm()}>新增配置</AuthButton>
|
onClick={this.handleSubmit}>保存</Button>
|
||||||
<Button
|
) : (
|
||||||
type="primary"
|
<AuthButton
|
||||||
style={{backgroundColor: 'orange', borderColor: 'orange'}}
|
icon={<EditOutlined/>}
|
||||||
icon={<HistoryOutlined/>}
|
type="primary"
|
||||||
onClick={store.showRecord}>更改历史</Button>
|
auth={`config.${store.type}.edit_config`}
|
||||||
|
onClick={() => this.setState({editable: true})}>编辑</AuthButton>
|
||||||
|
) : (
|
||||||
|
<AuthButton
|
||||||
|
auth="config.app.edit_config|config.service.edit_config"
|
||||||
|
disabled={view !== '1'}
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined/>}
|
||||||
|
onClick={() => store.showForm()}>新增配置</AuthButton>
|
||||||
|
)}
|
||||||
|
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{view === '1' && <TableView/>}
|
{view === '1' && <TableView/>}
|
||||||
{view === '2' && <TextView ref={ref => this.textView = ref}/>}
|
{view === '2' && <TextView ref={ref => this.textView = ref} editable={editable}/>}
|
||||||
{view === '3' && <JSONView ref={ref => this.JSONView = ref}/>}
|
{view === '3' && <JSONView ref={ref => this.JSONView = ref} editable={editable}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{store.recordVisible && <Record/>}
|
{store.recordVisible && <Record/>}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
.left {
|
|
||||||
width: 250px;
|
|
||||||
border-right: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: rgba(0, 0, 0, .85);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 250px;
|
||||||
|
border-right: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyBtn {
|
||||||
|
background-color: orange;
|
||||||
|
border-color: orange;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: orange;
|
||||||
|
border-color: orange;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable record = {};
|
@observable record = {};
|
||||||
@observable env = {};
|
@observable env = {};
|
||||||
|
@observable obj = {};
|
||||||
@observable type;
|
@observable type;
|
||||||
@observable id;
|
@observable id;
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
|
@ -19,6 +20,15 @@ class Store {
|
||||||
|
|
||||||
@observable f_name;
|
@observable f_name;
|
||||||
|
|
||||||
|
initial = (type, id) => {
|
||||||
|
this.type = type
|
||||||
|
this.id = id
|
||||||
|
const url = type === 'app' ? '/api/app/' : '/api/config/service/'
|
||||||
|
this.isFetching = true
|
||||||
|
return http.get(url, {params: {id}})
|
||||||
|
.then(res => this.obj = res)
|
||||||
|
}
|
||||||
|
|
||||||
fetchRecords = () => {
|
fetchRecords = () => {
|
||||||
const params = {type: this.type, id: this.id, env_id: this.env.id};
|
const params = {type: this.type, id: this.id, env_id: this.env.id};
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
|
|
Loading…
Reference in New Issue