ソースを参照

开始实现前端模板模块

James Iter 9 年 前
コミット
8e94923798
5 ファイル変更547 行追加2 行削除
  1. 4 0
      main.py
  2. 1 1
      templates/layout.html
  3. 409 0
      templates/os_template_show.html
  4. 130 0
      views/os_template.py
  5. 3 1
      views_route_table.py

+ 4 - 0
main.py

@@ -44,6 +44,8 @@ from views.disk import blueprint as view_disk_blueprint
 from views.disk import blueprints as view_disk_blueprints
 from views.log import blueprint as view_log_blueprint
 from views.log import blueprints as view_log_blueprints
+from views.os_template import blueprint as view_os_template_blueprint
+from views.os_template import blueprints as view_os_template_blueprints
 
 from websockify.websocketproxy import WebSocketProxy
 
@@ -117,6 +119,8 @@ try:
     app.register_blueprint(view_disk_blueprints)
     app.register_blueprint(view_log_blueprint)
     app.register_blueprint(view_log_blueprints)
+    app.register_blueprint(view_os_template_blueprint)
+    app.register_blueprint(view_os_template_blueprints)
 
 except:
     logger.error(traceback.format_exc())

+ 1 - 1
templates/layout.html

@@ -318,7 +318,7 @@
                             </a>
                         </li>
                         <li>
-                            <a href="{{ url_for('v_guests.show') }}" title="GuestsTemplate">
+                            <a href="{{ url_for('v_os_templates.show') }}" title="GuestsTemplate">
                                 <i class="glyph-icon icon-linecons-inbox"></i>
                                 <span>虚拟机模板</span>
                             </a>

+ 409 - 0
templates/os_template_show.html

@@ -0,0 +1,409 @@
+{% extends "layout.html" %}
+{% block head %}
+    {{ super() }}
+    <style type="text/css">
+        .table {
+            font-size: 12px;
+            border-width: 1px;
+            line-height: 20px;
+        }
+        .table > thead > tr > th,
+        .table > tfoot > tr > th {
+            color: #999999;
+            font-weight: normal;
+            border-bottom: 0 solid #e1e6eb;
+            background-color: #F5F6FA;
+        }
+        
+        .table > tbody > tr > td {
+            color: #424547;
+        }
+
+        .table-bordered > tbody > tr {
+            padding-top: 10px;
+            padding-bottom: 10px;
+        }
+
+        .table-bordered > thead > tr > th,
+        .table-bordered > tbody > tr > th,
+        .table-bordered > tfoot > tr > th,
+        .table-bordered > thead > tr > td,
+        .table-bordered > tbody > tr > td,
+        .table-bordered > tfoot > tr > td {
+            border-style: solid;
+            border-width: 1px 0 0 0;
+        }
+
+        .btn,
+        .form-control,
+        .modal-content {
+            border-radius: 0 !important;
+        }
+
+        .btn-shortcut {
+            font-size: 12px !important;
+            padding: 0 26px;
+        }
+
+        .show {
+            display: inline-block !important;
+        }
+
+        .tr-selected td,
+        .tr-selected {
+            color: #000 !important;
+            background: #fffdf4 !important;
+        }
+    </style>
+
+{% endblock head %}
+{% block content %}
+
+<script type="text/javascript">
+    var page = 1;
+    var page_size = 10;
+    var keyword = '';
+    var resource_path = window.location.pathname;
+    var cur_url = resource_path;
+
+    $(document).ready(function() {
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+
+        var last_ready = null;
+        $('#content_search').keydown(function() {
+            if (last_ready !== null) {
+                clearTimeout(last_ready);
+            }
+            last_ready = setTimeout(function () {
+                keyword = $('#content_search').val();
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+                if (keyword.length > 0) {
+                    cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+                }
+                window.location.href=cur_url;
+            }, 1000);
+        });
+
+        $('#page_size').change(function () {
+            keyword = $('#content_search').val();
+            page_size = $('#page_size').val();
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+            if (keyword.length > 0) {
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+            }
+            window.location.href=cur_url;
+        });
+
+        $("thead").on('click', ".all_selector", function() {
+            if ($("thead .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tfoot").on('click', ".all_selector", function() {
+            if ($("tfoot .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tbody tr").on('click', "input[type='checkbox']", function() {
+            select_item_action();
+        });
+
+        $('body').addClass('add-transition');
+        $('.add-page-transition').on('click', function(){
+            var transAttr = $(this).attr('data-transition');
+            $('.add-transition').attr('class', 'add-transition');
+            $('.add-transition').addClass(transAttr);
+        });
+
+        $('#edit_remark_modal').on('show.bs.modal', function (me) {
+            $('#instance_uuid').val($(me.relatedTarget).parent().parent().find('div a')[0].text);
+            $('#edit_remark').val($(me.relatedTarget).prev().text());
+        });
+
+    });
+
+    function refresh() {
+        keyword = $('#content_search').val();
+        page = $('#pagination li.active a').text();
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+        if (keyword.length > 0) {
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+        }
+        window.location.href=cur_url;
+    }
+
+    function row_onmouseover(me) {
+        $(me).find(".edit_remark_trigger").css('display','inline-flex');
+    }
+
+    function row_onmouseout(me) {
+        $(me).find(".edit_remark_trigger").css('display','none');
+    }
+
+    function remark_update(me) {
+        var uuid = $('#instance_uuid').val();
+        var remark = $('#edit_remark').val();
+        $('#edit_remark_modal').modal('hide');
+        $.ajax({
+            url : '/api/os_template/' + uuid,
+            type : 'PATCH',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                remark: remark
+            }),
+            error : function() {
+                alter_danger('模板名称更新失败!');
+            },
+            success : function() {
+                $('#disk_list tbody').find('td:contains(' + uuid + ')').find('p').text(remark);
+                alter_success('模板名称更新成功!');
+            }
+        });
+    }
+
+    function get_selected_element(checked) {
+        if (checked === null) {
+            checked = true;
+        }
+
+        if (checked) {
+            return $('tbody :checked');
+        } else {
+
+            return $('tbody input:not(:checked)');
+        }
+    }
+    
+    function highlight_selected_element() {
+        var selected_element = get_selected_element(true);
+        var no_selected_element = get_selected_element(false);
+
+        selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', true);
+        });
+
+        no_selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', false);
+        });
+    }
+    
+    function shortcut_bar_enable() {
+        var selected_element = get_selected_element(true);
+
+        if (selected_element.length > 0) {
+            $('.btn-shortcut').toggleClass('disabled', false);
+        } else {
+            $('.btn-shortcut').toggleClass('disabled', true);
+        }
+    }
+
+    function select_item_action() {
+        highlight_selected_element();
+        shortcut_bar_enable();
+    }
+
+    function mount(uuid, disk_uuid) {
+        $.ajax({
+            url : '/api/guest/_attach_disk/' + uuid + '/' + disk_uuid,
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('实例挂载指令发送失败!');
+            },
+            success : function() {
+                alter_success('实例挂载指令发送成功!');
+            }
+        });
+    }
+
+</script>
+<div class="panel">
+    <div class="panel-body">
+        <h3 class="title-hero" style="font-size: 24px;">
+            虚拟机模板
+        </h3>
+        <div>
+            <div id="datatable-row-highlight_wrapper" class="dataTables_wrapper form-inline">
+                <div class="row" style="padding: 10px 10px 10px 0; width: 100%;">
+                    <div class="col-sm-12" style="padding-right: 0;">
+                        <div id="datatable-row-highlight_filter" class="dataTables_filter" style="display: inline-block;">
+                            <input id="content_search" type="search" class="form-control" placeholder="模糊搜索..." value="{%- if keyword -%} {{ keyword }} {%- endif -%}" style="margin-left: 0; border-radius: 0;">
+                        </div>
+                        <div class="pull-right">
+                            <button class="btn btn-default" onclick="refresh()" style="border-radius: 0;"><span class="glyph-icon icon-elusive-arrows-cw"></span></button>
+                            <a class="btn btn-info add-page-transition" href="/os_templates/create" data-transition="pt-page-moveFromRight-init" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">添加模板</a>
+                        </div>
+                    </div>
+                </div>
+                <table id="os_template_list" class="table table-bordered table-hover" cellspacing="0" width="100%" role="grid"
+                       style="width: 100%; margin-bottom: 0; border-bottom-width: 0;">
+                <thead>
+                <tr role="row">
+                    <th><input class="all_selector" title="选取所有" type="checkbox"></th>
+                    <th width="180px;">名称</th>
+                    <th>状态</th>
+                    <th>路径</th>
+                    <th>ICON</th>
+                    <th>初始化作业</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                {% for item in os_templates_ret.data %}
+                <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                    <td><input title="选中" type="checkbox"></td>
+                    <td>
+                        <div>
+                            <p style="display: inline-block;">{{ item.label }}</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>{% if item.active == 0 %}
+                        <span style="color: #990000;">未启用</span>
+                        {% else %}
+                        <span style="color: #00BB00;">启用</span>
+                        {% endif %}
+                    </td>
+                    <td>{{ item.path }}</td>
+                    <td><span class="{{ item.icon }}"></span></td>
+                    <td>{{ item.boot_job_id }}</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.state not in [1] %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#mount_modal"
+                                       onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text);">
+                                        启用
+                                    </a>
+                                </li>
+                                <li class="{% if item.state not in [1] %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#mount_modal"
+                                       onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text);">
+                                        禁用
+                                    </a>
+                                </li>
+                                <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                <li class="{% if item.state not in [1] %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#mount_modal"
+                                       onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text);">
+                                        变更初始化作业
+                                    </a>
+                                </li>
+                                <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                <li class="{% if item.state not in [1, 255] %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                       onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text);
+                                       $('#delete_instance_desc').text($('#instance_uuid').val().split('-')[0] + '/' + $($(this).parent().parent().parent().parent().parent().children()[1]).find('div p')[0].textContent)">
+                                        删除
+                                    </a>
+                                </li>
+                            </ul>
+                        </div>
+                    </td>
+                </tr>
+                {% endfor %}
+                </tbody>
+                </table>
+
+                <table class="table table-bordered" style="border-top-width: 0; z-index: 99; position: sticky; bottom: 0;">
+                    <tfoot>
+                    <tr style="height: 70px;">
+                        <th><input type="checkbox" title="选取所有" class="all_selector"></th>
+                        <th>
+                            <div class="row">
+                                <div class="col-sm-6">
+                                    <button class="btn btn-default btn-shortcut disabled" onclick="batch_boot();">编辑名称</button>
+                                </div>
+                                <div class="col-sm-3" style="font-size: 12px; padding-top: 5px; text-align: right;">
+                                    共有{{ os_templates_ret.paging.total }}条,每页显示:
+                                    <select id="page_size" name="datatable-row-highlight_length" title="page_size" class="form-control" style="height: 22px; vertical-align: baseline;">
+                                        <option value="10" {% if page_size == 10 %} selected {% endif %}>10</option>
+                                        <option value="20" {% if page_size == 20 %} selected {% endif %}>20</option>
+                                        <option value="50" {% if page_size == 50  %} selected {% endif %}>50</option>
+                                    </select>&nbsp;&nbsp;条
+                                </div>
+                                <div class="col-sm-3" style="text-align: left;">
+                                    <div class="dataTables_paginate paging_bootstrap" id="datatable-row-highlight_paginate">
+                                        <ul id="pagination" class="pagination">
+                                            <li class="{% if page == 1 %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page - 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">«</a>
+                                            </li>
+                                            {% for item in pages %}
+                                            <li class="{% if item == page %} active {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ item }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">{{ item }}</a>
+                                            </li>
+                                            {% endfor %}
+                                            <li class="{% if page == last_page %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page + 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">»</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </th>
+                    </tr>
+                    </tfoot>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<input id="instance_uuid" title="模板 ID" class="form-control" name="uuid" hidden>
+
+<div class="modal" id="edit_remark_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">编辑模板名称:</h4>
+            </div>
+            <div class="modal-body">
+                <input id="edit_remark" title="实例名称" class="form-control" name="remark">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="remark_update();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="delete_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">删除模板:</h4>
+            </div>
+            <div class="modal-body">
+                <p>你确定要删除该模板吗?</p>
+                <h3 style="color: orangered;" id="delete_instance_desc"></h3>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="delete_at();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock content %}

+ 130 - 0
views/os_template.py

@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+from flask import Blueprint, render_template, url_for, request
+import requests
+from math import ceil
+
+
+__author__ = 'James Iter'
+__date__ = '2017/7/11'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2017 by James Iter.'
+
+
+blueprint = Blueprint(
+    'v_os_template',
+    __name__,
+    url_prefix='/os_template'
+)
+
+blueprints = Blueprint(
+    'v_os_templates',
+    __name__,
+    url_prefix='/os_templates'
+)
+
+
+def show():
+    args = list()
+    page = int(request.args.get('page', 1))
+    page_size = int(request.args.get('page_size', 10))
+    keyword = request.args.get('keyword', None)
+    guest_uuid = request.args.get('guest_uuid', None)
+    sequence = request.args.get('sequence', None)
+    order_by = request.args.get('order_by', None)
+    order = request.args.get('order', None)
+    filters = list()
+    resource_path = request.path
+
+    if page is not None:
+        args.append('page=' + page.__str__())
+
+    if page_size is not None:
+        args.append('page_size=' + page_size.__str__())
+
+    if keyword is not None:
+        args.append('keyword=' + keyword.__str__())
+
+    if guest_uuid is not None:
+        filters.append('guest_uuid:in:' + guest_uuid.__str__())
+
+    if sequence is not None:
+        filters.append('sequence:in:' + sequence.__str__())
+
+    if order_by is not None:
+        args.append('order_by=' + order_by)
+
+    if order is not None:
+        args.append('order=' + order)
+
+    if filters.__len__() > 0:
+        args.append('filter=' + ','.join(filters))
+
+    host_url = request.host_url.rstrip('/')
+
+    os_templates_url = host_url + url_for('api_os_templates.r_get_by_filter')
+    if keyword is not None:
+        os_templates_url = host_url + url_for('api_os_templates.r_content_search')
+
+    if args.__len__() > 0:
+        os_templates_url = os_templates_url + '?' + '&'.join(args)
+
+    os_templates_ret = requests.get(url=os_templates_url)
+    os_templates_ret = json.loads(os_templates_ret.content)
+
+    last_page = int(ceil(os_templates_ret['paging']['total'] / float(page_size)))
+    page_length = 5
+    pages = list()
+    if page < int(ceil(page_length / 2.0)):
+        for i in range(1, page_length + 1):
+            pages.append(i)
+            if i == last_page:
+                break
+
+    elif last_page - page < page_length / 2:
+        for i in range(last_page - page_length + 1, last_page + 1):
+            if i < 1:
+                continue
+            pages.append(i)
+
+    else:
+        for i in range(page - page_length / 2, page + int(ceil(page_length / 2.0))):
+            pages.append(i)
+            if i == last_page:
+                break
+
+    return render_template('os_template_show.html', os_templates_ret=os_templates_ret, resource_path=resource_path,
+                           page=page, page_size=page_size, keyword=keyword, pages=pages, order_by=order_by, order=order,
+                           last_page=last_page)
+
+
+def create():
+    host_url = request.host_url.rstrip('/')
+
+    if request.method == 'POST':
+        size = request.form.get('size')
+        quantity = request.form.get('quantity')
+        remark = request.form.get('remark')
+
+        payload = {
+            "size": int(size),
+            "quantity": int(quantity),
+            "remark": remark
+        }
+
+        url = host_url + '/api/disk'
+        headers = {'content-type': 'application/json'}
+        r = requests.post(url, data=json.dumps(payload), headers=headers)
+        j_r = json.loads(r.content)
+        return render_template('success.html', go_back_url='/disks', timeout=10000, title='提交成功',
+                               message_title='创建实例的请求已被接受',
+                               message='您所提交的资源正在创建中。根据所提交资源的数量,需要等待几到十几秒钟。页面将在10秒钟后自动跳转到实例列表页面!')
+
+    else:
+        return render_template('disk_create.html')
+
+
+

+ 3 - 1
views_route_table.py

@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 from models.utils import add_rule_views
-from views import guest, disk, log
+from views import guest, disk, log, os_template
 
 
 __author__ = 'James Iter'
@@ -23,3 +23,5 @@ add_rule_views(disk.blueprint, '/detail/<uuid>', views_func='disk.detail', metho
 
 add_rule_views(log.blueprints, '', views_func='log.show', methods=['GET'])
 
+add_rule_views(os_template.blueprints, '', views_func='os_template.show', methods=['GET'])
+