From e9c0a042185f93f489215539f5b24e54946c6836 Mon Sep 17 00:00:00 2001 From: vapao Date: Mon, 12 Apr 2021 22:33:10 +0800 Subject: [PATCH] upgrade host module --- spug_api/apps/host/add.py | 59 ++++++++ spug_api/apps/host/models.py | 17 ++- spug_api/apps/host/urls.py | 3 + spug_api/apps/host/utils.py | 174 ++++++++++++++++++++++ spug_api/apps/host/views.py | 7 +- spug_api/libs/helper.py | 60 ++++++++ spug_web/src/pages/host/CloudImport.js | 92 ++++++++++++ spug_web/src/pages/host/Detail.js | 24 ++- spug_web/src/pages/host/Form.js | 2 +- spug_web/src/pages/host/Table.js | 80 ++++++++-- spug_web/src/pages/host/icons/alibaba.png | Bin 0 -> 4045 bytes spug_web/src/pages/host/icons/centos.png | Bin 0 -> 2591 bytes spug_web/src/pages/host/icons/coreos.png | Bin 0 -> 2059 bytes spug_web/src/pages/host/icons/debian.png | Bin 0 -> 2100 bytes spug_web/src/pages/host/icons/excel.png | Bin 0 -> 4222 bytes spug_web/src/pages/host/icons/freebsd.png | Bin 0 -> 1962 bytes spug_web/src/pages/host/icons/index.js | 23 +++ spug_web/src/pages/host/icons/suse.png | Bin 0 -> 2394 bytes spug_web/src/pages/host/icons/tencent.png | Bin 0 -> 4851 bytes spug_web/src/pages/host/icons/ubuntu.png | Bin 0 -> 2212 bytes spug_web/src/pages/host/icons/windows.png | Bin 0 -> 1443 bytes spug_web/src/pages/host/index.js | 2 + spug_web/src/pages/host/index.module.less | 4 + spug_web/src/pages/host/store.js | 1 + 24 files changed, 527 insertions(+), 21 deletions(-) create mode 100644 spug_api/apps/host/add.py create mode 100644 spug_api/apps/host/utils.py create mode 100644 spug_api/libs/helper.py create mode 100644 spug_web/src/pages/host/CloudImport.js create mode 100644 spug_web/src/pages/host/icons/alibaba.png create mode 100644 spug_web/src/pages/host/icons/centos.png create mode 100644 spug_web/src/pages/host/icons/coreos.png create mode 100644 spug_web/src/pages/host/icons/debian.png create mode 100644 spug_web/src/pages/host/icons/excel.png create mode 100644 spug_web/src/pages/host/icons/freebsd.png create mode 100644 spug_web/src/pages/host/icons/index.js create mode 100644 spug_web/src/pages/host/icons/suse.png create mode 100644 spug_web/src/pages/host/icons/tencent.png create mode 100644 spug_web/src/pages/host/icons/ubuntu.png create mode 100644 spug_web/src/pages/host/icons/windows.png create mode 100644 spug_web/src/pages/host/index.module.less diff --git a/spug_api/apps/host/add.py b/spug_api/apps/host/add.py new file mode 100644 index 0000000..0a80e22 --- /dev/null +++ b/spug_api/apps/host/add.py @@ -0,0 +1,59 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# 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) diff --git a/spug_api/apps/host/models.py b/spug_api/apps/host/models.py index 8276939..440cf37 100644 --- a/spug_api/apps/host/models.py +++ b/spug_api/apps/host/models.py @@ -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' diff --git a/spug_api/apps/host/urls.py b/spug_api/apps/host/urls.py index 71dd57a..5aecd4f 100644 --- a/spug_api/apps/host/urls.py +++ b/spug_api/apps/host/urls.py @@ -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), ] diff --git a/spug_api/apps/host/utils.py b/spug_api/apps/host/utils.py new file mode 100644 index 0000000..7d32af3 --- /dev/null +++ b/spug_api/apps/host/utils.py @@ -0,0 +1,174 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# 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 diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index bc9977b..b3f3fe4 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -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) diff --git a/spug_api/libs/helper.py b/spug_api/libs/helper.py new file mode 100644 index 0000000..dbd60f1 --- /dev/null +++ b/spug_api/libs/helper.py @@ -0,0 +1,60 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# 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() diff --git a/spug_web/src/pages/host/CloudImport.js b/spug_web/src/pages/host/CloudImport.js new file mode 100644 index 0000000..f0eb7e0 --- /dev/null +++ b/spug_web/src/pages/host/CloudImport.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( + store.cloudImport = null}> + + + + +
+ + + + + + {step === 0 ? ( + + ) : ([ + , + + ])} + +
+
+ ); +}) diff --git a/spug_web/src/pages/host/Detail.js b/spug_web/src/pages/host/Detail.js index 86bc3e0..cad09e7 100644 --- a/spug_web/src/pages/host/Detail.js +++ b/spug_web/src/pages/host/Detail.js @@ -20,13 +20,35 @@ export default observer(function () { {host.pkey ? '是' : '否'} {host.desc} - + {group_ids.map(g_id => ( {store.groups[g_id]} ))} + {host.id ? ( + 扩展信息}> + {host.instance_id} + {host.os_name} + {host.cpu}核 + {host.memory}GB + {host.disk.map(x => `${x}GB`).join(', ')} + {host.private_ip_address.join(', ')} + {host.public_ip_address.join(', ')} + {host.instance_charge_type_alias} + {host.internet_charge_type_alisa} + {host.created_time} + {host.expired_time || 'N/A'} + {host.updated_at} + + ) : null} + ) }) \ No newline at end of file diff --git a/spug_web/src/pages/host/Form.js b/spug_web/src/pages/host/Form.js index 886c01a..eeb9f1b 100644 --- a/spug_web/src/pages/host/Form.js +++ b/spug_web/src/pages/host/Form.js @@ -124,7 +124,7 @@ export default observer(function () { - + {fileList.length === 0 ? : null} diff --git a/spug_web/src/pages/host/Table.js b/spug_web/src/pages/host/Table.js index 3fae7f7..c601827 100644 --- a/spug_web/src/pages/host/Table.js +++ b/spug_web/src/pages/host/Table.js @@ -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 ( +
{props.ip[0]}({props.isPublic ? '公' : '私有'})
+ ) + } else { + return null + } + } + return ( } onClick={() => store.showForm()}>新建, - } - onClick={() => store.importVisible = true}>批量导入 + + + + + + Excel + + + + + + 阿里云 + + + + + + 腾讯云 + + + + )}> + + + ]} pagination={{ showSizeChanger: true, @@ -61,14 +101,30 @@ function ComTable() { title="主机名称" render={info => store.showDetail(info)}>{info.name}} sorter={(a, b) => a.name.localeCompare(b.name)}/> - a.name.localeCompare(b.name)}/> - - + ( +
+ + +
+ )}/> + ( + + + + + {info.cpu}核 {info.memory}GB + + )}/> + + v ? 已验证 : 未验证}/> {hasPermission('host.host.edit|host.host.del|host.host.console') && ( ( store.showForm(info)}>编辑 - handleDelete(info)}>删除 + handleDelete(info)}>删除 )}/> )} diff --git a/spug_web/src/pages/host/icons/alibaba.png b/spug_web/src/pages/host/icons/alibaba.png new file mode 100644 index 0000000000000000000000000000000000000000..04818397cee53b3f38b40c5d10f99c30625c573e GIT binary patch literal 4045 zcmb7Hc{r5)*B@()EMS(s_}!1+6tG(#6`Zh!<-4G1mK9HP4o+5>|D$j~qgfQjxTfZ>Qjy8v{j0ZhLz z0ANHX_y?QQL4R}T0e~Ag0ON0t9nBssL)xZg|F#STbpI+A(Enk(6)^mZkJ3I|uSL=f zYoMWRFaW^DeYEKS_jCDaYPL8_8-fkSM8h?}Pu|5X;Ig}XxL@E=7C; zKVSb~jc_>R7ej-FkHRnr=of|H1BcjP%s{#Ucz2Mhyo$U6L>mYKfwb^$9vbE-{om;{ z4-WAn5CSz|u&}T&`7mYq0K6wmQC(dfrl15 zj|(K=0{lTox-ORkLI`jO;=#qM8I$-8SHzJ`>0*_hT~rP2>sV zLtIw_*ym+&M!BEo;bo6CRO5yB{%pRYQl<%`mm=@K=X=uPjH~q@&}p=Jo5J?2*$B>^ zQEbzpB#1scS!ZN8Slj9Mdf>S{c458M!Li3&+D$5{IZAsYzqWA_*BeHaxRWEk9}kO{ zmbtGn1*>}?wy1X3dMKFWcNIJ=mQ}c9+%wU)?UEC)Bk4m`ym2#CXso<@6P4HkQFxFa z&|TDXu5`fmET^$meO-@BJr%9=w_xUK)&pwIpaf;Y^sR-j3crqPgVT20D*sSWPfX90 zy`?GZ1^A-RB=#>d<7%>FenoAwno@)WS?%MyppsElUxdQ@AY|`FXwAvJ4I`UYo!(_T zkK1?)k^*PaIrFkRbUnE(@VqL@9Q_{9-2pvo4i#EP8~S(*Ki0$5$@}$1%x7gR``JS& zp1Af^U<*j0^*P7GFHwPSUfEP%LG-g2TjM9o=c}yB-mf_D zG0TdgTO(B1jmeJ4$d+L+ie(qR#9ZHB4F6k~t`i$i@Ey;MpT{qGYmzRDJUn@m%KkaG z$(mO>^?2__G-*&%X<;?GK`>7SYh#O$yP~PIJSQBm42e5#>1N_$cdVwJbt6m0H;IJZ zX=GBikqQbEMZx><;;*3QG96jzu~`ykib!eZi7c+iZMyeddvB4>UgL2a;mD6`ocU^0 zHU@XfCR#o8S{fPkCdGUSWsC-xMm)A)@m~Xf)_BE3s_JG+B_GD<6@m?}$n!Y|fQjWkMxPqV7$mN=Oot zH&CA<4V_QQYOv3qEyyQv2ewLt$jW|L&VkQ-={=6BoZvpQgWtlwA3Q5%J-)1gwxVF( z9e^z0^iYy-ZL0aYl(cF84F#T=ke%Y9`V5tTfS2<2{ocR`*U^X&e5K?2DtNCHG4@$$ z*BYg?`FQ>#%Wi%f3wMecIVQ!(+yUct9(DXTl(J+GiHf+snPG1pCqhf62lsfF}KoG4g!EdC5@W1GM$T z3kOI2hQ%{d7PVnbYGC|hj(b2#^9zsDmw7t|FjY^smZT>JttR-YgYIqdJnhuhD>_vH z(S&9Bpq=JEu5(Pr@xV&Qg-Zj^^L@M0vygumR%CRYF;aAH(!md5!F(YkM4vT9+@`&& zIDc_8wT|q=f38#sqWGq>rp5CEN2ha0W3^_p@JLGZeXV7I!L(-$WUaJ|*Jk0pyo(Ij z{x|GW9tA_)W9U7;D4%acN=L}4xahZvha`A5`mMq(Bs4*=7NFJw>MWh!+ZA~CXSI*-cn+57`>}chr}sc$bz%p@ry|8mQ-cG7W9#NwF_n6Szd5l9uR&W z__x#i398I0@(srS%xf=Bf95!;B+1A#>vP(NpNupe>{)wHkwV;=HOL*V#|z>1-&3$a z_udJy!6csePj){h7izwkB$}wfD&C!*KbT9q(UWyZMG>X;4i>4sVcwu8fihpgPINO= z*^1X9@;cF_9c4nuzI5}aN>OpwSQgWs@UsdiAvf|zKcg@^v#N7Vx0|bf7C$MUvJMPI z>*^-<1V-;oatAgv#i`rFq7>$%GBkLIL@<%?s8#gvhXyA$VCc^FnphvlV;3$%c<0Mo zi0D+$((X78_``G8Uh5)Qwoa}nSIoHXDLIz}An>AjMo2^_|LKj0cQJjtL44H=n!)!H z;<)t9g;{u+L7UspzCxGwscr#=U;VnY5_i_*<2YJ-q41qRu9J@+eU2F}NWH!r;V85~ zXxtJTeRqBDa@gc3p8XWqyl*cV(i(gBb{~fmqImjyDrhRp5Yd^I5SRAZr+}?M(Rmme zJfWgel(*4l>b|5a{HP#KGrRd+AlF|t9*>e*O%Ad9OgJo^yls5WK5{UE}RD^EFL-0kWBIyWDaekv)2MH`Q2k?7YOK)gQL9;aOu! zror|d#~v9yYs}Q0|NIT-=6$NU1vWmObDbyMMfQUrZ!5-ju8l~!)^@dezM#hzSCuHb zG1xPtK{(00)y4nuo(JDjB9(KzP}~8>r`++L?M=q6jME0_QsPYU@w9pOOHHS)`bK9` z4clM0?BtnnXW`i5wMdM~Rgcmho-0(B{vK+pR#iG-u>)8Z_>q)77*O}Kt{!Aud%mh7 zx@j$W%3XsVA50MF8&M6Y_R==~7=9|`^yi#u82zf9SGGZANB>+)lIO557nd;jPO1~y*mh=E zut_~vRigg8c^;!enoGu@zzOBP?QXR!JegI~a~Lo5H8#>y$d&wV#CUlvT&Uq4*#ik{$=e8hzvYrT%!E4s7eCwpslfrkPX2JrO|<(Y8JTZD zwQFE8F{2pod}pt3&kXLmlFYbv&m*E&1P5g3R9yNF(FE=fpeVIvz26wgOatQaI_c;K z7o+;7UyuiLCoBxB5yh>=mJfbB>%+d1iHVP;zE3aS6XaM_#Ygygmgt#2tG(*lN+-8k z!*GueK3AL~U392_^Jbh0`$%E;Qk#$g`(WX^soAIDszdQgz1YYMqp7I4{^=UvNTKzd z@Hyud7oe^#Uwh6nxXD3kk$#U3SXB_9z#6W1Fh68?ySUM-is*3ZrewnO?T+$sj$Beg zlOxhac6f2NNnRwTGCp;3q(LpH)$wgAgNi}4^4u;N+cmZ;^cJe26(Q>NncM#$);T@> z{SE&XRnJSj76BL)Xz3e}wW(I28`xA?c4-1r6A+r;|1q@R>DeV81=U-JjaQg;_Zq}p ztsO1N1X7h=&yZkL`nS_{F;>VaA-%@T)~_};QyF99&45)Gs5%SR%}ArWXSFN-#<>{Z z>u)W~^YLW;^2xy>Iv=na?E^+LlI}@@U4=477`D4JLbjdHZQ<6yr6u|l)~3k=G@aOm;?>w4%CC$TeZ_& z2T4;IU?xw*&;>01c`3N5>p<^g)baB?HpkSxF)H?0CrODGNE=;N{Z1`3g^F5rNsGB< zQSuUpS7m$0utou_SpIYntpvu!5ZHI19};%UF_zu5|81j1mPB1r`&yT>gQIQ8aa(Xv z!;7h@gZS|Nl)YCoc$tzz_-m_vVIG^_oy{p~=pMZ&awa1`V_z`Y$%v(C#6MT3ax27G z9Zdl0$!Q|Q=Vlamo9REc8Zd1l6E=2GiLyF7>twm=pzdJZyF6)qk8eW;08wS3H{x#=~ zvyWf9T0)*BHxcq8CB+RxneanDoJ)NEVmT*XW;&Hjrj+$OeAfyjI)pXT*Y zc`&#hx^eYERY%CvbHT>4!#^`s4R%+5Vti{aW3LbKVG5iTZ~E|^-|XIKTW@A3vAbT1 zUUqz)tehMI^vGS?v7BJ!Y>wHV=UKb?-;6GQ=A7r#x1S%v0vdADNRgJ11cF9G2#Oj@%0(iQ#N;XnW1)zEAfiO< zGZ9g+FrrVNhy_#-rAZOX6A?7FQPC(;4RfR7dp}(B!u0j1=?6T zSfWrU8(t7wh|F^HhaQ2@KyN4lnJiW^`Aih5ILm54G7{m3X@!9-R8gJND->#kRv`+9 z!})U=GF7yvM6QxTp0Uwt1cgF*`^BmyvM2}!q|i!*(g)jFdKC*O0V)jyP&_F(8Au=lRJtdDLLk!!vj8GUAmKq0oBq|N0OT54c8b}(_^D}uWkp$AXWD=X|#UT(lR62*r zqS0ssFDjElpt0yfvOJ|0mMCS=kZlEG`@1ahA7vQ<8b|`GG$NI1^-vWEB2}vzQ z0Rey;Um{Z|&Ahw$j=qJ=hBS(GkesVgMFU@x%uxKq0U1#a(j!15S}BEsBa_KQ9F5{d z#u4NM5)GnANpzwV`(0lC|0WNQIE6P?%Kuc(&=yjT=H-t*K$st$1SygB(I98zOHkfz64hAi`a9&Oxd;5oTb2~oVDBXPa%HufXL=?LF z8(^t;zp*5|yZ7}p*O+(5um@^q?!>G|j!b=@_Tm#i8alspy)gC~@)~>$hEB!G!>3Nx z_I~UsZ03JB%jE@SeeVCO;r`~{$HjWzZOa$$GhSS@Y2%+2+e*8tlYg%#b)Fy;E!tmn zFTAuBZoJUkaOAe3<;B3;rdOXD@BLMjtGiP3eD`j*4ehmeKVFt9#RuwY+bf@@cQ@v? zzAI^I-EJ&hzASFm8zch&ESwjLGJULmwP#S^Z;Gwo`p8r?9&Iwcd^zx?N%3Ww@NQ3+ z<<3mOk1s!rU-B@>ZuApd7t4_$5AH5*^-Y>0@@I-W-42IEl`e_CI+**Q zWZK^9y>e--$%OItUp9T0)AZ{W%m(uFq{b$c+tR`SJKZV4)0g$=#gOB{F`)~BNnRI1 zipD!a9{bbEbtSr;nP+lvwYIm7vmLEcLQfQD#_t14UcATR(3lo3~LfZyw>-OOOfA7eS_yZlf>TR39^@u>*e6-YIOtSm+&yLw4 z^`JO4zsF_Qvb*4csyfBQmbR9iQ{TVb_DI3i`xe_xl1{!-buU}6Y{L$Fu~D{ek@foI zr{Q~_Vd{&EN5^n>1Qtxc{m|7hBxBXd9^j;N)}6BxEkZIkwd8jsq{?(2V<%9J1AGEH zJJ@%8_syA~`UaeWCR(Lsw#F-MHMewPS1)pYmJ8f=5u@{VrJSD;rRhwQ0;$X@=P=C< zbWU8p+hIN<(LsER>3t8hGzlm>M5nQ>n z(YuVA=lVK5Q9e0Y{%B!c(-cDXTT{{{hpUY)d$Dit@4<{ppy#qO*E&9U*Dajp9Ft#p z#|!O5vs*Q=CJ7La)7g8xLK}rGZSD5uJEn;wm1ELDLzsuUE~B14ii19?zMgPrYJA~I zW{|EPle(<|0xd$4+gW?AZF2ET5$U;A*|T%Y$DL_zo~WRWW6(No-#k4=g6>MiC<6d_ zSg14!OaCQ_;qQ}be0*~ppOqz6~m(3GO3O%Z3EI(4YujAAa?l8{e$Cea_*EI`i zo+}E1vWfzSLd}S+v1jX#59pHA zNje?Z;$OdBE%PN#y4K{AnyD)jic`sM!`pMhcqwu>CqicU8ladtrm#GU5Xe~QxdqdF z`e>^I*!{+T{L|qpZqM{Bs@IsRJ-cif{VmMH?+dDo2cE$p~E zwW5oYDEE>b!&b8?m7{;r94dsP-tjJv3P)2+nVDfbvCc=uvvXXk_BBS81J#0TYeVGJ z!-kq+-TLbnmQQ0`%fatq+1#&jy4!XlUGXyY_yO^W%8k_imiG7VVRixD4(Q|HQ@630 z`R{Wkcl6IlNsCOE!kX-GrnObEog@}u=_|{~t8WMj8lXIAY_fYgVUD#$L(cv8tb#;` z$=7r)dQpD+RsjV5TB(;J?eaMvYcoIDyCP))O27`Oo4v(0(SD3;7^7lbwHmX#BTMYR z_DvdSFz~C6IW$)us+#j##Ux+x`3{zKp5SH{zan7|7jr|XzCldK$i45_+0lzL5>xdn z&{8`NJdz3Kfvg1+E4Tuev4z;~>Q4*&l{QV~2hsftZF$c>Juwd7U5obcQ*tb@zd!QQ zpWCwW8E(@NFFoxBdv88N#h? z+}-DTyP(Fv7TW7B$5-^g0u>?aBE82<nFSEN(@yT3NQT1(hmQ=tHd7XpzdQK((ty@L(2Kpa-N`M0TQL`-jJ~XXnh!?=j!K z-@W&DzbTL>B}}Kzr&1`C>C)9=1v#p{&$KDzJ0Tj=kb_UUNG76CPC5ObAim^!l0}iQ zic;J9rkg^UV$rEmh!k0(0L2YVM2)9mOq;<(q9~N;Sepq!^%w!9Vd*-fkk;S$4Gqw# zg|zit8B1o0!?e294l|~7B&kq`9_6cPu`xijO+Y#@U<3l#44FoYz$T_3q|svKJ|ASUK{lH~A{Z9Ckw9z=qlG@< zL5x{Yv(7~5a3kRLh@|0GLP#SceV&5BB$K^#Y_z;66j?IRhL}Kz$pQ@qZ(h&P7D9pj z7RD>l7M0zEfeOrmTg@m&!s!#nWbIy0K zaMXMrjIg*2E}RB4BDq|c!RN9y3=Yg=@sTK&hR5Pe_<5CmWF!xcf?$pqj*>u-1m;Ub zt9U#f6a|a85N{QK!d7au5Qq`QCb)GZ_X}J0Yi$K_W(*;4vkJ#EC%Qnc#R=S^#Z5q5 z91tc$P@U1c4)>nX^OVJyS+^Ba$D45j@T|xJ-75n4-;#fBul|3F2a-X7-cI>l=S*yo z?dYAp)B(A8sU*xu%EwIVq;V=MnABJZAxR;WxQ56^%oydl9Ha&$o$LujZO^)lMU$ZQ z@6rUo~VD? zKR8@gF?RRfe=c8LHyHUDNfAIOl9ZHkyRuz?I44<13uhWq|wbe5Lhq_YGI|qkmS68WU6vPWpkx=Ncl`vO}6jiMa~e z{(_CkG<0TuV9n$^N3@)!dqo#q1z#*ma-G534P(P__+;9~cZ2@mu0FM?F{Cs;Le!a7 zb){S?pSePpm$SQV&fScdLr>0sud2!~TKc#5#}bxA?K#zQb2%Ogf7G2J3s_c2t0|cwP20O3d|iFM@@nV7PodHG?2Gz>TI5>K9cQwh+H*f3F$+3!rTqIN zbGp)QNSZ2nA4n_cwT!cWzL<=D*U}6K*vHg;W3-Cdd%~$PlG@_Mk2W0d5)H}Gse9Zb z8t|j)j6BZ)YFSOf`Wf}Cea%!jnGT8H054VP6{Yo3mDml0KFZvCSJ8pY+DIU`1uu6=V!Z!a4Us^cQFISjm3Whh0 z+!s&NOuN5s3pDM(*2^`A`ik;qIRkAn;oz8xX}y^e=GV0KuS>oWY!_WgMSYiCi$CNn z9)o>~nsesQYJF!a)~AbQCoS2P8Nzxuf3!cR=?phiW$Y{Y#iuvok|661cW6n(A+TU0 zTQ_@C1CiCP!Mr3`)u*;qwW#4tJyE>{qac5>8XwMy%*3tqcx%M)<8OZ(Y=Oq znyz?{JKelngZMFa2Op#QgmdMoA@4M)@*_6KP47hjn6j@w@a_s)4|Q+qQDAJ`=bWul zpRzjbl5%1Ck#0-8UtG!OZ)iNb+KO6}&t?OK)uAKdLkRXkj2oZZv*TXOXJWO>tjSqk z0L~X?*aJ!+`N_4=O!JPF2J8LDqfkeb{o~UQu7mbGL!)P@f>_wJOZE1J zlMSVXUw)b}scMraSIu~|;|I8%IMeM9eVu4%Y@Me+Sk<7f+kB&;y8O~cv?2Iu?)kYt z-L3d`bR_G;S;s$K_DN=L{Tg|_WkFd(WN!igqBB zwQGJp*WMdiyd`MmHJG<^=Jug2Zs+0Dv77Wd$`4&nC((a)-bi0|GKo|!MJh=W*RI-< F`(Jg6Iy?XX literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/icons/debian.png b/spug_web/src/pages/host/icons/debian.png new file mode 100644 index 0000000000000000000000000000000000000000..51614f04297e07da6f6ca1a5682678cf95ecaf8f GIT binary patch literal 2100 zcmbVMdsq`!77qeNgRm$Sf!aC-pU`A733)Ezl>~@QfP_@gpg2i}B+X-DGC)FG@DVAk zumYAUtGGq?b46YS1gX?g5GxR?3(KM?2(%V!*UH)!{B~twCn|RT@awm`^Uch?=icA> zoyR@rzMq^J??VhG5(oqzQG!5*k80Pq*d71JMi_cxlAPIN^ z!Sp;*jLRU9CP@bPs09HS6b4yEg~9T0 z+?qw5$(0G>p4q}B9!ZN~W-bWkU_3%buVz>zYG4UzWV=79*8>yx+>*A zDraE{FGtt(c^}}y^G-rcc>7rJKJom_?uYkS5GG8+QqW8+2eu%Dr|lpz!Wz6LV6}Z$ zFA{-+n8{+fP^{ZE+i+_Qu(TYyGCn_lzh%qYb@8!-%G+1B?U$5Q_S2VY916wWb02lgmC$^m z4$HgR5{9@3PX;&d&maY!y3Fqv&k?%{8bd?pJic7xM=v_kJ-W9<`NzWZeD|IZDY1RC z`{IzTA1YG;!?4PBc;^a@^SHI^*i6B^zAUO^SFAC=WTKw+a}`HD(&fOMubjwT)fyU> zMvgoL*(zb@$>QzN{}|%?CmN+Ra{oKRPd8LHDjrFFKLL4#Bj8cGL%hDD zaK%@4|Myyde5KyuKfQ?gaQV>>-pQ-IUmf1q*i=&MHx@)LOY*84R)z(M6|k{>&(geVqc3#We!1$c!Pm@JXq1{W zV&88iXHH%U{<<$J-<-08a(d4fn=`Jg$)q^?;+=-Ue5W`)yt?8*T_0(7-*q~1er(ND z#{Eg}@2`pV>!-ESfQ*KucCStYaqZUHWr{ZilTyY&TLoGoncKFO)6!lVV7+a+p5Oxv zP8Hqt)_%wU$|Le`c;@-5YBzW!ZDua|lkdfS>Rk8am{|AjA>QP@;hOO~maKy%aqh)Y ze?1Y?<)?SMe`w`6ko-f9ypnd5@7D17#zJY2sQLZBrw;Yhy2)U3*9j~PEK!npR#&vIa%pUjg+jD1?2r-8(S=qhRA*vhSl>5N*FE&T&Ny$s zu;J?Utfuk(YNyXwAW{EPQbp@@LfYOt&amI1(1Xz?&)dOXgb+^7p~j3->?~LH#^r$B zA4S&)ox!bV|FS4FCH>>@;2`~+xGbCO^>E&}Jo2|H$<5Of)#tso-q!3lKGJ^eKq1 zvM%1b(wUUlS9Er4Um$TcPh>DmfA6(%WyBT}jc`0Oy*kmZM zrUo>O#^2LZ=LOz6LaGXq!zX3A{H(LMz literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/icons/excel.png b/spug_web/src/pages/host/icons/excel.png new file mode 100644 index 0000000000000000000000000000000000000000..b7eaa6b25e327d181802e4955e3845316d745855 GIT binary patch literal 4222 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 Sk*xps`>QHyK+B(7g#8ERGuZb4 literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/icons/freebsd.png b/spug_web/src/pages/host/icons/freebsd.png new file mode 100644 index 0000000000000000000000000000000000000000..cee9dcd6c4a531f7ac9040c61509d67103c835f8 GIT binary patch literal 1962 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 literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/icons/ubuntu.png b/spug_web/src/pages/host/icons/ubuntu.png new file mode 100644 index 0000000000000000000000000000000000000000..eb81c78736e539d25975426421755685f35c823b GIT binary patch literal 2212 zcmbVMd010d77wyiDbR62#+Ygv76r*p$U+bjlCTD7h{GmJmItJeg}fIM_yDOa%2*H? zK%^>)fK^!v1RO^Ngs)Xv6ttGAC{l(BSc^-vq6C>26=(kN>(_7I_ujkr-1|G{ch0%z z+~V+1e_Ly$H3EUK4GiE2;8AM&mYc(WeRf++ePOgGc2n5qdrxHn00SGMyl4S~S%)_%i7_?04jftf2 z34E0wkRl7nQUeiLp+ZSks)R1Z_-sWpbqv^m9Dqb!hok+JY5+~aQLqvM(F0ATDHUjwMUhykfxI!Wr*A@#tN8qPh85u3M8PS;>qII%5l6tw<)*k^qd`aj z{MU{5qCsJ%3cw2hP^nQ%02uDLqzvcokBLk`*c(QeS_U7AD2=0(Xykwb3gmcW;2oS) zCS{PNbQ)DepkOIfF%|1Up-{1O3MmOorqT#>ktZREMj$WQd7nPT)00RdQ@I?fCz(j( zQt4b4n?|D%J*g}Tk;bMk=>{r5NTiSeOWZP;`>ihL4|N%SYCr@j)k38*ZK(>vQOb=?E-u2biFl8NXNGa%rboxir!<1e6!!g+^k?6kHgq-I z<)fYt^&9U`&fT~&l^gf+?#S$+LNm5o9?WG22|^2TxQU)P4zOhkuay zATWPVv;D@@gNN4gu1D7mUyh*uD_CK-^{hX1oTCl4-$7hpPxzeLT{|7AZr{B~Ig${R zH!?`6O9+M*78wNWEJF~zh6!GsXz_EY-_PtEQKy$?29F8aHBUcB&4X9sST30gQgi}- zmJ*dyQI-`^8RMQlav_Nk(DvfAm8f)fV9ULr!=s$Bx(3@>(kG);;8@U6mGjfCnT^w_ z2DfOYa2R@ZAT_t+KHj0DLl9ryDXh5HvG+jB6Fly~w$&ws?^gNUrmoBIcxD(|2rDU{ zJtMwez9r{w?Zq*V{rW=*DUIX?L%3&3Sd6OZfT-%i7dy*dtvtgU$a4uEx1!3OKIHQO@#JOa8d(PVZxc@|RNlh=o{ z6I%Dx7%ekX<9GP7erR%9{`I*9dt`@0I&I5Tl-1Nv=9GG(5UM)Tdd)Amfala>b@w8 zhJ(%SS$Q^E_NgnesGTppteVH0I^y(}*Y%~F^=p?K9?u+2oG-L0u0R)`ndvS&jp?td z936Zh+%~O$La@=^{H)=Y_p+kKqq)D&lgRGk8@ddN?QMfC?rO&0n?9LyM^#gKiX=*W z#0NDqZJ8?sm-`&HR|i-ZKj<+#d^j61SpL%k?xcPQ3np%#ve_f^9$mM-UmWCCh;1yg zj}850b8&nPbLe{)Ze-f(9LKx%WiByYzT)1sqr)5Yp+(IVzd~!i`}}vpDq7QLW+TT` z7}a0gg3OF1>SJ{T0e{+NUuiuu=%cRKtglCVQ8TQHX(1`$m&n-==vH{w$$LZlS%DD& zUHR>u&b2?L{Z+H6_Wba4uAPMuCD+mGAA`3ir^W||RvxMC5AnI!o@-aVfU2CQKYb46 zTo~foghd7!x0gPKE?@ki&^au!H9HP7e?jxl9Ur2vm2=!&MDR_52;_!xTG)v@{{t&a BeqR6p literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/icons/windows.png b/spug_web/src/pages/host/icons/windows.png new file mode 100644 index 0000000000000000000000000000000000000000..482a8b1a3b935fa1ee7c4c1f237f2752c0dfd32c GIT binary patch literal 1443 zcmbVMZ%i9y7{AFt!W^PR!tfe17fO^dyf%A}pxx?cujU5#VtIz{Vo zVs1Sk+8GF;9$5h>Wu{CVLDr!*+DuYpot=CQwGgBgC#*PWHIWvEpc#@v3l|2XDSV6x zdz%Wepyb37nwDg6Je5kBQ&zLA#Bq|QX`Ha&7K;foOzME7v1yZ}8jBvhK;;x6sR^=# z>K@ss+^;z?m}!0sVloiea4e~XLcx;ZX*P+IW&#&QeP8QrRSScE(%6)(Mh21q4+B;1 zS2zIa#-cH--O~fv**j}&9^@~8#{9Y#pznFP}XRJJJx3L6eqHIx{sg9y- zCYrLuOjetnpxJsNW+$vgKbzT8HZSF&J#}7Ny^kb)HrnTIwA<}uz0FOL_C~sB>z7oG zl{iq0Ex_0XTkBJ88IJ;3O;#eZ+*|B|P(s#ZH6bTaj|bfoU^zk3-lG)`1rG(5a*Rpzd?W)@}WMjfjEEX#XKKksNC1pa0P~|)xyCJWM!rD_%J#&^zv6N z=fD3lyZh9q@mQO`_tMvA1pC42j=&lD|RNYb??YLa!oFt zxKcX3@ErN`#gYrpmpvgO%1UHIwnDOaZ2n9W664%Du;E&VwEU~z1G z@pN|ESnZwMc-oQOeeL62?YU2${4R!f87HgTjgR)>ZD+dMjomqZ)G%|;U$sgcnR{=~ z0sl{!;lW<520h?!}y`!UKn0`b3%MLa_a&%b<(i>;oRKbJflF3j-v zcLgu8r$_yleaAAc`429hxL;#heTTa-zfjdR{_U$}4L2Sfz)$`xEzm&auZn)xDO)Lwmi1>X$?^NSkNB;(@ CUHw@A literal 0 HcmV?d00001 diff --git a/spug_web/src/pages/host/index.js b/spug_web/src/pages/host/index.js index 65beb94..28141a9 100644 --- a/spug_web/src/pages/host/index.js +++ b/spug_web/src/pages/host/index.js @@ -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 () { {store.formVisible && } {store.importVisible && } + {store.cloudImport && } {store.selectorVisible && store.selectorVisible = false} onOk={store.updateGroup}/>} diff --git a/spug_web/src/pages/host/index.module.less b/spug_web/src/pages/host/index.module.less new file mode 100644 index 0000000..b6a71ac --- /dev/null +++ b/spug_web/src/pages/host/index.module.less @@ -0,0 +1,4 @@ +.steps { + width: 350px; + margin: 0 auto 30px; +} \ No newline at end of file diff --git a/spug_web/src/pages/host/store.js b/spug_web/src/pages/host/store.js index 7608480..bbc4f7f 100644 --- a/spug_web/src/pages/host/store.js +++ b/spug_web/src/pages/host/store.js @@ -20,6 +20,7 @@ class Store { @observable isFetching = false; @observable formVisible = false; @observable importVisible = false; + @observable cloudImport = null; @observable detailVisible = false; @observable selectorVisible = false;