Преглед изворни кода

优化虚拟机列表前端风格

James Iter пре 9 година
родитељ
комит
87b48a3835
5 измењених фајлова са 446 додато и 7 уклоњено
  1. 74 0
      models/utils.py
  2. 162 0
      static/bootstrap/js/affix.js
  3. 186 1
      templates/guest.html
  4. 16 2
      templates/layout.html
  5. 8 4
      views/guest.py

+ 74 - 0
models/utils.py

@@ -4,6 +4,8 @@
 from functools import wraps
 
 import commands
+
+import time
 from flask import make_response, g, request
 from flask.wrappers import Response
 from werkzeug.utils import import_string, cached_property
@@ -129,3 +131,75 @@ def add_rule_api(blueprint, rule, api_func=None, **options):
 
 def add_rule_views(blueprint, rule, views_func=None, **options):
     blueprint.add_url_rule(rule=rule, view_func=LazyView(''.join(['views.', views_func])), **options)
+
+
+@app.context_processor
+def utility_processor():
+
+    def format_price(amount, currency=u'¥'):
+        return u'{0:.2f}{1}'.format(amount, currency)
+
+    def format_datetime_by_tus(tus, fmt='%y-%m-%d %H:%M'):
+        return time.strftime(fmt, time.localtime(tus/1000/1000))
+
+    def format_guest_status(status):
+        from status import GuestState
+
+        color = 'FF645B'
+        icon = 'glyph-icon icon-bolt'
+        desc = '未知状态'
+
+        if status == GuestState.running.value:
+            color = '82c251'
+            icon = 'glyph-icon icon-circle'
+            desc = '运行中'
+
+        elif status == GuestState.no_state.value:
+            color = 'FFC543'
+            icon = 'glyph-icon icon-spinner'
+            desc = '创建中'
+
+        elif status == GuestState.blocked.value:
+            color = '3D4245'
+            icon = 'glyph-icon icon-spinner'
+            desc = '被阻塞'
+
+        elif status == GuestState.paused.value:
+            color = 'FCFF07'
+            icon = 'glyph-icon icon-spinner'
+            desc = '暂停'
+
+        elif status == GuestState.shutdown.value:
+            color = '4E5356'
+            icon = 'glyph-icon icon-spinner'
+            desc = '关闭'
+
+        elif status == GuestState.shutoff.value:
+            color = 'FFC543'
+            icon = 'glyph-icon icon-spinner'
+            desc = '断电'
+
+        elif status == GuestState.crashed.value:
+            color = '9E2927'
+            icon = 'glyph-icon icon-spinner'
+            desc = '已崩溃'
+
+        elif status == GuestState.pm_suspended.value:
+            color = 'FCFF07'
+            icon = 'glyph-icon icon-spinner'
+            desc = '悬挂'
+
+        elif status == GuestState.dirty.value:
+            color = 'FCFF07'
+            icon = 'glyph-icon icon-spinner'
+            desc = '创建失败,待清理'
+
+        else:
+            pass
+
+        return '<span class="{icon}" style="color: #{color};">&nbsp;&nbsp;{desc}</span>'.format(
+            icon=icon, color=color, desc=desc)
+
+    return dict(format_price=format_price, format_datetime_by_tus=format_datetime_by_tus,
+                format_guest_status=format_guest_status)
+

+ 162 - 0
static/bootstrap/js/affix.js

@@ -0,0 +1,162 @@
+/* ========================================================================
+ * Bootstrap: affix.js v3.3.7
+ * http://getbootstrap.com/javascript/#affix
+ * ========================================================================
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // AFFIX CLASS DEFINITION
+  // ======================
+
+  var Affix = function (element, options) {
+    this.options = $.extend({}, Affix.DEFAULTS, options)
+
+    this.$target = $(this.options.target)
+      .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
+      .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))
+
+    this.$element     = $(element)
+    this.affixed      = null
+    this.unpin        = null
+    this.pinnedOffset = null
+
+    this.checkPosition()
+  }
+
+  Affix.VERSION  = '3.3.7'
+
+  Affix.RESET    = 'affix affix-top affix-bottom'
+
+  Affix.DEFAULTS = {
+    offset: 0,
+    target: window
+  }
+
+  Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
+    var scrollTop    = this.$target.scrollTop()
+    var position     = this.$element.offset()
+    var targetHeight = this.$target.height()
+
+    if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
+
+    if (this.affixed == 'bottom') {
+      if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
+      return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
+    }
+
+    var initializing   = this.affixed == null
+    var colliderTop    = initializing ? scrollTop : position.top
+    var colliderHeight = initializing ? targetHeight : height
+
+    if (offsetTop != null && scrollTop <= offsetTop) return 'top'
+    if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
+
+    return false
+  }
+
+  Affix.prototype.getPinnedOffset = function () {
+    if (this.pinnedOffset) return this.pinnedOffset
+    this.$element.removeClass(Affix.RESET).addClass('affix')
+    var scrollTop = this.$target.scrollTop()
+    var position  = this.$element.offset()
+    return (this.pinnedOffset = position.top - scrollTop)
+  }
+
+  Affix.prototype.checkPositionWithEventLoop = function () {
+    setTimeout($.proxy(this.checkPosition, this), 1)
+  }
+
+  Affix.prototype.checkPosition = function () {
+    if (!this.$element.is(':visible')) return
+
+    var height       = this.$element.height()
+    var offset       = this.options.offset
+    var offsetTop    = offset.top
+    var offsetBottom = offset.bottom
+    var scrollHeight = Math.max($(document).height(), $(document.body).height())
+
+    if (typeof offset != 'object')         offsetBottom = offsetTop = offset
+    if (typeof offsetTop == 'function')    offsetTop    = offset.top(this.$element)
+    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
+
+    var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
+
+    if (this.affixed != affix) {
+      if (this.unpin != null) this.$element.css('top', '')
+
+      var affixType = 'affix' + (affix ? '-' + affix : '')
+      var e         = $.Event(affixType + '.bs.affix')
+
+      this.$element.trigger(e)
+
+      if (e.isDefaultPrevented()) return
+
+      this.affixed = affix
+      this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
+
+      this.$element
+        .removeClass(Affix.RESET)
+        .addClass(affixType)
+        .trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
+    }
+
+    if (affix == 'bottom') {
+      this.$element.offset({
+        top: scrollHeight - height - offsetBottom
+      })
+    }
+  }
+
+
+  // AFFIX PLUGIN DEFINITION
+  // =======================
+
+  function Plugin(option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.affix')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  var old = $.fn.affix
+
+  $.fn.affix             = Plugin
+  $.fn.affix.Constructor = Affix
+
+
+  // AFFIX NO CONFLICT
+  // =================
+
+  $.fn.affix.noConflict = function () {
+    $.fn.affix = old
+    return this
+  }
+
+
+  // AFFIX DATA-API
+  // ==============
+
+  $(window).on('load', function () {
+    $('[data-spy="affix"]').each(function () {
+      var $spy = $(this)
+      var data = $spy.data()
+
+      data.offset = data.offset || {}
+
+      if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
+      if (data.offsetTop    != null) data.offset.top    = data.offsetTop
+
+      Plugin.call($spy, data)
+    })
+  })
+
+}(jQuery);

+ 186 - 1
templates/guest.html

@@ -2,7 +2,192 @@
 {% block title %} Guest {% endblock %}
 {% block head %}
   {{ super() }}
+    <style type="text/css">
+        .table {
+            font-size: 12px;
+            border-width: 1px;
+        }
+        .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: #6d6e6f;
+        }
+
+        .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;
+        }
+
+    </style>
+
 {% endblock head %}
 {% block content %}
-    Welcome on my awesome homepage.
+
+<script type="text/javascript">
+
+    /* Datatable row highlight */
+
+    /*
+    $(document).ready(function() {
+        $('#datatable-row-highlight tbody').on( 'click', 'tr', function () {
+            $(this).toggleClass('tr-selected');
+        } );
+    });
+    */
+</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;">
+                    <div class="col-sm-6">
+                        <div class="dataTables_length" id="datatable-row-highlight_length">
+                            <label>
+                                <select name="datatable-row-highlight_length" aria-controls="datatable-row-highlight" class="form-control">
+                                    <option value="10">10</option>
+                                    <option value="25">25</option>
+                                    <option value="50">50</option>
+                                    <option value="100">100</option>
+                                </select> records per page</label>
+                        </div>
+                    </div>
+                    <div class="col-sm-6">
+                        <div id="datatable-row-highlight_filter" class="dataTables_filter">
+                            <label>
+                                <input type="search" class="form-control" placeholder="Search..." aria-controls="datatable-row-highlight"></label>
+                        </div>
+                    </div>
+                </div>
+                <table id="guest_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 type="checkbox"></th>
+                    <th>名称</th>
+                    <th></th>
+                    <th>状态</th>
+                    <th>计算节点</th>
+                    <th>配置</th>
+                    <th>IP</th>
+                    <th>密码</th>
+                    <th>创建时间</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                {% for item in data %}
+                <tr role="row" class="odd">
+                    <td><input type="checkbox"></td>
+                    <td>
+                        <a href="javascript:;">{{ item.name }}</a>
+                        <br />
+                        {{ item.remark }}
+                    </td>
+                    <td>{{ item.os_template_id }}</td>
+                    <td>{{ format_guest_status(item.status)|safe }}</td>
+                    <td>{{ item.on_host }}</td>
+                    <td>CPU :&nbsp;&nbsp;{{ item.cpu }}&nbsp;核<br />内存 :&nbsp;&nbsp;{{ item.memory }}&nbsp;GB</td>
+                    <td>{{ item.ip }}</td>
+                    <td>{{ item.password }}</td>
+                    <td>{{ format_datetime_by_tus(item.create_time) }}</td>
+                    <td>
+                        <div class="dropdown inline-block">
+                            <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
+                                更多
+                            </a>
+                            <ul class="dropdown-menu">
+                                <li>
+                                    <a href="javascript:;">
+                                        <span>启用</span>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:;">
+                                        <span>禁用</span>
+                                    </a>
+                                </li>
+                                <li class="">
+                                    <a href="javascript:;">
+                                        <span>编辑</span>
+                                    </a>
+                                </li>
+                                <li class="">
+                                    <a href="javascript:;">
+                                        <span>密码重置</span>
+                                    </a>
+                                </li>
+                                <li class="">
+                                    <a href="javascript:;">
+                                        <span>删除</span>
+                                    </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>
+                        <th><input type="checkbox" title="select-all"></th>
+                        <th>
+                            <div class="row">
+                                <div class="col-sm-6">
+                                    <div class="dataTables_info" id="datatable-row-highlight_info" role="status" aria-live="polite">
+                                        Showing 1 to 10 of 57 entries
+                                    </div>
+                                </div>
+                                <div class="col-sm-6">
+                                    <div class="dataTables_paginate paging_bootstrap" id="datatable-row-highlight_paginate">
+                                        <ul class="pagination">
+                                            <li class="previous disabled">
+                                                <a href="#">Previous</a>
+                                            </li>
+                                            <li class="active">
+                                                <a href="#">1</a>
+                                            </li>
+                                            <li>
+                                                <a href="#">2</a>
+                                            </li>
+                                            <li>
+                                                <a href="#">3</a>
+                                            </li>
+                                            <li>
+                                                <a href="#">4</a>
+                                            </li>
+                                            <li>
+                                                <a href="#">5</a>
+                                            </li>
+                                            <li class="next">
+                                                <a href="#">Next</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </th>
+                    </tr>
+                    </tfoot>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
 {% endblock content %}

+ 16 - 2
templates/layout.html

@@ -7,6 +7,17 @@
     <style>
         /* Loading Spinner */
         .spinner{margin:0;width:70px;height:18px;margin:-35px 0 0 -9px;position:absolute;top:50%;left:50%;text-align:center}.spinner > div{width:18px;height:18px;background-color:#333;border-radius:100%;display:inline-block;-webkit-animation:bouncedelay 1.4s infinite ease-in-out;animation:bouncedelay 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.spinner .bounce1{-webkit-animation-delay:-.32s;animation-delay:-.32s}.spinner .bounce2{-webkit-animation-delay:-.16s;animation-delay:-.16s}@-webkit-keyframes bouncedelay{0%,80%,100%{-webkit-transform:scale(0.0)}40%{-webkit-transform:scale(1.0)}}@keyframes bouncedelay{0%,80%,100%{transform:scale(0.0);-webkit-transform:scale(0.0)}40%{transform:scale(1.0);-webkit-transform:scale(1.0)}}
+
+        .affix {
+            top: 10px;
+            position: sticky;
+        }
+
+        .affix-top {
+        }
+
+        .affix-bottom {
+        }
     </style>
 
 
@@ -64,6 +75,8 @@
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons-min/spinnericon/spinnericon.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons/iconic/iconic.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons/elusive/elusive.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons/meteocons/meteocons.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons/typicons/typicons.css') }}">
 
     <!-- WIDGETS -->
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/accordion-ui/accordion.css') }}">
@@ -182,7 +195,7 @@
                     <div class="user-account-btn dropdown">
                         <a href="#" title="My Account" class="user-profile clearfix" data-toggle="dropdown">
                             <img width="28" src="{{ url_for('static', filename='images/icons/JimV-icon_144X144.png') }}" alt="Profile image">
-                            <span>Thomas Barnes</span>
+                            <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;James Iter</span>
                             <i class="glyph-icon icon-angle-down"></i>
                         </a>
                         <div class="dropdown-menu float-left">
@@ -241,7 +254,7 @@
             </div><!-- #page-header -->
             <div id="page-sidebar" style="margin-top: 20px;">
                 <div class="scroll-sidebar">
-                    <ul id="sidebar-menu">
+                    <ul id="sidebar-menu" data-spy="affix" data-offset-top="10">
                         <li class="header"><span>虚拟化平台管理</span></li>
                         <li>
                             <a href="{{ url_for('v_guests.show') }}" title="Guests">
@@ -306,6 +319,7 @@
         <!-- WIDGETS -->
         <script type="text/javascript" src="{{ url_for('static', filename='tether/js/tether.js') }}"></script>
         <script type="text/javascript" src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"></script>
+        <script type="text/javascript" src="{{ url_for('static', filename='bootstrap/js/affix.js') }}"></script>
         <!-- Bootstrap Dropdown -->
         <!-- <script type="text/javascript" src="{{ url_for('static', filename='widgets/dropdown/dropdown.js') }}"></script> -->
         <!-- Bootstrap Tooltip -->

+ 8 - 4
views/guest.py

@@ -1,8 +1,8 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-
-
-from flask import Blueprint, render_template
+import json
+from flask import Blueprint, render_template, url_for, request
+import requests
 
 
 __author__ = 'James Iter'
@@ -25,6 +25,10 @@ blueprints = Blueprint(
 
 
 def show():
-    return render_template('guest.html')
+    host_url = request.host_url.rstrip('/')
+    url = host_url + url_for('api_guests.r_get_by_filter')
+    ret = requests.get(url=url)
+    ret = json.loads(ret.content)
+    return render_template('guest.html', data=ret['data'])