mirror of https://github.com/openspug/spug
upgrade host module
parent
e9c0a04218
commit
ef84cf6bff
|
@ -0,0 +1,81 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.views.generic import View
|
||||||
|
from libs import json_response, JsonParser, Argument, human_datetime
|
||||||
|
from apps.host.models import Host, HostExtend
|
||||||
|
from apps.host.utils import check_os_type
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendView(View):
|
||||||
|
def get(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('host_id', type=int, help='参数错误')
|
||||||
|
).parse(request.GET)
|
||||||
|
if error is None:
|
||||||
|
host = Host.objects.filter(pk=form.host_id).first()
|
||||||
|
if not host:
|
||||||
|
return json_response(error='未找到指定主机')
|
||||||
|
if not host.is_verified:
|
||||||
|
return json_response(error='该主机还未验证')
|
||||||
|
cli = host.get_ssh()
|
||||||
|
commands = [
|
||||||
|
"lscpu | grep '^CPU(s)' | awk '{print $2}'",
|
||||||
|
"free -m | awk 'NR==2{print $2}'",
|
||||||
|
"hostname -I",
|
||||||
|
"cat /etc/os-release | grep PRETTY_NAME | awk -F \\\" '{print $2}'",
|
||||||
|
"fdisk -l | grep '^Disk /' | awk '{print $5}'"
|
||||||
|
]
|
||||||
|
code, out = cli.exec_command(';'.join(commands))
|
||||||
|
if code != 0:
|
||||||
|
return json_response(error=f'Exception: {out}')
|
||||||
|
response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []}
|
||||||
|
for index, line in enumerate(out.strip().split('\n')):
|
||||||
|
if index == 0:
|
||||||
|
response['cpu'] = int(line)
|
||||||
|
elif index == 1:
|
||||||
|
response['memory'] = round(int(line) / 1000, 1)
|
||||||
|
elif index == 2:
|
||||||
|
for ip in line.split():
|
||||||
|
if ipaddress.ip_address(ip).is_global:
|
||||||
|
response['public_ip_address'].append(ip)
|
||||||
|
else:
|
||||||
|
response['private_ip_address'].append(ip)
|
||||||
|
elif index == 3:
|
||||||
|
response['os_name'] = line
|
||||||
|
else:
|
||||||
|
response['disk'].append(round(int(line) / 1024 / 1024 / 1024, 0))
|
||||||
|
return json_response(response)
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('host_id', type=int, help='参数错误'),
|
||||||
|
Argument('instance_id', required=False),
|
||||||
|
Argument('os_name', help='请输入操作系统'),
|
||||||
|
Argument('cpu', type=int, help='请输入CPU核心数'),
|
||||||
|
Argument('memory', type=float, help='请输入内存大小'),
|
||||||
|
Argument('disk', type=list, filter=lambda x: len(x), help='请添加磁盘'),
|
||||||
|
Argument('private_ip_address', type=list, filter=lambda x: len(x), help='请添加内网IP'),
|
||||||
|
Argument('public_ip_address', type=list, required=False),
|
||||||
|
Argument('instance_charge_type', default='Other'),
|
||||||
|
Argument('internet_charge_type', default='Other'),
|
||||||
|
Argument('created_time', required=False),
|
||||||
|
Argument('expired_time', required=False)
|
||||||
|
).parse(request.body)
|
||||||
|
if error is None:
|
||||||
|
host = Host.objects.filter(pk=form.host_id).first()
|
||||||
|
form.disk = json.dumps(form.disk)
|
||||||
|
form.public_ip_address = json.dumps(form.public_ip_address) if form.public_ip_address else '[]'
|
||||||
|
form.private_ip_address = json.dumps(form.private_ip_address)
|
||||||
|
form.updated_at = human_datetime()
|
||||||
|
form.os_type = check_os_type(form.os_name)
|
||||||
|
if hasattr(host, 'hostextend'):
|
||||||
|
extend = host.hostextend
|
||||||
|
extend.update_by_dict(form)
|
||||||
|
else:
|
||||||
|
extend = HostExtend.objects.create(host=host, **form)
|
||||||
|
return json_response(extend.to_view())
|
||||||
|
return json_response(error=error)
|
|
@ -46,7 +46,7 @@ class Host(models.Model, ModelMixin):
|
||||||
class HostExtend(models.Model, ModelMixin):
|
class HostExtend(models.Model, ModelMixin):
|
||||||
INSTANCE_CHARGE_TYPES = (
|
INSTANCE_CHARGE_TYPES = (
|
||||||
('PrePaid', '包年包月'),
|
('PrePaid', '包年包月'),
|
||||||
('PostPaid', '按量付费'),
|
('PostPaid', '按量计费'),
|
||||||
('Other', '其他')
|
('Other', '其他')
|
||||||
)
|
)
|
||||||
INTERNET_CHARGE_TYPES = (
|
INTERNET_CHARGE_TYPES = (
|
||||||
|
@ -55,8 +55,8 @@ class HostExtend(models.Model, ModelMixin):
|
||||||
('Other', '其他')
|
('Other', '其他')
|
||||||
)
|
)
|
||||||
host = models.OneToOneField(Host, on_delete=models.CASCADE)
|
host = models.OneToOneField(Host, on_delete=models.CASCADE)
|
||||||
instance_id = models.CharField(max_length=64)
|
instance_id = models.CharField(max_length=64, null=True)
|
||||||
zone_id = models.CharField(max_length=30)
|
zone_id = models.CharField(max_length=30, null=True)
|
||||||
cpu = models.IntegerField()
|
cpu = models.IntegerField()
|
||||||
memory = models.FloatField()
|
memory = models.FloatField()
|
||||||
disk = models.CharField(max_length=255, default='[]')
|
disk = models.CharField(max_length=255, default='[]')
|
||||||
|
@ -66,7 +66,7 @@ class HostExtend(models.Model, ModelMixin):
|
||||||
public_ip_address = models.CharField(max_length=255)
|
public_ip_address = models.CharField(max_length=255)
|
||||||
instance_charge_type = models.CharField(max_length=20, choices=INSTANCE_CHARGE_TYPES)
|
instance_charge_type = models.CharField(max_length=20, choices=INSTANCE_CHARGE_TYPES)
|
||||||
internet_charge_type = models.CharField(max_length=20, choices=INTERNET_CHARGE_TYPES)
|
internet_charge_type = models.CharField(max_length=20, choices=INTERNET_CHARGE_TYPES)
|
||||||
created_time = models.CharField(max_length=20)
|
created_time = models.CharField(max_length=20, null=True)
|
||||||
expired_time = models.CharField(max_length=20, null=True)
|
expired_time = models.CharField(max_length=20, null=True)
|
||||||
updated_at = models.CharField(max_length=20, default=human_datetime)
|
updated_at = models.CharField(max_length=20, default=human_datetime)
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,12 @@ from django.urls import path
|
||||||
|
|
||||||
from apps.host.views import *
|
from apps.host.views import *
|
||||||
from apps.host.group import GroupView
|
from apps.host.group import GroupView
|
||||||
|
from apps.host.extend import ExtendView
|
||||||
from apps.host.add import get_regions, cloud_import
|
from apps.host.add import get_regions, cloud_import
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', HostView.as_view()),
|
path('', HostView.as_view()),
|
||||||
|
path('extend/', ExtendView.as_view()),
|
||||||
path('group/', GroupView.as_view()),
|
path('group/', GroupView.as_view()),
|
||||||
path('import/', post_import),
|
path('import/', post_import),
|
||||||
path('import/cloud/', cloud_import),
|
path('import/cloud/', cloud_import),
|
||||||
|
|
|
@ -51,11 +51,14 @@ class HostView(View):
|
||||||
if other and (not form.id or other.id != form.id):
|
if other and (not form.id or other.id != form.id):
|
||||||
return json_response(error=f'已存在的主机名称【{form.name}】')
|
return json_response(error=f'已存在的主机名称【{form.name}】')
|
||||||
if form.id:
|
if form.id:
|
||||||
Host.objects.filter(pk=form.id).update(**form)
|
Host.objects.filter(pk=form.id).update(is_verified=True, **form)
|
||||||
host = Host.objects.get(pk=form.id)
|
host = Host.objects.get(pk=form.id)
|
||||||
else:
|
else:
|
||||||
host = Host.objects.create(created_by=request.user, **form)
|
host = Host.objects.create(created_by=request.user, is_verified=True, **form)
|
||||||
host.groups.set(group_ids)
|
host.groups.set(group_ids)
|
||||||
|
response = host.to_view()
|
||||||
|
response['group_ids'] = group_ids
|
||||||
|
return json_response(response)
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
|
@ -91,7 +94,6 @@ class HostView(View):
|
||||||
if detection:
|
if detection:
|
||||||
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
|
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
|
||||||
Host.objects.filter(pk=form.id).delete()
|
Host.objects.filter(pk=form.id).delete()
|
||||||
print('pk: ', form.id)
|
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,13 +145,7 @@ def post_import(request):
|
||||||
|
|
||||||
|
|
||||||
def valid_ssh(hostname, port, username, password=None, pkey=None, with_expect=True):
|
def valid_ssh(hostname, port, username, password=None, pkey=None, with_expect=True):
|
||||||
try:
|
private_key, public_key = AppSetting.get_ssh_key()
|
||||||
private_key = AppSetting.get('private_key')
|
|
||||||
public_key = AppSetting.get('public_key')
|
|
||||||
except KeyError:
|
|
||||||
private_key, public_key = SSH.generate_key()
|
|
||||||
AppSetting.set('private_key', private_key, 'ssh private key')
|
|
||||||
AppSetting.set('public_key', public_key, 'ssh public key')
|
|
||||||
if password:
|
if password:
|
||||||
_cli = SSH(hostname, port, username, password=str(password))
|
_cli = SSH(hostname, port, username, password=str(password))
|
||||||
_cli.add_public_key(public_key)
|
_cli.add_public_key(public_key)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
# Released under the AGPL-3.0 License.
|
# Released under the AGPL-3.0 License.
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from apps.setting.models import Setting, KEYS_DEFAULT
|
from apps.setting.models import Setting, KEYS_DEFAULT
|
||||||
|
from libs.ssh import SSH
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,3 +30,13 @@ class AppSetting:
|
||||||
Setting.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc})
|
Setting.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc})
|
||||||
else:
|
else:
|
||||||
raise KeyError('invalid key')
|
raise KeyError('invalid key')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ssh_key(cls):
|
||||||
|
public_key = cls.get_default('public_key')
|
||||||
|
private_key = cls.get_default('private_key')
|
||||||
|
if not private_key or not public_key:
|
||||||
|
private_key, public_key = SSH.generate_key()
|
||||||
|
cls.set('private_key', private_key)
|
||||||
|
cls.set('public_key', public_key)
|
||||||
|
return private_key, public_key
|
||||||
|
|
|
@ -18,6 +18,11 @@ class ModelMixin(object):
|
||||||
else:
|
else:
|
||||||
return {f.attname: getattr(self, f.attname) for f in self._meta.fields}
|
return {f.attname: getattr(self, f.attname) for f in self._meta.fields}
|
||||||
|
|
||||||
|
def update_by_dict(self, data):
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
# 使用该混入类,需要request.user对象实现has_perms方法
|
# 使用该混入类,需要request.user对象实现has_perms方法
|
||||||
class PermissionMixin(object):
|
class PermissionMixin(object):
|
||||||
|
|
|
@ -1,19 +1,117 @@
|
||||||
import React from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Drawer, Descriptions, List } from 'antd';
|
import { Drawer, Descriptions, List, Button, Input, Select, DatePicker, Tag, message } from 'antd';
|
||||||
|
import { EditOutlined, SaveOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { http } from 'libs';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
const host = store.record;
|
const [edit, setEdit] = useState(false);
|
||||||
const group_ids = host.group_ids || [];
|
const [host, setHost] = useState(store.record);
|
||||||
|
const diskInput = useRef();
|
||||||
|
const sipInput = useRef();
|
||||||
|
const gipInput = useRef();
|
||||||
|
const [tag, setTag] = useState();
|
||||||
|
const [inputVisible, setInputVisible] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (store.detailVisible) {
|
||||||
|
setHost(lds.cloneDeep(store.record))
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [store.detailVisible])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputVisible === 'disk') {
|
||||||
|
diskInput.current.focus()
|
||||||
|
} else if (inputVisible === 'sip') {
|
||||||
|
sipInput.current.focus()
|
||||||
|
} else if (inputVisible === 'gip') {
|
||||||
|
gipInput.current.focus()
|
||||||
|
}
|
||||||
|
}, [inputVisible])
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
setLoading(true)
|
||||||
|
if (host.created_time) host.created_time = moment(host.created_time).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
if (host.expired_time) host.expired_time = moment(host.expired_time).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
http.post('/api/host/extend/', {host_id: host.id, ...host})
|
||||||
|
.then(res => {
|
||||||
|
Object.assign(host, res);
|
||||||
|
setEdit(false);
|
||||||
|
setHost(lds.cloneDeep(host));
|
||||||
|
store.fetchRecords()
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFetch() {
|
||||||
|
setFetching(true);
|
||||||
|
http.get('/api/host/extend/', {params: {host_id: host.id}})
|
||||||
|
.then(res => {
|
||||||
|
Object.assign(host, res);
|
||||||
|
setHost(lds.cloneDeep(host));
|
||||||
|
message.success('同步成功')
|
||||||
|
})
|
||||||
|
.finally(() => setFetching(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(e, key) {
|
||||||
|
host[key] = e && e.target ? e.target.value : e;
|
||||||
|
setHost({...host})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
store.detailVisible = false;
|
||||||
|
setEdit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTagConfirm(key) {
|
||||||
|
if (tag) {
|
||||||
|
if (key === 'disk') {
|
||||||
|
const value = Number(tag);
|
||||||
|
if (lds.isNaN(value)) return message.error('请输入数字');
|
||||||
|
host.disk ? host.disk.push(value) : host.disk = [value]
|
||||||
|
} else if (key === 'sip') {
|
||||||
|
host.private_ip_address ? host.private_ip_address.push(tag) : host.private_ip_address = [tag]
|
||||||
|
} else if (key === 'gip') {
|
||||||
|
host.public_ip_address ? host.public_ip_address.push(tag) : host.public_ip_address = [tag]
|
||||||
|
}
|
||||||
|
setHost(lds.cloneDeep(host))
|
||||||
|
}
|
||||||
|
setTag(undefined);
|
||||||
|
setInputVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTagRemove(key, index) {
|
||||||
|
if (key === 'disk') {
|
||||||
|
host.disk.splice(index, 1)
|
||||||
|
} else if (key === 'sip') {
|
||||||
|
host.private_ip_address.splice(index, 1)
|
||||||
|
} else if (key === 'gip') {
|
||||||
|
host.public_ip_address.splice(index, 1)
|
||||||
|
}
|
||||||
|
setHost(lds.cloneDeep(host))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width={500}
|
width={550}
|
||||||
title={host.name}
|
title={host.name}
|
||||||
placement="right"
|
placement="right"
|
||||||
onClose={() => store.detailVisible = false}
|
onClose={handleClose}
|
||||||
visible={store.detailVisible}>
|
visible={store.detailVisible}>
|
||||||
<Descriptions bordered size="small" title={<span style={{fontWeight: 500}}>基本信息</span>} column={1}>
|
<Descriptions
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
labelStyle={{width: 150}}
|
||||||
|
title={<span style={{fontWeight: 500}}>基本信息</span>}
|
||||||
|
column={1}>
|
||||||
<Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
|
<Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
|
||||||
<Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
|
<Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
|
||||||
<Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
|
<Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
|
||||||
|
@ -21,34 +119,148 @@ export default observer(function () {
|
||||||
<Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
|
<Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
|
||||||
<Descriptions.Item label="所属分组">
|
<Descriptions.Item label="所属分组">
|
||||||
<List>
|
<List>
|
||||||
{group_ids.map(g_id => (
|
{lds.get(host, 'group_ids', []).map(g_id => (
|
||||||
<List.Item key={g_id} style={{padding: '6px 0'}}>{store.groups[g_id]}</List.Item>
|
<List.Item key={g_id} style={{padding: '6px 0'}}>{store.groups[g_id]}</List.Item>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
{host.id ? (
|
<Descriptions
|
||||||
<Descriptions
|
bordered
|
||||||
bordered
|
size="small"
|
||||||
size="small"
|
column={1}
|
||||||
column={1}
|
className={edit ? styles.hostExtendEdit : null}
|
||||||
style={{marginTop: 24}}
|
labelStyle={{width: 150}}
|
||||||
title={<span style={{fontWeight: 500}}>扩展信息</span>}>
|
style={{marginTop: 24}}
|
||||||
<Descriptions.Item label="实例ID">{host.instance_id}</Descriptions.Item>
|
extra={edit ? ([
|
||||||
<Descriptions.Item label="操作系统">{host.os_name}</Descriptions.Item>
|
<Button key="1" type="link" loading={fetching} icon={<SyncOutlined/>} onClick={handleFetch}>同步</Button>,
|
||||||
<Descriptions.Item label="CPU">{host.cpu}核</Descriptions.Item>
|
<Button key="2" type="link" loading={loading} icon={<SaveOutlined/>} onClick={handleSubmit}>保存</Button>
|
||||||
<Descriptions.Item label="内存">{host.memory}GB</Descriptions.Item>
|
]) : (
|
||||||
<Descriptions.Item label="磁盘">{host.disk.map(x => `${x}GB`).join(', ')}</Descriptions.Item>
|
<Button type="link" icon={<EditOutlined/>} onClick={() => setEdit(true)}>编辑</Button>
|
||||||
<Descriptions.Item label="内网IP">{host.private_ip_address.join(', ')}</Descriptions.Item>
|
)}
|
||||||
<Descriptions.Item label="公网IP">{host.public_ip_address.join(', ')}</Descriptions.Item>
|
title={<span style={{fontWeight: 500}}>扩展信息</span>}>
|
||||||
<Descriptions.Item label="实例付费方式">{host.instance_charge_type_alias}</Descriptions.Item>
|
<Descriptions.Item label="实例ID">
|
||||||
<Descriptions.Item label="网络付费方式">{host.internet_charge_type_alisa}</Descriptions.Item>
|
{edit ? (
|
||||||
<Descriptions.Item label="创建时间">{host.created_time}</Descriptions.Item>
|
<Input value={host.instance_id} onChange={e => handleChange(e, 'instance_id')} placeholder="选填"/>
|
||||||
<Descriptions.Item label="到期时间">{host.expired_time || 'N/A'}</Descriptions.Item>
|
) : host.instance_id}
|
||||||
<Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
<Descriptions.Item label="操作系统">
|
||||||
) : null}
|
{edit ? (
|
||||||
|
<Input value={host.os_name} onChange={e => handleChange(e, 'os_name')}
|
||||||
|
placeholder="例如:Ubuntu Server 16.04.1 LTS"/>
|
||||||
|
) : host.os_name}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="CPU">
|
||||||
|
{edit ? (
|
||||||
|
<Input suffix="核" style={{width: 100}} value={host.cpu} onChange={e => handleChange(e, 'cpu')}
|
||||||
|
placeholder="数字"/>
|
||||||
|
) : host.cpu ? `${host.cpu}核` : null}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="内存">
|
||||||
|
{edit ? (
|
||||||
|
<Input suffix="GB" style={{width: 100}} value={host.memory} onChange={e => handleChange(e, 'memory')}
|
||||||
|
placeholder="数字"/>
|
||||||
|
) : host.memory ? `${host.memory}GB` : null}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="磁盘">
|
||||||
|
{lds.get(host, 'disk', []).map((item, index) => (
|
||||||
|
<Tag visible closable={edit} key={index} onClose={() => handleTagRemove('disk', index)}>{item}GB</Tag>
|
||||||
|
))}
|
||||||
|
{edit && (inputVisible === 'disk' ? (
|
||||||
|
<Input
|
||||||
|
ref={diskInput}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
value={tag}
|
||||||
|
className={styles.tagNumberInput}
|
||||||
|
onChange={e => setTag(e.target.value)}
|
||||||
|
onBlur={() => handleTagConfirm('disk')}
|
||||||
|
onPressEnter={() => handleTagConfirm('disk')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tag className={styles.tagAdd} onClick={() => setInputVisible('disk')}><PlusOutlined/> 新建</Tag>
|
||||||
|
))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="内网IP">
|
||||||
|
{lds.get(host, 'private_ip_address', []).map((item, index) => (
|
||||||
|
<Tag visible closable={edit} key={index} onClose={() => handleTagRemove('sip', index)}>{item}</Tag>
|
||||||
|
))}
|
||||||
|
{edit && (inputVisible === 'sip' ? (
|
||||||
|
<Input
|
||||||
|
ref={sipInput}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
value={tag}
|
||||||
|
className={styles.tagInput}
|
||||||
|
onChange={e => setTag(e.target.value)}
|
||||||
|
onBlur={() => handleTagConfirm('sip')}
|
||||||
|
onPressEnter={() => handleTagConfirm('sip')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tag className={styles.tagAdd} onClick={() => setInputVisible('sip')}><PlusOutlined/> 新建</Tag>
|
||||||
|
))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="公网IP">
|
||||||
|
{lds.get(host, 'public_ip_address', []).map((item, index) => (
|
||||||
|
<Tag visible closable={edit} key={index} onClose={() => handleTagRemove('gip', index)}>{item}</Tag>
|
||||||
|
))}
|
||||||
|
{edit && (inputVisible === 'gip' ? (
|
||||||
|
<Input
|
||||||
|
ref={gipInput}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
value={tag}
|
||||||
|
className={styles.tagInput}
|
||||||
|
onChange={e => setTag(e.target.value)}
|
||||||
|
onBlur={() => handleTagConfirm('gip')}
|
||||||
|
onPressEnter={() => handleTagConfirm('gip')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tag className={styles.tagAdd} onClick={() => setInputVisible('gip')}><PlusOutlined/> 新建</Tag>
|
||||||
|
))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="实例计费方式">
|
||||||
|
{edit ? (
|
||||||
|
<Select
|
||||||
|
style={{width: 150}}
|
||||||
|
value={host.instance_charge_type}
|
||||||
|
placeholder="请选择"
|
||||||
|
onChange={v => handleChange(v, 'instance_charge_type')}>
|
||||||
|
<Select.Option value="PrePaid">包年包月</Select.Option>
|
||||||
|
<Select.Option value="PostPaid">按量计费</Select.Option>
|
||||||
|
<Select.Option value="Other">其他</Select.Option>
|
||||||
|
</Select>
|
||||||
|
) : host.instance_charge_type_alias}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="网络计费方式">
|
||||||
|
{edit ? (
|
||||||
|
<Select
|
||||||
|
style={{width: 150}}
|
||||||
|
value={host.internet_charge_type}
|
||||||
|
placeholder="请选择"
|
||||||
|
onChange={v => handleChange(v, 'internet_charge_type')}>
|
||||||
|
<Select.Option value="PayByBandwidth">按带宽计费</Select.Option>
|
||||||
|
<Select.Option value="PayByTraffic">按流量计费</Select.Option>
|
||||||
|
<Select.Option value="Other">其他</Select.Option>
|
||||||
|
</Select>
|
||||||
|
) : host.internet_charge_type_alisa}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间">
|
||||||
|
{edit ? (
|
||||||
|
<DatePicker
|
||||||
|
value={host.created_time ? moment(host.created_time) : undefined}
|
||||||
|
onChange={v => handleChange(v, 'created_time')}/>
|
||||||
|
) : host.created_time}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="到期时间">
|
||||||
|
{edit ? (
|
||||||
|
<DatePicker
|
||||||
|
value={host.expired_time ? moment(host.expired_time) : undefined}
|
||||||
|
onChange={v => handleChange(v, 'expired_time')}/>
|
||||||
|
) : host.expired_time}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -46,7 +46,8 @@ export default observer(function () {
|
||||||
} else {
|
} else {
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
store.formVisible = false;
|
store.formVisible = false;
|
||||||
store.fetchRecords()
|
store.fetchRecords();
|
||||||
|
if (!store.record.id) handleNext(res)
|
||||||
}
|
}
|
||||||
}, () => setLoading(false))
|
}, () => setLoading(false))
|
||||||
}
|
}
|
||||||
|
@ -57,12 +58,24 @@ export default observer(function () {
|
||||||
return http.post('/api/host/', formData).then(res => {
|
return http.post('/api/host/', formData).then(res => {
|
||||||
message.success('验证成功');
|
message.success('验证成功');
|
||||||
store.formVisible = false;
|
store.formVisible = false;
|
||||||
store.fetchRecords()
|
store.fetchRecords();
|
||||||
|
if (!store.record.id) handleNext(res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
message.error('请输入授权密码')
|
message.error('请输入授权密码')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNext(res) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示信息',
|
||||||
|
content: '是否继续完善主机的扩展信息?',
|
||||||
|
onOk: () => {
|
||||||
|
store.record = res;
|
||||||
|
store.detailVisible = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const ConfirmForm = (props) => (
|
const ConfirmForm = (props) => (
|
||||||
<Form layout="vertical" style={{marginTop: 24}}>
|
<Form layout="vertical" style={{marginTop: 24}}>
|
||||||
<Form.Item required label="授权密码" help={`用户 ${props.username} 的密码, 该密码仅做首次验证使用,不会存储该密码。`}>
|
<Form.Item required label="授权密码" help={`用户 ${props.username} 的密码, 该密码仅做首次验证使用,不会存储该密码。`}>
|
||||||
|
@ -94,14 +107,14 @@ export default observer(function () {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
width={800}
|
width={700}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
title={store.record.id ? '编辑主机' : '新建主机'}
|
title={store.record.id ? '编辑主机' : '新建主机'}
|
||||||
okText="验证"
|
okText="验证"
|
||||||
onCancel={() => store.formVisible = false}
|
onCancel={() => store.formVisible = false}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
onOk={handleSubmit}>
|
onOk={handleSubmit}>
|
||||||
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}} initialValues={info}>
|
<Form form={form} labelCol={{span: 5}} wrapperCol={{span: 17}} initialValues={info}>
|
||||||
<Form.Item required name="group_ids" label="主机分组">
|
<Form.Item required name="group_ids" label="主机分组">
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
multiple
|
multiple
|
||||||
|
|
|
@ -2,3 +2,26 @@
|
||||||
width: 350px;
|
width: 350px;
|
||||||
margin: 0 auto 30px;
|
margin: 0 auto 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tagAdd {
|
||||||
|
background: #fff;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagNumberInput {
|
||||||
|
width: 78px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInput {
|
||||||
|
width: 140px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostExtendEdit {
|
||||||
|
:global(.ant-descriptions-item-content) {
|
||||||
|
padding: 4px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue