upgrade host module

pull/330/head
vapao 2021-04-12 22:33:10 +08:00
parent 19150210a2
commit e9c0a04218
24 changed files with 527 additions and 21 deletions

59
spug_api/apps/host/add.py Normal file
View File

@ -0,0 +1,59 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from libs import json_response, JsonParser, Argument
from apps.host.models import Host, HostExtend, Group
from apps.host import utils
import json
def get_regions(request):
form, error = JsonParser(
Argument('type', filter=lambda x: x in ('ali', 'tencent'), help='参数错误'),
Argument('ak', help='请输入AccessKey ID'),
Argument('ac', help='请输入AccessKey Secret'),
).parse(request.GET)
if error is None:
response = []
if form.type == 'ali':
for item in utils.fetch_ali_regions(form.ak, form.ac):
response.append({'id': item['RegionId'], 'name': item['LocalName']})
else:
for item in utils.fetch_tencent_regions(form.ak, form.ac):
response.append({'id': item['Region'], 'name': item['RegionName']})
return json_response(response)
return json_response(error=error)
def cloud_import(request):
form, error = JsonParser(
Argument('type', filter=lambda x: x in ('ali', 'tencent'), help='参数错误'),
Argument('ak', help='请输入AccessKey ID'),
Argument('ac', help='请输入AccessKey Secret'),
Argument('region_id', help='请选择区域'),
Argument('group_id', type=int, help='请选择分组')
).parse(request.body)
if error is None:
group = Group.objects.filter(pk=form.group_id).first()
if not group:
return json_response(error='未找到指定分组')
if form.type == 'ali':
instances = utils.fetch_ali_instances(form.ak, form.ac, form.region_id)
else:
instances = utils.fetch_tencent_instances(form.ak, form.ac, form.region_id)
host_add_ids = []
for item in instances:
instance_id = item['instance_id']
item['public_ip_address'] = json.dumps(item['public_ip_address'] or [])
item['private_ip_address'] = json.dumps(item['private_ip_address'] or [])
if HostExtend.objects.filter(instance_id=instance_id).exists():
HostExtend.objects.filter(instance_id=instance_id).update(**item)
else:
host = Host.objects.create(name=instance_id, created_by=request.user)
HostExtend.objects.create(host=host, **item)
host_add_ids.append(host.id)
if host_add_ids:
group.hosts.add(*host_add_ids)
return json_response(len(instances))
return json_response(error=error)

View File

@ -6,15 +6,17 @@ from libs import ModelMixin, human_datetime
from apps.account.models import User
from apps.setting.utils import AppSetting
from libs.ssh import SSH
import json
class Host(models.Model, ModelMixin):
name = models.CharField(max_length=50)
hostname = models.CharField(max_length=50)
port = models.IntegerField()
port = models.IntegerField(null=True)
username = models.CharField(max_length=50)
pkey = models.TextField(null=True)
desc = models.CharField(max_length=255, null=True)
is_verified = models.BooleanField(default=False)
created_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
@ -28,6 +30,8 @@ class Host(models.Model, ModelMixin):
def to_view(self):
tmp = self.to_dict()
if hasattr(self, 'hostextend'):
tmp.update(self.hostextend.to_view())
tmp['group_ids'] = []
return tmp
@ -55,7 +59,7 @@ class HostExtend(models.Model, ModelMixin):
zone_id = models.CharField(max_length=30)
cpu = models.IntegerField()
memory = models.FloatField()
disk = models.CharField(max_length=255)
disk = models.CharField(max_length=255, default='[]')
os_name = models.CharField(max_length=50)
os_type = models.CharField(max_length=20)
private_ip_address = models.CharField(max_length=255)
@ -66,6 +70,15 @@ class HostExtend(models.Model, ModelMixin):
expired_time = models.CharField(max_length=20, null=True)
updated_at = models.CharField(max_length=20, default=human_datetime)
def to_view(self):
tmp = self.to_dict(excludes=('id',))
tmp['disk'] = json.loads(self.disk)
tmp['private_ip_address'] = json.loads(self.private_ip_address)
tmp['public_ip_address'] = json.loads(self.public_ip_address)
tmp['instance_charge_type_alias'] = self.get_instance_charge_type_display()
tmp['internet_charge_type_alisa'] = self.get_internet_charge_type_display()
return tmp
class Meta:
db_table = 'host_extend'

View File

@ -5,10 +5,13 @@ from django.urls import path
from apps.host.views import *
from apps.host.group import GroupView
from apps.host.add import get_regions, cloud_import
urlpatterns = [
path('', HostView.as_view()),
path('group/', GroupView.as_view()),
path('import/', post_import),
path('import/cloud/', cloud_import),
path('import/region/', get_regions),
path('parse/', post_parse),
]

174
spug_api/apps/host/utils.py Normal file
View File

@ -0,0 +1,174 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from libs.helper import make_ali_request, make_tencent_request
from collections import defaultdict
from datetime import datetime
def check_os_type(os_name):
os_name = os_name.lower()
types = ('centos', 'coreos', 'debian', 'suse', 'ubuntu', 'windows', 'freebsd', 'tencent', 'alibaba')
for t in types:
if t in os_name:
return t
return 'unknown'
def check_instance_charge_type(value, supplier):
if supplier == 'ali':
if value in ('PrePaid', 'PostPaid'):
return value
else:
return 'Other'
if supplier == 'tencent':
if value == 'PREPAID':
return 'PrePaid'
if value == 'POSTPAID_BY_HOUR':
return 'PostPaid'
return 'Other'
def check_internet_charge_type(value, supplier):
if supplier == 'ali':
if value in ('PayByTraffic', 'PayByBandwidth'):
return value
else:
return 'Other'
if supplier == 'tencent':
if value == 'TRAFFIC_POSTPAID_BY_HOUR':
return 'PayByTraffic'
if value in ('BANDWIDTH_PREPAID', 'BANDWIDTH_POSTPAID_BY_HOUR'):
return 'PayByBandwidth'
return 'Other'
def parse_utc_date(value):
if not value:
return None
s_format = '%Y-%m-%dT%H:%M:%S%z'
if len(value) == 17:
s_format = '%Y-%m-%dT%H:%M%z'
date = datetime.strptime(value, s_format)
return date.astimezone().strftime('%Y-%m-%d %H:%M:%S')
def parse__date(value, supplier):
if not value:
return None
date = datetime.strptime(value, '%Y-%m-%dT%H:%M%z')
return date.astimezone().strftime('%Y-%m-%d %H:%M:%S')
def fetch_ali_regions(ak, ac):
params = dict(Action='DescribeRegions')
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Regions' in res:
return res['Regions']['Region']
else:
raise Exception(res)
def fetch_ali_disks(ak, ac, region_id, page_number=1):
data, page_size = defaultdict(list), 20
params = dict(
Action='DescribeDisks',
RegionId=region_id,
PageNumber=page_number,
PageSize=page_size
)
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Disks' in res:
for item in res['Disks']['Disk']:
data[item['InstanceId']].append(item['Size'])
if len(res['Disks']['Disk']) == page_size:
page_number += 1
new_data = fetch_ali_disks(ak, ac, 'http://ecs.aliyuncs.com', page_number)
data.update(new_data)
return data
else:
raise Exception(res)
def fetch_ali_instances(ak, ac, region_id, page_number=1):
data, page_size = {}, 20
params = dict(
Action='DescribeInstances',
RegionId=region_id,
PageNumber=page_number,
PageSize=page_size
)
res = make_ali_request(ak, ac, 'http://ecs.aliyuncs.com', params)
if 'Instances' not in res:
raise Exception(res)
for item in res['Instances']['Instance']:
network_interface = item['NetworkInterfaces']['NetworkInterface']
data[item['InstanceId']] = dict(
instance_id=item['InstanceId'],
os_name=item['OSName'],
os_type=check_os_type(item['OSName']),
cpu=item['Cpu'],
memory=item['Memory'] / 1024,
created_time=parse_utc_date(item['CreationTime']),
expired_time=parse_utc_date(item['ExpiredTime']),
instance_charge_type=check_instance_charge_type(item['InstanceChargeType'], 'ali'),
internet_charge_type=check_internet_charge_type(item['InternetChargeType'], 'ali'),
public_ip_address=item['PublicIpAddress']['IpAddress'],
private_ip_address=list(map(lambda x: x['PrimaryIpAddress'], network_interface)),
zone_id=item['ZoneId']
)
if len(res['Instances']['Instance']) == page_size:
page_number += 1
new_data = fetch_ali_instances(ak, ac, region_id, page_number)
data.update(new_data)
if page_number != 1:
return data
for instance_id, disk in fetch_ali_disks(ak, ac, region_id).items():
if instance_id in data:
data[instance_id]['disk'] = disk
return list(data.values())
def fetch_tencent_regions(ak, ac):
params = dict(Action='DescribeRegions')
res = make_tencent_request(ak, ac, 'cvm.tencentcloudapi.com', params)
if 'RegionSet' in res['Response']:
return res['Response']['RegionSet']
else:
raise Exception(res)
def fetch_tencent_instances(ak, ac, region_id, page_number=1):
data, page_size = [], 20
params = dict(
Action='DescribeInstances',
Region=region_id,
Offset=(page_number - 1) * page_size,
Limit=page_size
)
res = make_tencent_request(ak, ac, 'cvm.tencentcloudapi.com', params)
if 'InstanceSet' not in res['Response']:
raise Exception(res)
for item in res['Response']['InstanceSet']:
data_disks = list(map(lambda x: x['DiskSize'], item['DataDisks']))
internet_charge_type = item['InternetAccessible']['InternetChargeType']
data.append(dict(
instance_id=item['InstanceId'],
os_name=item['OsName'],
os_type=check_os_type(item['OsName']),
cpu=item['CPU'],
memory=item['Memory'],
disk=[item['SystemDisk']['DiskSize']] + data_disks,
created_time=parse_utc_date(item['CreatedTime']),
expired_time=parse_utc_date(item['ExpiredTime']),
instance_charge_type=check_instance_charge_type(item['InstanceChargeType'], 'tencent'),
internet_charge_type=check_internet_charge_type(internet_charge_type, 'tencent'),
public_ip_address=item['PublicIpAddresses'],
private_ip_address=item['PrivateIpAddresses'],
zone_id=item['Placement']['Zone']
))
if len(res['Response']['InstanceSet']) == page_size:
page_number += 1
new_data = fetch_tencent_instances(ak, ac, region_id, page_number)
data.extend(new_data)
return data

View File

@ -10,7 +10,6 @@ from apps.host.models import Host, Group
from apps.app.models import Deploy
from apps.schedule.models import Task
from apps.monitor.models import Detection
from apps.account.models import Role
from libs.ssh import SSH, AuthenticationException
from paramiko.ssh_exception import BadAuthenticationType
from libs import AttrDict
@ -25,7 +24,7 @@ class HostView(View):
if not request.user.has_host_perm(host_id):
return json_response(error='无权访问该主机,请联系管理员')
return json_response(Host.objects.get(pk=host_id))
hosts = {x.id: x.to_view() for x in Host.objects.all()}
hosts = {x.id: x.to_view() for x in Host.objects.select_related('hostextend').all()}
for rel in Group.hosts.through.objects.all():
hosts[rel.host_id]['group_ids'].append(rel.group_id)
return json_response(list(hosts.values()))
@ -91,10 +90,8 @@ class HostView(View):
detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first()
if detection:
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
role = Role.objects.filter(host_perms__regex=fr'[^0-9]{form.id}[^0-9]').first()
if role:
return json_response(error=f'角色【{role.name}】的主机权限关联了该主机,请解除关联后再尝试删除该主机')
Host.objects.filter(pk=form.id).delete()
print('pk: ', form.id)
return json_response(error=error)

60
spug_api/libs/helper.py Normal file
View File

@ -0,0 +1,60 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from urllib.parse import quote, urlencode
from datetime import datetime
from pytz import timezone
import requests
import hashlib
import base64
import random
import time
import hmac
import uuid
def _special_url_encode(value) -> str:
if isinstance(value, (str, bytes)):
rst = quote(value)
else:
rst = urlencode(value)
return rst.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
def _make_ali_signature(key: str, params: dict) -> bytes:
sorted_str = _special_url_encode(dict(sorted(params.items())))
sign_str = 'GET&%2F&' + _special_url_encode(sorted_str)
sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest()
return base64.encodebytes(sign_digest).strip()
def _make_tencent_signature(endpoint: str, key: str, params: dict) -> bytes:
sorted_str = '&'.join(f'{k}={v}' for k, v in sorted(params.items()))
sign_str = f'POST{endpoint}/?{sorted_str}'
sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest()
return base64.encodebytes(sign_digest).strip()
def make_ali_request(ak, ac, endpoint, params):
params.update(
AccessKeyId=ak,
Format='JSON',
SignatureMethod='HMAC-SHA1',
SignatureNonce=uuid.uuid4().hex,
SignatureVersion='1.0',
Timestamp=datetime.now(tz=timezone('UTC')).strftime('%Y-%m-%dT%H:%M:%SZ'),
Version='2014-05-26'
)
params['Signature'] = _make_ali_signature(ac + '&', params)
return requests.get(endpoint, params).json()
def make_tencent_request(ak, ac, endpoint, params):
params.update(
Nonce=int(random.random() * 10000),
SecretId=ak,
Timestamp=int(time.time()),
Version='2017-03-12'
)
params['Signature'] = _make_tencent_signature(endpoint, ac, params)
return requests.post(f'https://{endpoint}', data=params).json()

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, Button, Steps, Cascader, message } from 'antd';
import http from 'libs/http';
import store from './store';
import styles from './index.module.less';
export default observer(function () {
const [loading, setLoading] = useState(false);
const [step, setStep] = useState(0);
const [ak, setAK] = useState();
const [ac, setAC] = useState();
const [regionId, setRegionId] = useState();
const [groupId, setGroupId] = useState([]);
const [regions, setRegions] = useState([]);
function handleSubmit() {
setLoading(true);
const formData = {ak, ac, type: store.cloudImport, region_id: regionId, group_id: groupId[groupId.length - 1]};
http.post('/api/host/import/cloud/', formData, {timeout: 120000})
.then(res => {
message.success(`已同步/导入 ${res} 台主机`);
store.cloudImport = null;
store.fetchRecords()
}, () => setLoading(false))
}
function fetchRegions() {
setLoading(true);
http.get('/api/host/import/region/', {params: {ak, ac, type: store.cloudImport}})
.then(res => {
setRegions(res)
setStep(1)
})
.finally(() => setLoading(false))
}
return (
<Modal
visible
maskClosable={false}
title="批量导入"
footer={null}
onCancel={() => store.cloudImport = null}>
<Steps current={step} className={styles.steps}>
<Steps.Step key={0} title="访问凭据"/>
<Steps.Step key={1} title="导入确认"/>
</Steps>
<Form labelCol={{span: 8}} wrapperCol={{span: 14}}>
<Form.Item hidden={step === 1} required label="AccessKey ID">
<Input value={ak} onChange={e => setAK(e.target.value)} placeholder="请输入"/>
</Form.Item>
<Form.Item hidden={step === 1} required label="AccessKey Secret">
<Input value={ac} onChange={e => setAC(e.target.value)} placeholder="请输入"/>
</Form.Item>
<Form.Item hidden={step === 0} required label="选择区域">
<Select placeholder="请选择" value={regionId} onChange={setRegionId}>
{regions.map(item => (
<Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item hidden={step === 0} required label="选择分组">
<Cascader
value={groupId}
onChange={setGroupId}
options={store.treeData}
fieldNames={{label: 'title'}}
placeholder="请选择"/>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 8}}>
{step === 0 ? (
<Button type="primary" loading={loading} disabled={!ak || !ac} onClick={fetchRegions}>下一步</Button>
) : ([
<Button
key="1"
type="primary"
loading={loading}
disabled={!regionId || !groupId}
onClick={handleSubmit}>同步导入</Button>,
<Button key="2" style={{marginLeft: 24}} onClick={() => setStep(0)}>上一步</Button>
])}
</Form.Item>
</Form>
</Modal>
);
})

View File

@ -20,13 +20,35 @@ export default observer(function () {
<Descriptions.Item label="独立密钥">{host.pkey ? '是' : '否'}</Descriptions.Item>
<Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
<Descriptions.Item label="所属分组">
<List >
<List>
{group_ids.map(g_id => (
<List.Item key={g_id} style={{padding: '6px 0'}}>{store.groups[g_id]}</List.Item>
))}
</List>
</Descriptions.Item>
</Descriptions>
{host.id ? (
<Descriptions
bordered
size="small"
column={1}
style={{marginTop: 24}}
title={<span style={{fontWeight: 500}}>扩展信息</span>}>
<Descriptions.Item label="实例ID">{host.instance_id}</Descriptions.Item>
<Descriptions.Item label="操作系统">{host.os_name}</Descriptions.Item>
<Descriptions.Item label="CPU">{host.cpu}</Descriptions.Item>
<Descriptions.Item label="内存">{host.memory}GB</Descriptions.Item>
<Descriptions.Item label="磁盘">{host.disk.map(x => `${x}GB`).join(', ')}</Descriptions.Item>
<Descriptions.Item label="内网IP">{host.private_ip_address.join(', ')}</Descriptions.Item>
<Descriptions.Item label="公网IP">{host.public_ip_address.join(', ')}</Descriptions.Item>
<Descriptions.Item label="实例付费方式">{host.instance_charge_type_alias}</Descriptions.Item>
<Descriptions.Item label="网络付费方式">{host.internet_charge_type_alisa}</Descriptions.Item>
<Descriptions.Item label="创建时间">{host.created_time}</Descriptions.Item>
<Descriptions.Item label="到期时间">{host.expired_time || 'N/A'}</Descriptions.Item>
<Descriptions.Item label="更新时间">{host.updated_at}</Descriptions.Item>
</Descriptions>
) : null}
</Drawer>
)
})

View File

@ -124,7 +124,7 @@ export default observer(function () {
<Input addonBefore="-p" placeholder="端口"/>
</Form.Item>
</Form.Item>
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥则优先使用该密钥。">
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥(私钥)则优先使用该密钥。">
<Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={handleUpload}
onChange={handleUploadChange}>
{fileList.length === 0 ? <Button loading={uploading} icon={<UploadOutlined/>}>点击上传</Button> : null}

View File

@ -5,11 +5,12 @@
*/
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { Table, Modal, message } from 'antd';
import { PlusOutlined, ImportOutlined } from '@ant-design/icons';
import { Action, TableCard, AuthButton } from 'components';
import { Table, Modal, Dropdown, Button, Menu, Avatar, Tooltip, Space, Tag, message } from 'antd';
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
import { Action, TableCard, AuthButton, AuthFragment } from 'components';
import { http, hasPermission } from 'libs';
import store from './store';
import icons from './icons';
function ComTable() {
useEffect(() => {
@ -30,6 +31,24 @@ function ComTable() {
})
}
function handleImport(menu) {
if (menu.key === 'excel') {
store.importVisible = true
} else {
store.cloudImport = menu.key
}
}
function IpAddress(props) {
if (props.ip && props.ip.length > 0) {
return (
<div>{props.ip[0]}<span style={{color: '#999'}}>{props.isPublic ? '公' : '私有'}</span></div>
)
} else {
return null
}
}
return (
<TableCard
rowKey="id"
@ -43,11 +62,32 @@ function ComTable() {
type="primary"
icon={<PlusOutlined/>}
onClick={() => store.showForm()}>新建</AuthButton>,
<AuthButton
auth="host.host.add"
type="primary"
icon={<ImportOutlined/>}
onClick={() => store.importVisible = true}>批量导入</AuthButton>
<AuthFragment auth="host.host.add">
<Dropdown overlay={(
<Menu onClick={handleImport}>
<Menu.Item key="excel">
<Space>
<Avatar shape="square" size={20} src={icons.excel}/>
<span>Excel</span>
</Space>
</Menu.Item>
<Menu.Item key="ali">
<Space>
<Avatar shape="square" size={20} src={icons.alibaba}/>
<span>阿里云</span>
</Space>
</Menu.Item>
<Menu.Item key="tencent">
<Space>
<Avatar shape="square" size={20} src={icons.tencent}/>
<span>腾讯云</span>
</Space>
</Menu.Item>
</Menu>
)}>
<Button type="primary">批量导入 <DownOutlined/></Button>
</Dropdown>
</AuthFragment>
]}
pagination={{
showSizeChanger: true,
@ -61,14 +101,30 @@ function ComTable() {
title="主机名称"
render={info => <Action.Button onClick={() => store.showDetail(info)}>{info.name}</Action.Button>}
sorter={(a, b) => a.name.localeCompare(b.name)}/>
<Table.Column title="连接地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/>
<Table.Column hide width={100} title="端口" dataIndex="port"/>
<Table.Column title="备注信息" dataIndex="desc"/>
<Table.Column title="IP地址" render={info => (
<div>
<IpAddress ip={info.public_ip_address} isPublic/>
<IpAddress ip={info.private_ip_address}/>
</div>
)}/>
<Table.Column title="配置信息" render={info => (
<Space>
<Tooltip title={info.os_name}>
<Avatar shape="square" size={16} src={icons[info.os_type]}/>
</Tooltip>
<span>{info.cpu} {info.memory}GB</span>
</Space>
)}/>
<Table.Column hide title="备注信息" dataIndex="desc"/>
<Table.Column
title="状态"
dataIndex="is_verified"
render={v => v ? <Tag color="green">已验证</Tag> : <Tag color="orange"></Tag>}/>
{hasPermission('host.host.edit|host.host.del|host.host.console') && (
<Table.Column width={160} title="操作" render={info => (
<Action>
<Action.Button auth="host.host.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
<Action.Button auth="host.host.del" onClick={() => handleDelete(info)}>删除</Action.Button>
<Action.Button danger auth="host.host.del" onClick={() => handleDelete(info)}>删除</Action.Button>
</Action>
)}/>
)}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,23 @@
import iconExcel from './excel.png';
import iconCentos from './centos.png';
import iconAlibaba from './alibaba.png';
import iconCoreos from './coreos.png';
import iconDebian from './debian.png';
import iconFreebsd from './freebsd.png';
import iconSuse from './suse.png';
import iconTencent from './tencent.png';
import iconUbuntu from './ubuntu.png';
import iconWindows from './windows.png';
export default {
excel: iconExcel,
alibaba: iconAlibaba,
centos: iconCentos,
coreos: iconCoreos,
debian: iconDebian,
freebsd: iconFreebsd,
suse: iconSuse,
tencent: iconTencent,
ubuntu: iconUbuntu,
windows: iconWindows,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -12,6 +12,7 @@ import Group from './Group';
import ComTable from './Table';
import ComForm from './Form';
import ComImport from './Import';
import CloudImport from './CloudImport';
import Detail from './Detail';
import Selector from './Selector';
import store from './store';
@ -40,6 +41,7 @@ export default observer(function () {
<Detail/>
{store.formVisible && <ComForm/>}
{store.importVisible && <ComImport/>}
{store.cloudImport && <CloudImport/>}
{store.selectorVisible &&
<Selector oneGroup={!store.addByCopy} onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
</AuthDiv>

View File

@ -0,0 +1,4 @@
.steps {
width: 350px;
margin: 0 auto 30px;
}

View File

@ -20,6 +20,7 @@ class Store {
@observable isFetching = false;
@observable formVisible = false;
@observable importVisible = false;
@observable cloudImport = null;
@observable detailVisible = false;
@observable selectorVisible = false;