Просмотр исходного кода

项目视图核心功能完成

James Iter 7 лет назад
Родитель
Сommit
bfa2a3d89e

+ 145 - 2
jimvc/api/guest.py

@@ -886,6 +886,108 @@ def r_get(uuids):
     return ret
 
 
+def exchange_guest_os_templates_logo(os_templates_image_mapping_by_id=None, os_templates_profile_mapping_by_id=None,
+                                     os_template_image_id=None):
+    assert isinstance(os_templates_image_mapping_by_id, dict)
+    assert isinstance(os_templates_profile_mapping_by_id, dict)
+    assert isinstance(os_template_image_id, int)
+
+    if os_templates_image_mapping_by_id[os_template_image_id]['logo'] == "":
+        logo = os_templates_profile_mapping_by_id[os_templates_image_mapping_by_id[os_template_image_id]['os_template_profile_id']]['icon']
+    else:
+        logo = os_templates_image_mapping_by_id[os_template_image_id]['logo']
+
+    label = os_templates_image_mapping_by_id[os_template_image_id]['label']
+    return logo, label
+
+
+def format_guest_status(_status, progress):
+    from jimvc.models import GuestState
+
+    color = 'FF645B'
+    icon = 'glyph-icon icon-bolt'
+    desc = '未知状态'
+
+    if _status == GuestState.booting.value:
+        color = '00BBBB'
+        icon = 'glyph-icon icon-circle'
+        desc = '启动中'
+
+    elif _status == GuestState.running.value:
+        color = '00BB00'
+        icon = 'glyph-icon icon-circle'
+        desc = '运行中'
+
+    elif _status == GuestState.creating.value:
+        color = 'FFC543'
+        icon = 'glyph-icon icon-spinner'
+        desc = ' '.join(['创建中', str(progress) + '%'])
+
+    elif _status == GuestState.blocked.value:
+        color = '3D4245'
+        icon = 'glyph-icon icon-minus-square'
+        desc = '被阻塞'
+
+    elif _status == GuestState.paused.value:
+        color = 'B7B904'
+        icon = 'glyph-icon icon-pause'
+        desc = '暂停'
+
+    elif _status == GuestState.shutdown.value:
+        color = '4E5356'
+        icon = 'glyph-icon icon-terminal'
+        desc = '关闭'
+
+    elif _status == GuestState.shutoff.value:
+        color = 'FFC543'
+        icon = 'glyph-icon icon-plug'
+        desc = '断电'
+
+    elif _status == GuestState.crashed.value:
+        color = '9E2927'
+        icon = 'glyph-icon icon-question'
+        desc = '已崩溃'
+
+    elif _status == GuestState.pm_suspended.value:
+        color = 'FCFF07'
+        icon = 'glyph-icon icon-anchor'
+        desc = '悬挂'
+
+    elif _status == GuestState.migrating.value:
+        color = '1CF5E7'
+        icon = 'glyph-icon icon-space-shuttle'
+        desc = '迁移中'
+
+    elif _status == GuestState.dirty.value:
+        color = 'FF0707'
+        icon = 'glyph-icon icon-remove'
+        desc = '创建失败,待清理'
+
+    else:
+        pass
+
+    return '<span class="{icon}" style="color: #{color};">&nbsp;&nbsp;{desc}</span>'.format(
+        icon=icon, color=color, desc=desc)
+
+
+def exchange_guest_bandwidth(bandwidth=None):
+    assert isinstance(bandwidth, int)
+
+    if bandwidth == 0:
+        bandwidth = '<span style="font-size: 16px;" title="无限带宽">&nbsp;∞</span>'
+
+    elif 0 < bandwidth < 1000 ** 2:
+        bandwidth = str(bandwidth // 1000) + ' Kbps'
+
+    elif 1000 ** 2 <= bandwidth < 1000 ** 3:
+        bandwidth = str(bandwidth // 1000 ** 2) + ' Mbps'
+
+    else:
+        bandwidth = str(bandwidth // 1000 ** 3) + ' Gbps'
+
+    return bandwidth
+
+
 @Utils.dumps2response
 def r_get_by_filter():
     ret = guest_base.get_by_filter()
@@ -935,6 +1037,16 @@ def r_get_by_filter():
     for host in hosts_ret['data']:
         hosts_mapping_by_node_id[int(host['node_id'])] = host
 
+    os_templates_image, _ = OSTemplateImage.get_by_filter()
+    os_templates_image_mapping_by_id = dict()
+    for os_template_image in os_templates_image:
+        os_templates_image_mapping_by_id[os_template_image['id']] = os_template_image
+
+    os_templates_profile, _ = OSTemplateProfile.get_by_filter()
+    os_templates_profile_mapping_by_id = dict()
+    for os_template_profile in os_templates_profile:
+        os_templates_profile_mapping_by_id[os_template_profile['id']] = os_template_profile
+
     for i, guest in enumerate(ret['data']):
 
         guest_uuid = ret['data'][i]['uuid']
@@ -969,6 +1081,17 @@ def r_get_by_filter():
         if not hosts_mapping_by_node_id[ret['data'][i]['node_id']]['alive']:
             ret['data'][i]['status'] = GuestState.no_state.value
 
+        ret['data'][i]['hostname'] = hosts_mapping_by_node_id[guest['node_id']]['hostname']
+
+        ret['data'][i]['html'] = dict()
+        ret['data'][i]['html']['logo'], ret['data'][i]['html']['os_template_label'] = exchange_guest_os_templates_logo(
+            os_templates_image_mapping_by_id=os_templates_image_mapping_by_id,
+            os_templates_profile_mapping_by_id=os_templates_profile_mapping_by_id,
+            os_template_image_id=guest['os_template_image_id'])
+
+        ret['data'][i]['html']['status'] = format_guest_status(_status=guest['status'], progress=guest['progress'])
+        ret['data'][i]['html']['bandwidth'] = exchange_guest_bandwidth(bandwidth=guest['bandwidth'])
+
     return ret
 
 
@@ -1021,6 +1144,16 @@ def r_content_search():
     for host in hosts_ret['data']:
         hosts_mapping_by_node_id[int(host['node_id'])] = host
 
+    os_templates_image, _ = OSTemplateImage.get_by_filter()
+    os_templates_image_mapping_by_id = dict()
+    for os_template_image in os_templates_image:
+        os_templates_image_mapping_by_id[os_template_image['id']] = os_template_image
+
+    os_templates_profile, _ = OSTemplateProfile.get_by_filter()
+    os_templates_profile_mapping_by_id = dict()
+    for os_template_profile in os_templates_profile:
+        os_templates_profile_mapping_by_id[os_template_profile['id']] = os_template_profile
+
     for i, guest in enumerate(ret['data']):
 
         guest_uuid = ret['data'][i]['uuid']
@@ -1055,6 +1188,17 @@ def r_content_search():
         if not hosts_mapping_by_node_id[ret['data'][i]['node_id']]['alive']:
             ret['data'][i]['status'] = GuestState.no_state.value
 
+        ret['data'][i]['hostname'] = hosts_mapping_by_node_id[guest['node_id']]['hostname']
+
+        ret['data'][i]['html'] = dict()
+        ret['data'][i]['html']['logo'], ret['data'][i]['html']['os_template_label'] = exchange_guest_os_templates_logo(
+            os_templates_image_mapping_by_id=os_templates_image_mapping_by_id,
+            os_templates_profile_mapping_by_id=os_templates_profile_mapping_by_id,
+            os_template_image_id=guest['os_template_image_id'])
+
+        ret['data'][i]['html']['status'] = format_guest_status(_status=guest['status'], progress=guest['progress'])
+        ret['data'][i]['html']['bandwidth'] = exchange_guest_bandwidth(bandwidth=guest['bandwidth'])
+
     return ret
 
 
@@ -1105,8 +1249,7 @@ def r_distribute_count():
 def r_update(uuid):
 
     args_rules = [
-        Rules.UUID.value,
-        Rules.SERVICE_ID.value
+        Rules.UUID.value
     ]
 
     if 'remark' in request.json:

+ 73 - 3
jimvc/api/project.py

@@ -11,6 +11,7 @@ from jimvc.api.base import Base
 from jimvc.models import Utils
 from jimvc.models import Rules
 from jimvc.models import Project
+from jimvc.models import Service
 
 
 __author__ = 'James Iter'
@@ -114,17 +115,86 @@ def r_update(_id):
 
 @Utils.dumps2response
 def r_get(ids):
-    return project_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+    ret = project_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+    project_ids = list()
+
+    if -1 == ids.find(','):
+        project_ids.append(ret['data']['id'])
+
+    else:
+        for project in ret['data']:
+            project_ids.append(project['id'])
+
+    rows, _ = Service.get_by_filter(filter_str=':'.join([
+        'project_id', 'in', ','.join(project_id.__str__() for project_id in project_ids)]))
+
+    services_mapping_by_project_id = dict()
+
+    for row in rows:
+        if row['project_id'] not in services_mapping_by_project_id:
+            services_mapping_by_project_id[row['project_id']] = list()
+
+        services_mapping_by_project_id[row['project_id']].append(row)
+
+    if -1 == ids.find(','):
+        ret['data']['services'] = services_mapping_by_project_id[ret['data']['id']]
+
+    else:
+        for i, project in enumerate(ret['data']):
+            ret['data'][i]['services'] = services_mapping_by_project_id[project['id']]
+
+    return ret
 
 
 @Utils.dumps2response
 def r_get_by_filter():
-    return project_base.get_by_filter()
+    ret = project_base.get_by_filter()
+
+    project_ids = list()
+    for project in ret['data']:
+        project_ids.append(project['id'])
+
+    rows, _ = Service.get_by_filter(filter_str=':'.join([
+        'project_id', 'in', ','.join(project_id.__str__() for project_id in project_ids)]))
+
+    services_mapping_by_project_id = dict()
+
+    for row in rows:
+        if row['project_id'] not in services_mapping_by_project_id:
+            services_mapping_by_project_id[row['project_id']] = list()
+
+        services_mapping_by_project_id[row['project_id']].append(row)
+
+    for i, project in enumerate(ret['data']):
+        ret['data'][i]['services'] = services_mapping_by_project_id[project['id']]
+
+    return ret
 
 
 @Utils.dumps2response
 def r_content_search():
-    return project_base.content_search()
+    ret = project_base.content_search()
+
+    project_ids = list()
+    for project in ret['data']:
+        project_ids.append(project['id'])
+
+    rows, _ = Service.get_by_filter(filter_str=':'.join([
+        'project_id', 'in', ','.join(project_id.__str__() for project_id in project_ids)]))
+
+    services_mapping_by_project_id = dict()
+
+    for row in rows:
+        if row['project_id'] not in services_mapping_by_project_id:
+            services_mapping_by_project_id[row['project_id']] = list()
+
+        services_mapping_by_project_id[row['project_id']].append(row)
+
+    for i, project in enumerate(ret['data']):
+        ret['data'][i]['services'] = services_mapping_by_project_id[project['id']]
+
+    return ret
 
 
 @Utils.dumps2response

+ 5 - 0
jimvc/models/event_processor.py

@@ -171,6 +171,11 @@ class EventProcessor(object):
                     cls.disk.size = data['disk_info']['virtual-size'] / (1024 ** 3)
                     cls.disk.update()
 
+                    cls.guest.uuid = uuid
+                    cls.guest.get_by('uuid')
+                    cls.guest.progress = 100
+                    cls.guest.update()
+
                 else:
                     cls.guest.uuid = uuid
                     cls.guest.get_by('uuid')

+ 2 - 2
jimvc/themes/default/static/themes/admin/layout.js

@@ -55,7 +55,7 @@ function body_sizer() {
 
         $('#page-sidebar').css('height', contentHeight);
         $('.scroll-sidebar').css('height', contentHeight);
-        $('#page-content').css('min-height', contentHeight);
+        // $('#page-content').css('min-height', contentHeight);
 
     } else {
 
@@ -65,7 +65,7 @@ function body_sizer() {
 
         $('#page-sidebar').css('height', contentHeight);
         $('.scroll-sidebar').css('height', contentHeight);
-        $('#page-content').css('min-height', contentHeight);
+        // $('#page-content').css('min-height', contentHeight);
 
     }
 

+ 205 - 202
jimvc/themes/default/templates/guests_show.html

@@ -373,7 +373,7 @@
                 alter_danger('实例备注更新失败!');
             },
             success : function() {
-                $('#guest_list tbody').find('td:contains(' + uuid + ')').next().next().find('p').text(remark);
+                $('.guest_list tbody').find('td:contains(' + uuid + ')').next().next().find('p').text(remark);
                 alter_success('实例备注更新成功!');
             }
         });
@@ -910,7 +910,7 @@
         refresh_project();
     }
 
-    function refresh_project_cards() {
+    function refresh_projects_card() {
         $.ajax({
             url : '/api/projects',
             type : 'GET',
@@ -920,10 +920,10 @@
             },
             success : function(data, textStatus, xhr) {
                 $('#project_tool_bar').css('display', 'unset');
-                $('#project_cards').css('display', 'unset');
-                $('#project_cards').empty();
+                $('#projects_card').css('display', 'unset');
+                $('#projects_card').empty();
                 $.each(data.data, function(k, v) {
-                    $('#project_cards').append(
+                    $('#projects_card').append(
                         '<a class="project" href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '"><span class="project_name">' + v['name'] + '</span></a>'
                     );
                 });
@@ -931,16 +931,204 @@
         });
     }
 
+    function exchange_disabled(status, set) {
+        if (status in set) {
+            return 'disabled'
+        }
+    }
+
+    function exchange_by_snapshot_not_creatable_disabled(status) {
+        if (!status) {
+            return 'disabled'
+        }
+    }
+
+    function exchange_guest_for_tr(guest) {
+        return '<tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">' +
+                '<td style="display: none;">' + guest.uuid + '</td>' +
+                '<td><input title="选中" type="checkbox"></td>' +
+                '<td width="180px;">' +
+                    '<div><a href="/guest/detail/' + guest.uuid + '">' + guest.label + '</a></div>' +
+                    '<div><p style="display: inline-block;">' + guest.remark + '</p>' +
+                        '<a href="javascript:;" class="edit_remark_trigger" data-toggle="modal" data-target="#edit_remark_modal" style="display: none; float: right;">' +
+                            '<span class="glyph-icon icon-elusive-pencil" style="width: 20px; height: 20px; margin-left: 10px; border-radius: 0; border: 1px solid rgb(220, 233, 255); background-color: #ffffff;"></span>' +
+                        '</a>' +
+                    '</div>' +
+                '</td>' +
+                '<td width="40px" style="padding-left: 12px;">' +
+                    '<span class="' + guest.html.logo + '" title="' + guest.html.os_template_label + '"></span>' +
+                '</td>' +
+                '<td>' + guest.html.status + '</td>' +
+                '<td>' + guest.hostname + '</td>' +
+                '<td>' +
+                    'CPU :&nbsp;&nbsp;' + guest.cpu + '&nbsp;核<br />' +
+                    '内存 :&nbsp;&nbsp;' + guest.memory + '&nbsp;GB<br />' +
+                    '带宽 :&nbsp;' + guest.html.bandwidth + '</td>' +
+                '<td>' + guest.ip + '</td>' +
+                '<td width="150px;"><a href="javascript:;" class="glyph-icon icon-elusive-eye-off" onclick="change_display_password_for_row(this);" style="color: #014c8c;"></a><span class="unreal_password">******</span><span class="real_password" style="display: none;">' + guest.password + '</span></td>' +
+                '<td>' +
+                    '<div class="dropdown inline-block">' +
+                        '<a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">更多</a>' +
+                    '<ul class="dropdown-menu">' +
+                    '<li class="' + exchange_disabled(guest.status, [5, 6, 7]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#boot_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#boot_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '启动' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#reboot_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#reboot_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '重启' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#force_reboot_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#force_reboot_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '强制重启' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#shutdown_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#shutdown_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '停止' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#force_shutdown_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#force_shutdown_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '强制停止' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="divider"></li>' +
+                    '<li class="' + exchange_by_snapshot_not_creatable_disabled(guest.snapshot.creatable) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#create_snapshot_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#create_snapshot_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent);' +
+                            '$(\'#snapshot_label\').val($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '创建快照' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="divider"></li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#suspend_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#suspend_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '暂停' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [4]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#resume_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#resume_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '恢复' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="divider"></li>' +
+                    '<li class="' + exchange_disabled(guest.status, [1, 2]) + '">' +
+                        '<a href="/guest/vnc/' + guest.uuid+ '" target="_blank">远程连接</a>' +
+                    '</li>' +
+                    '<li class="divider"></li>' +
+                    '<li class="">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#allocate_bandwidth_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#allocate_bandwidth_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '分配带宽' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [6]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#adjust_ability_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#adjust_ability_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                            '变更配置' +
+                        '</a>' +
+                    '<li class="divider"></li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#reset_password_modal"' +
+                            'onclick="$(\'#reset_password_type\').val(\'single\')">密码重置</a>' +
+                    '</li>' +
+                    '<li class="' + exchange_disabled(guest.status, [2, 4, 5, 6, 7]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#migrate_modal"' +
+                            'onclick="$(\'#migrate_type\').val(\'single\')">' +
+                                '迁移到' +
+                        '</a>' +
+                    '</li>' +
+                    '<li class="divider"></li>' +
+                    '<li class=" ' + exchange_disabled(guest.status, [5, 6, 7, 255]) + '">' +
+                        '<a href="javascript:;" data-toggle="modal" data-target="#delete_modal"' +
+                            'onclick="$(\'#instance_uuid\').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());' +
+                            '$(\'#delete_instance_desc\').text($($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div a\')[0].textContent + \'/\' + $($(this).parent().parent().parent().parent().parent().children()[2]).find(\'div p\')[0].textContent)">' +
+                                '删除' +
+                        '</a>' +
+                    '</li>' +
+                    '</ul>' +
+                    '</div>' +
+                '</td>' +
+            '</tr>'
+    }
+
+    function get_service_html(service_id) {
+        var html = '';
+
+        $.ajax({
+            url : '/api/guests?filter=service_id:eq:' + service_id,
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            async: false,
+            error : function() {
+            },
+            success : function(data, textStatus, xhr) {
+                $.each(data.data, function(k, v) {
+                    html += exchange_guest_for_tr(v);
+                });
+            }
+        });
+        return html
+    }
+
     function refresh_services(project_id) {
         $('#project_services').css('display', 'unset');
+        $('#project_services').empty();
+
+        $.ajax({
+            url : '/api/projects/' + project_id,
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+            },
+            success : function(data, textStatus, xhr) {
+                $('#project_desc').css('display', 'unset');
+                $('#project_desc').text(data.data['description']);
+
+                $.each(data.data.services, function(k, v) {
+                    var html = '<div class="project-service-name">' + v['name'] + '</div>' +
+                        '<div>' +
+                        '    <table class="table-hover table-bordered-project-service guest_list" cellspacing="0" width="100%" role="grid"' +
+                        '           style="width: 100%; margin-bottom: 4px; border-bottom-width: 0;">' +
+                        '        <tbody>';
+                    html += get_service_html(v['id']);
+                    html += '        </tbody>' +
+                        '    </table>' +
+                        '</div>';
+                    $('#project_services').append(html);
+                });
+            }
+        });
     }
 
     function refresh_project() {
         var project_id = window.location.hash.replace(/\#/, "").split('#');
 
         $('#project_tool_bar').css('display', 'none');
-        $('#project_cards').css('display', 'none');
-        $('#project_tabs').css('display', 'none');
+        $('#projects_card').css('display', 'none');
+        $('#projects_tab').css('display', 'none');
         $('#project_desc').css('display', 'none');
         $('#project_services').css('display', 'none');
 
@@ -955,16 +1143,16 @@
                 error : function() {
                 },
                 success : function(data, textStatus, xhr) {
-                    $('#project_tabs').css('display', 'unset');
-                    $('#project_tabs').empty();
-                    $('#project_tabs').append('<ul class="nav nav-tabs project-nav-tabs mrg25B"></ul>');
+                    $('#projects_tab').css('display', 'unset');
+                    $('#projects_tab').empty();
+                    $('#projects_tab').append('<ul class="nav nav-tabs project-nav-tabs mrg25B"></ul>');
                     $.each(data.data, function(k, v) {
                         if (v['id'].toString() === project_id) {
-                            $('#project_tabs > ul').append(
+                            $('#projects_tab > ul').append(
                                 '<li class="active"><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
                             );
                         } else {
-                            $('#project_tabs > ul').append(
+                            $('#projects_tab > ul').append(
                                 '<li><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
                             );
                         }
@@ -972,22 +1160,9 @@
                 }
             });
 
-            $.ajax({
-                url : '/api/projects/' + project_id,
-                type : 'GET',
-                contentType: "application/json; charset=utf-8",
-                dataType: 'json',
-                error : function() {
-                },
-                success : function(data, textStatus, xhr) {
-                    $('#project_desc').css('display', 'unset');
-                    $('#project_desc').text(data.data['description']);
-                }
-            });
-
             refresh_services(project_id);
         } else {
-            refresh_project_cards();
+            refresh_projects_card();
         }
     }
 </script>
@@ -1010,7 +1185,7 @@
                         </div>
                     </div>
                 </div>
-                <table id="guest_list" class="table table-bordered table-hover" cellspacing="0" width="100%" role="grid"
+                <table class="table table-bordered table-hover guest_list" cellspacing="0" width="100%" role="grid"
                        style="width: 100%; margin-bottom: 0; border-bottom-width: 0;">
                 <thead>
                 <tr role="row">
@@ -1285,182 +1460,10 @@
                     </div>
                 </div>
                 <div style="margin-top: 20px;">
-                    <div id="project_tabs" style="display: none;"></div>
+                    <div id="projects_tab" style="display: none;"></div>
                     <div id="project_desc" style="display: none;"></div>
-                    <div id="project_services" style="display: none;">
-                        <div id="project_service_name" class="project-service-name">Web</div>
-                        <div id="project_service_guests">
-                            <table id="guest_list" class="table-hover table-bordered-project-service" cellspacing="0" width="100%" role="grid"
-                                   style="width: 100%; margin-bottom: 4px; border-bottom-width: 0;">
-                                <tbody>
-                                {% for item in guests %}
-                                    <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
-                                        <td style="display: none;">{{ item.uuid }}</td>
-                                        <td><input title="选中" type="checkbox"></td>
-                                        <td width="180px;">
-                                            <div>
-                                                <a href="/guest/detail/{{ item.uuid }}">{{ item.label }}</a>
-                                            </div>
-                                            <div>
-                                                <p style="display: inline-block;">{{ item.remark }}</p>
-                                                <a href="javascript:;" class="edit_remark_trigger" data-toggle="modal" data-target="#edit_remark_modal" style="display: none; float: right;">
-                                                    <span class="glyph-icon icon-elusive-pencil" style="width: 20px; height: 20px; margin-left: 10px; border-radius: 0; border: 1px solid rgb(220, 233, 255); background-color: #ffffff;"></span>
-                                                </a>
-                                            </div>
-                                        </td>
-                                        <td width="40px" style="padding-left: 12px;">
-                                            {% if os_templates_image_mapping_by_id[item.os_template_image_id | string].logo == "" %}
-                                                <span class="{{ os_templates_profile_mapping_by_id[os_templates_image_mapping_by_id[item.os_template_image_id | string].os_template_profile_id].icon }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
-                                            {% else %}
-                                                <span class="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].logo }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
-                                            {% endif %}
-                                        </td>
-                                        <td>{{ format_guest_status(item.status, item.progress)|safe }}</td>
-                                        <td>{{ hosts_mapping_by_node_id[item.node_id | string].hostname }}</td>
-                                        <td>
-                                            CPU :&nbsp;&nbsp;{{ item.cpu }}&nbsp;核<br />
-                                            内存 :&nbsp;&nbsp;{{ item.memory }}&nbsp;GB<br />
-                                            带宽 :&nbsp;
-                                            {% if item.bandwidth == 0 %}
-                                                <span style="font-size: 16px;" title="无限带宽">
-                                &nbsp;∞
-                                </span>
-                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
-                                                {{ item.bandwidth // 1000 }}
-
-                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
-                                                {{ item.bandwidth // 1000**2 }}
-
-                                            {% else %}
-                                                {{ item.bandwidth // 1000**3 }}
-
-                                            {% endif %}
-
-                                            {% if item.bandwidth == 0 %}
-                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
-                                                Kbps
-                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
-                                                Mbps
-                                            {% else %}
-                                                Gbps
-                                            {% endif %}
-                                        </td>
-                                        <td>{{ item.ip }}</td>
-                                        <td width="150px;"><a href="#" class="glyph-icon icon-elusive-eye-off" onclick="change_display_password_for_row(this);" style="color: #014c8c;"></a><span class="unreal_password">******</span><span class="real_password" style="display: none;">{{ item.password }}</span></td>
-                                        <td>
-                                            <div class="dropdown inline-block">
-                                                <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
-                                                    更多
-                                                </a>
-                                                <ul class="dropdown-menu">
-                                                    <li class="{% if item.status not in [5, 6, 7] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#boot_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#boot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            启动
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#reboot_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            重启
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_reboot_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#force_reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            强制重启
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#shutdown_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            停止
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_shutdown_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#force_shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            强制停止
-                                                        </a>
-                                                    </li>
-                                                    <li class="divider"></li>
-                                                    <li class="{% if not item.snapshot.creatable %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#create_snapshot_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#create_snapshot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent);
-                                                $('#snapshot_label').val($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            创建快照
-                                                        </a>
-                                                    </li>
-                                                    <li class="divider"></li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#suspend_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#suspend_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            暂停
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [4] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#resume_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#resume_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            恢复
-                                                        </a>
-                                                    </li>
-                                                    <li class="divider"></li>
-                                                    <li class="{% if item.status not in [1, 2] %} disabled {% endif %}">
-                                                        <a href="/guest/vnc/{{ item.uuid }}" target="_blank">远程连接</a>
-                                                    </li>
-                                                    <li class="divider"></li>
-                                                    <li class="">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#allocate_bandwidth_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#allocate_bandwidth_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            分配带宽
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [6] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#adjust_ability_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#adjust_ability_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            变更配置
-                                                        </a>
-                                                    <li class="divider"></li>
-                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#reset_password_modal"
-                                                           onclick="$('#reset_password_type').val('single')">
-                                                            密码重置
-                                                        </a>
-                                                    </li>
-                                                    <li class="{% if item.status not in [2, 4, 5, 6, 7] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#migrate_modal"
-                                                           onclick="$('#migrate_type').val('single')">
-                                                            迁移到
-                                                        </a>
-                                                    </li>
-                                                    <li class="divider"></li>
-                                                    <li class="{% if item.status not in [5, 6, 7, 255] %} disabled {% endif %}">
-                                                        <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
-                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
-                                                $('#delete_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
-                                                            删除
-                                                        </a>
-                                                    </li>
-                                                </ul>
-                                            </div>
-                                        </td>
-                                    </tr>
-                                {% endfor %}
-                                </tbody>
-                            </table>
-                        </div>
-                    </div>
-                    <div id="project_cards" style="display: none;"></div>
+                    <div id="project_services" style="display: none;"></div>
+                    <div id="projects_card" style="display: none;"></div>
                 </div>
             </div>
         </div>