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

前端实现ssh-key 绑定、解绑操作

James Iter 8 лет назад
Родитель
Сommit
43ccc25479

+ 26 - 7
api/ssh_key.py

@@ -4,6 +4,7 @@
 
 from flask import Blueprint
 from flask import request
+from werkzeug.datastructures import ImmutableMultiDict
 import json
 import jimit as ji
 
@@ -132,6 +133,7 @@ def r_content_search():
 
 @Utils.dumps2response
 def r_delete(ids):
+    # TODO: 做好依赖逻辑处理。比如已关联 ssh_key 的删除。
     return ssh_key_base.delete(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
 
 
@@ -150,7 +152,13 @@ def r_bound(ssh_key_id):
         for row in rows:
             guests_uuid.append(row['guest_uuid'])
 
-        request.args['filter_str'] = ':'.join(['uuid', 'in', ','.join(guests_uuid)])
+        if guests_uuid.__len__() == 0:
+            guests_uuid.append('_')
+
+        request.__setattr__('args', ImmutableMultiDict([
+            ('filter', ':'.join(['uuid', 'in', ','.join(guests_uuid)])),
+            ('page_size', 10000)
+        ]))
 
         return guest_base.get_by_filter()
     except ji.PreviewingError, e:
@@ -172,13 +180,14 @@ def r_unbound(ssh_key_id):
         for row in rows:
             guests_uuid.append(row['guest_uuid'])
 
-        from werkzeug.datastructures import ImmutableMultiDict
+        if guests_uuid.__len__() == 0:
+            guests_uuid.append('_')
+
         request.__setattr__('args', ImmutableMultiDict([
             ('filter', ':'.join(['uuid', 'notin', ','.join(guests_uuid)])),
             ('page_size', 10000)
         ]))
 
-        x = guest_base.get_by_filter()
         return guest_base.get_by_filter()
     except ji.PreviewingError, e:
         return json.loads(e.message)
@@ -230,8 +239,13 @@ def r_bind(ssh_key_id, uuids):
         for row in rows:
             guests_uuid.append(row['guest_uuid'])
 
-        request.args['filter_str'] = ':'.join(['uuid', 'in', ','.join(guests_uuid)])
-        request.args['page_size'] = 10000
+        if guests_uuid.__len__() == 0:
+            guests_uuid.append('_')
+
+        request.__setattr__('args', ImmutableMultiDict([
+            ('filter', ':'.join(['uuid', 'in', ','.join(guests_uuid)])),
+            ('page_size', 10000)
+        ]))
 
         return guest_base.get_by_filter()
     except ji.PreviewingError, e:
@@ -270,8 +284,13 @@ def r_unbind(ssh_key_id, uuids):
         for row in rows:
             guests_uuid.append(row['guest_uuid'])
 
-        request.args['filter_str'] = ':'.join(['uuid', 'in', ','.join(guests_uuid)])
-        request.args['page_size'] = 10000
+        if guests_uuid.__len__() == 0:
+            guests_uuid.append('_')
+
+        request.__setattr__('args', ImmutableMultiDict([
+            ('filter', ':'.join(['uuid', 'in', ','.join(guests_uuid)])),
+            ('page_size', 10000)
+        ]))
 
         return guest_base.get_by_filter()
     except ji.PreviewingError, e:

+ 1 - 0
requirements.txt

@@ -10,3 +10,4 @@ gunicorn
 gevent
 redis
 websockify
+werkzeug

+ 93 - 0
static/multi-select/css/multi-select.css

@@ -0,0 +1,93 @@
+.ms-container{
+  background: transparent url('../img/switch.png') no-repeat 50% 50%;
+  width: 370px;
+}
+
+.ms-container:after{
+  content: ".";
+  display: block;
+  height: 0;
+  line-height: 0;
+  font-size: 0;
+  clear: both;
+  min-height: 0;
+  visibility: hidden;
+}
+
+.ms-container .ms-selectable, .ms-container .ms-selection{
+  background: #fff;
+  color: #555555;
+  float: left;
+  width: 45%;
+}
+.ms-container .ms-selection{
+  float: right;
+}
+
+.ms-container .ms-list{
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+  transition: border linear 0.2s, box-shadow linear 0.2s;
+  border: 1px solid #ccc;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  position: relative;
+  height: 200px;
+  padding: 0;
+  overflow-y: auto;
+}
+
+.ms-container .ms-list.ms-focus{
+  border-color: rgba(82, 168, 236, 0.8);
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  outline: 0;
+  outline: thin dotted \9;
+}
+
+.ms-container ul{
+  margin: 0;
+  list-style-type: none;
+  padding: 0;
+}
+
+.ms-container .ms-optgroup-container{
+  width: 100%;
+}
+
+.ms-container .ms-optgroup-label{
+  margin: 0;
+  padding: 5px 0px 0px 5px;
+  cursor: pointer;
+  color: #999;
+}
+
+.ms-container .ms-selectable li.ms-elem-selectable,
+.ms-container .ms-selection li.ms-elem-selection{
+  border-bottom: 1px #eee solid;
+  padding: 2px 10px;
+  color: #555;
+  font-size: 14px;
+}
+
+.ms-container .ms-selectable li.ms-hover,
+.ms-container .ms-selection li.ms-hover{
+  cursor: pointer;
+  color: #fff;
+  text-decoration: none;
+  background-color: #08c;
+}
+
+.ms-container .ms-selectable li.disabled,
+.ms-container .ms-selection li.disabled{
+  background-color: #eee;
+  color: #aaa;
+  cursor: text;
+}

BIN
static/multi-select/img/switch.png


+ 544 - 0
static/multi-select/js/jquery.multi-select.js

@@ -0,0 +1,544 @@
+/*
+* MultiSelect v0.9.12
+* Copyright (c) 2012 Louis Cuny
+*
+* This program is free software. It comes without any warranty, to
+* the extent permitted by applicable law. You can redistribute it
+* and/or modify it under the terms of the Do What The Fuck You Want
+* To Public License, Version 2, as published by Sam Hocevar. See
+* http://sam.zoy.org/wtfpl/COPYING for more details.
+*/
+
+!function ($) {
+
+  "use strict";
+
+
+ /* MULTISELECT CLASS DEFINITION
+  * ====================== */
+
+  var MultiSelect = function (element, options) {
+    this.options = options;
+    this.$element = $(element);
+    this.$container = $('<div/>', { 'class': "ms-container" });
+    this.$selectableContainer = $('<div/>', { 'class': 'ms-selectable' });
+    this.$selectionContainer = $('<div/>', { 'class': 'ms-selection' });
+    this.$selectableUl = $('<ul/>', { 'class': "ms-list", 'tabindex' : '-1' });
+    this.$selectionUl = $('<ul/>', { 'class': "ms-list", 'tabindex' : '-1' });
+    this.scrollTo = 0;
+    this.elemsSelector = 'li:visible:not(.ms-optgroup-label,.ms-optgroup-container,.'+options.disabledClass+')';
+  };
+
+  MultiSelect.prototype = {
+    constructor: MultiSelect,
+
+    init: function(){
+      var that = this,
+          ms = this.$element;
+
+      if (ms.next('.ms-container').length === 0){
+        ms.css({ position: 'absolute', left: '-9999px' });
+        ms.attr('id', ms.attr('id') ? ms.attr('id') : Math.ceil(Math.random()*1000)+'multiselect');
+        this.$container.attr('id', 'ms-'+ms.attr('id'));
+        this.$container.addClass(that.options.cssClass);
+        ms.find('option').each(function(){
+          that.generateLisFromOption(this);
+        });
+
+        this.$selectionUl.find('.ms-optgroup-label').hide();
+
+        if (that.options.selectableHeader){
+          that.$selectableContainer.append(that.options.selectableHeader);
+        }
+        that.$selectableContainer.append(that.$selectableUl);
+        if (that.options.selectableFooter){
+          that.$selectableContainer.append(that.options.selectableFooter);
+        }
+
+        if (that.options.selectionHeader){
+          that.$selectionContainer.append(that.options.selectionHeader);
+        }
+        that.$selectionContainer.append(that.$selectionUl);
+        if (that.options.selectionFooter){
+          that.$selectionContainer.append(that.options.selectionFooter);
+        }
+
+        that.$container.append(that.$selectableContainer);
+        that.$container.append(that.$selectionContainer);
+        ms.after(that.$container);
+
+        that.activeMouse(that.$selectableUl);
+        that.activeKeyboard(that.$selectableUl);
+
+        var action = that.options.dblClick ? 'dblclick' : 'click';
+
+        that.$selectableUl.on(action, '.ms-elem-selectable', function(){
+          that.select($(this).data('ms-value'));
+        });
+        that.$selectionUl.on(action, '.ms-elem-selection', function(){
+          that.deselect($(this).data('ms-value'));
+        });
+
+        that.activeMouse(that.$selectionUl);
+        that.activeKeyboard(that.$selectionUl);
+
+        ms.on('focus', function(){
+          that.$selectableUl.focus();
+        });
+      }
+
+      var selectedValues = ms.find('option:selected').map(function(){ return $(this).val(); }).get();
+      that.select(selectedValues, 'init');
+
+      if (typeof that.options.afterInit === 'function') {
+        that.options.afterInit.call(this, this.$container);
+      }
+    },
+
+    'generateLisFromOption' : function(option, index, $container){
+      var that = this,
+          ms = that.$element,
+          attributes = "",
+          $option = $(option);
+
+      for (var cpt = 0; cpt < option.attributes.length; cpt++){
+        var attr = option.attributes[cpt];
+
+        if(attr.name !== 'value' && attr.name !== 'disabled'){
+          attributes += attr.name+'="'+attr.value+'" ';
+        }
+      }
+      var selectableLi = $('<li '+attributes+'><span>'+that.escapeHTML($option.text())+'</span></li>'),
+          selectedLi = selectableLi.clone(),
+          value = $option.val(),
+          elementId = that.sanitize(value);
+
+      selectableLi
+        .data('ms-value', value)
+        .addClass('ms-elem-selectable')
+        .attr('id', elementId+'-selectable');
+
+      selectedLi
+        .data('ms-value', value)
+        .addClass('ms-elem-selection')
+        .attr('id', elementId+'-selection')
+        .hide();
+
+      if ($option.prop('disabled') || ms.prop('disabled')){
+        selectedLi.addClass(that.options.disabledClass);
+        selectableLi.addClass(that.options.disabledClass);
+      }
+
+      var $optgroup = $option.parent('optgroup');
+
+      if ($optgroup.length > 0){
+        var optgroupLabel = $optgroup.attr('label'),
+            optgroupId = that.sanitize(optgroupLabel),
+            $selectableOptgroup = that.$selectableUl.find('#optgroup-selectable-'+optgroupId),
+            $selectionOptgroup = that.$selectionUl.find('#optgroup-selection-'+optgroupId);
+
+        if ($selectableOptgroup.length === 0){
+          var optgroupContainerTpl = '<li class="ms-optgroup-container"></li>',
+              optgroupTpl = '<ul class="ms-optgroup"><li class="ms-optgroup-label"><span>'+optgroupLabel+'</span></li></ul>';
+
+          $selectableOptgroup = $(optgroupContainerTpl);
+          $selectionOptgroup = $(optgroupContainerTpl);
+          $selectableOptgroup.attr('id', 'optgroup-selectable-'+optgroupId);
+          $selectionOptgroup.attr('id', 'optgroup-selection-'+optgroupId);
+          $selectableOptgroup.append($(optgroupTpl));
+          $selectionOptgroup.append($(optgroupTpl));
+          if (that.options.selectableOptgroup){
+            $selectableOptgroup.find('.ms-optgroup-label').on('click', function(){
+              var values = $optgroup.children(':not(:selected, :disabled)').map(function(){ return $(this).val();}).get();
+              that.select(values);
+            });
+            $selectionOptgroup.find('.ms-optgroup-label').on('click', function(){
+              var values = $optgroup.children(':selected:not(:disabled)').map(function(){ return $(this).val();}).get();
+              that.deselect(values);
+            });
+          }
+          that.$selectableUl.append($selectableOptgroup);
+          that.$selectionUl.append($selectionOptgroup);
+        }
+        index = index === undefined ? $selectableOptgroup.find('ul').children().length : index + 1;
+        selectableLi.insertAt(index, $selectableOptgroup.children());
+        selectedLi.insertAt(index, $selectionOptgroup.children());
+      } else {
+        index = index === undefined ? that.$selectableUl.children().length : index;
+
+        selectableLi.insertAt(index, that.$selectableUl);
+        selectedLi.insertAt(index, that.$selectionUl);
+      }
+    },
+
+    'addOption' : function(options){
+      var that = this;
+
+      if (options.value !== undefined && options.value !== null){
+        options = [options];
+      } 
+      $.each(options, function(index, option){
+        if (option.value !== undefined && option.value !== null &&
+            that.$element.find("option[value='"+option.value+"']").length === 0){
+          var $option = $('<option value="'+option.value+'">'+option.text+'</option>'),
+              $container = option.nested === undefined ? that.$element : $("optgroup[label='"+option.nested+"']"),
+              index = parseInt((typeof option.index === 'undefined' ? $container.children().length : option.index));
+
+          if (option.optionClass) {
+            $option.addClass(option.optionClass);
+          }
+
+          if (option.disabled) {
+            $option.prop('disabled', true);
+          }
+
+          $option.insertAt(index, $container);
+          that.generateLisFromOption($option.get(0), index, option.nested);
+        }
+      });
+    },
+
+    'escapeHTML' : function(text){
+      return $("<div>").text(text).html();
+    },
+
+    'activeKeyboard' : function($list){
+      var that = this;
+
+      $list.on('focus', function(){
+        $(this).addClass('ms-focus');
+      })
+      .on('blur', function(){
+        $(this).removeClass('ms-focus');
+      })
+      .on('keydown', function(e){
+        switch (e.which) {
+          case 40:
+          case 38:
+            e.preventDefault();
+            e.stopPropagation();
+            that.moveHighlight($(this), (e.which === 38) ? -1 : 1);
+            return;
+          case 37:
+          case 39:
+            e.preventDefault();
+            e.stopPropagation();
+            that.switchList($list);
+            return;
+          case 9:
+            if(that.$element.is('[tabindex]')){
+              e.preventDefault();
+              var tabindex = parseInt(that.$element.attr('tabindex'), 10);
+              tabindex = (e.shiftKey) ? tabindex-1 : tabindex+1;
+              $('[tabindex="'+(tabindex)+'"]').focus();
+              return;
+            }else{
+              if(e.shiftKey){
+                that.$element.trigger('focus');
+              }
+            }
+        }
+        if($.inArray(e.which, that.options.keySelect) > -1){
+          e.preventDefault();
+          e.stopPropagation();
+          that.selectHighlighted($list);
+          return;
+        }
+      });
+    },
+
+    'moveHighlight': function($list, direction){
+      var $elems = $list.find(this.elemsSelector),
+          $currElem = $elems.filter('.ms-hover'),
+          $nextElem = null,
+          elemHeight = $elems.first().outerHeight(),
+          containerHeight = $list.height(),
+          containerSelector = '#'+this.$container.prop('id');
+
+      $elems.removeClass('ms-hover');
+      if (direction === 1){ // DOWN
+
+        $nextElem = $currElem.nextAll(this.elemsSelector).first();
+        if ($nextElem.length === 0){
+          var $optgroupUl = $currElem.parent();
+
+          if ($optgroupUl.hasClass('ms-optgroup')){
+            var $optgroupLi = $optgroupUl.parent(),
+                $nextOptgroupLi = $optgroupLi.next(':visible');
+
+            if ($nextOptgroupLi.length > 0){
+              $nextElem = $nextOptgroupLi.find(this.elemsSelector).first();
+            } else {
+              $nextElem = $elems.first();
+            }
+          } else {
+            $nextElem = $elems.first();
+          }
+        }
+      } else if (direction === -1){ // UP
+
+        $nextElem = $currElem.prevAll(this.elemsSelector).first();
+        if ($nextElem.length === 0){
+          var $optgroupUl = $currElem.parent();
+
+          if ($optgroupUl.hasClass('ms-optgroup')){
+            var $optgroupLi = $optgroupUl.parent(),
+                $prevOptgroupLi = $optgroupLi.prev(':visible');
+
+            if ($prevOptgroupLi.length > 0){
+              $nextElem = $prevOptgroupLi.find(this.elemsSelector).last();
+            } else {
+              $nextElem = $elems.last();
+            }
+          } else {
+            $nextElem = $elems.last();
+          }
+        }
+      }
+      if ($nextElem.length > 0){
+        $nextElem.addClass('ms-hover');
+        var scrollTo = $list.scrollTop() + $nextElem.position().top - 
+                       containerHeight / 2 + elemHeight / 2;
+
+        $list.scrollTop(scrollTo);
+      }
+    },
+
+    'selectHighlighted' : function($list){
+      var $elems = $list.find(this.elemsSelector),
+          $highlightedElem = $elems.filter('.ms-hover').first();
+
+      if ($highlightedElem.length > 0){
+        if ($list.parent().hasClass('ms-selectable')){
+          this.select($highlightedElem.data('ms-value'));
+        } else {
+          this.deselect($highlightedElem.data('ms-value'));
+        }
+        $elems.removeClass('ms-hover');
+      }
+    },
+
+    'switchList' : function($list){
+      $list.blur();
+      this.$container.find(this.elemsSelector).removeClass('ms-hover');
+      if ($list.parent().hasClass('ms-selectable')){
+        this.$selectionUl.focus();
+      } else {
+        this.$selectableUl.focus();
+      }
+    },
+
+    'activeMouse' : function($list){
+      var that = this;
+
+      this.$container.on('mouseenter', that.elemsSelector, function(){
+        $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover');
+        $(this).addClass('ms-hover');
+      });
+
+      this.$container.on('mouseleave', that.elemsSelector, function () {
+        $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover');
+      });
+    },
+
+    'refresh' : function() {
+      this.destroy();
+      this.$element.multiSelect(this.options);
+    },
+
+    'destroy' : function(){
+      $("#ms-"+this.$element.attr("id")).remove();
+      this.$element.off('focus');
+      this.$element.css('position', '').css('left', '');
+      this.$element.removeData('multiselect');
+    },
+
+    'select' : function(value, method){
+      if (typeof value === 'string'){ value = [value]; }
+
+      var that = this,
+          ms = this.$element,
+          msIds = $.map(value, function(val){ return(that.sanitize(val)); }),
+          selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable').filter(':not(.'+that.options.disabledClass+')'),
+          selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection').filter(':not(.'+that.options.disabledClass+')'),
+          options = ms.find('option:not(:disabled)').filter(function(){ return($.inArray(this.value, value) > -1); });
+
+      if (method === 'init'){
+        selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'),
+        selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection');
+      }
+
+      if (selectables.length > 0){
+        selectables.addClass('ms-selected').hide();
+        selections.addClass('ms-selected').show();
+
+        options.prop('selected', true);
+
+        that.$container.find(that.elemsSelector).removeClass('ms-hover');
+
+        var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container');
+        if (selectableOptgroups.length > 0){
+          selectableOptgroups.each(function(){
+            var selectablesLi = $(this).find('.ms-elem-selectable');
+            if (selectablesLi.length === selectablesLi.filter('.ms-selected').length){
+              $(this).find('.ms-optgroup-label').hide();
+            }
+          });
+
+          var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container');
+          selectionOptgroups.each(function(){
+            var selectionsLi = $(this).find('.ms-elem-selection');
+            if (selectionsLi.filter('.ms-selected').length > 0){
+              $(this).find('.ms-optgroup-label').show();
+            }
+          });
+        } else {
+          if (that.options.keepOrder && method !== 'init'){
+            var selectionLiLast = that.$selectionUl.find('.ms-selected');
+            if((selectionLiLast.length > 1) && (selectionLiLast.last().get(0) != selections.get(0))) {
+              selections.insertAfter(selectionLiLast.last());
+            }
+          }
+        }
+        if (method !== 'init'){
+          ms.trigger('change');
+          if (typeof that.options.afterSelect === 'function') {
+            that.options.afterSelect.call(this, value);
+          }
+        }
+      }
+    },
+
+    'deselect' : function(value){
+      if (typeof value === 'string'){ value = [value]; }
+
+      var that = this,
+          ms = this.$element,
+          msIds = $.map(value, function(val){ return(that.sanitize(val)); }),
+          selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'),
+          selections = this.$selectionUl.find('#' + msIds.join('-selection, #')+'-selection').filter('.ms-selected').filter(':not(.'+that.options.disabledClass+')'),
+          options = ms.find('option').filter(function(){ return($.inArray(this.value, value) > -1); });
+
+      if (selections.length > 0){
+        selectables.removeClass('ms-selected').show();
+        selections.removeClass('ms-selected').hide();
+        options.prop('selected', false);
+
+        that.$container.find(that.elemsSelector).removeClass('ms-hover');
+
+        var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container');
+        if (selectableOptgroups.length > 0){
+          selectableOptgroups.each(function(){
+            var selectablesLi = $(this).find('.ms-elem-selectable');
+            if (selectablesLi.filter(':not(.ms-selected)').length > 0){
+              $(this).find('.ms-optgroup-label').show();
+            }
+          });
+
+          var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container');
+          selectionOptgroups.each(function(){
+            var selectionsLi = $(this).find('.ms-elem-selection');
+            if (selectionsLi.filter('.ms-selected').length === 0){
+              $(this).find('.ms-optgroup-label').hide();
+            }
+          });
+        }
+        ms.trigger('change');
+        if (typeof that.options.afterDeselect === 'function') {
+          that.options.afterDeselect.call(this, value);
+        }
+      }
+    },
+
+    'select_all' : function(){
+      var ms = this.$element,
+          values = ms.val();
+
+      ms.find('option:not(":disabled")').prop('selected', true);
+      this.$selectableUl.find('.ms-elem-selectable').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').hide();
+      this.$selectionUl.find('.ms-optgroup-label').show();
+      this.$selectableUl.find('.ms-optgroup-label').hide();
+      this.$selectionUl.find('.ms-elem-selection').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').show();
+      this.$selectionUl.focus();
+      ms.trigger('change');
+      if (typeof this.options.afterSelect === 'function') {
+        var selectedValues = $.grep(ms.val(), function(item){
+          return $.inArray(item, values) < 0;
+        });
+        this.options.afterSelect.call(this, selectedValues);
+      }
+    },
+
+    'deselect_all' : function(){
+      var ms = this.$element,
+          values = ms.val();
+
+      ms.find('option').prop('selected', false);
+      this.$selectableUl.find('.ms-elem-selectable').removeClass('ms-selected').show();
+      this.$selectionUl.find('.ms-optgroup-label').hide();
+      this.$selectableUl.find('.ms-optgroup-label').show();
+      this.$selectionUl.find('.ms-elem-selection').removeClass('ms-selected').hide();
+      this.$selectableUl.focus();
+      ms.trigger('change');
+      if (typeof this.options.afterDeselect === 'function') {
+        this.options.afterDeselect.call(this, values);
+      }
+    },
+
+    sanitize: function(value){
+      var hash = 0, i, character;
+      if (value.length == 0) return hash;
+      var ls = 0;
+      for (i = 0, ls = value.length; i < ls; i++) {
+        character  = value.charCodeAt(i);
+        hash  = ((hash<<5)-hash)+character;
+        hash |= 0; // Convert to 32bit integer
+      }
+      return hash;
+    }
+  };
+
+  /* MULTISELECT PLUGIN DEFINITION
+   * ======================= */
+
+  $.fn.multiSelect = function () {
+    var option = arguments[0],
+        args = arguments;
+
+    return this.each(function () {
+      var $this = $(this),
+          data = $this.data('multiselect'),
+          options = $.extend({}, $.fn.multiSelect.defaults, $this.data(), typeof option === 'object' && option);
+
+      if (!data){ $this.data('multiselect', (data = new MultiSelect(this, options))); }
+
+      if (typeof option === 'string'){
+        data[option](args[1]);
+      } else {
+        data.init();
+      }
+    });
+  };
+
+  $.fn.multiSelect.defaults = {
+    keySelect: [32],
+    selectableOptgroup: false,
+    disabledClass : 'disabled',
+    dblClick : false,
+    keepOrder: false,
+    cssClass: ''
+  };
+
+  $.fn.multiSelect.Constructor = MultiSelect;
+
+  $.fn.insertAt = function(index, $parent) {
+    return this.each(function() {
+      if (index === 0) {
+        $parent.prepend(this);
+      } else {
+        $parent.children().eq(index - 1).after(this);
+      }
+    });
+};
+
+}(window.jQuery);

+ 8 - 1
templates/layout.html

@@ -143,7 +143,6 @@
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/maps/vector-maps/vectormaps.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/markdown/markdown.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/modal/modal.css') }}">
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/multi-select/multiselect.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/multi-upload/fileupload.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/nestable/nestable.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='widgets/noty-notifications/noty.css') }}">
@@ -201,6 +200,9 @@
     <!-- formvalidation CSS -->
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='formvalidation/css/formValidation.min.css') }}">
 
+    <!-- Multi-select CSS -->
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='multi-select/css/multi-select.css') }}">
+
     <!-- JimV -->
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/icons.css') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/host-table.css') }}">
@@ -464,6 +466,11 @@
         <!--<script type="text/javascript" src="{{ url_for('static', filename='formvalidation/js/formValidation.popular.min.js') }}"></script>-->
         <script type="text/javascript" src="{{ url_for('static', filename='formvalidation/js/language/zh_CN.js') }}"></script>
 
+        <!-- Multi-select -->
+        <script type="text/javascript" src="{{ url_for('static', filename='multi-select/js/jquery.multi-select.js') }}"></script>
+        <!-- Quicksearch -->
+        <script type="text/javascript" src="{{ url_for('static', filename='js-core/jquery.quicksearch.js') }}"></script>
+
         <!-- JS-URL -->
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/url.js') }}"></script>
         <!-- JimV -->

+ 160 - 30
templates/ssh_keys_show.html

@@ -66,7 +66,6 @@
             background: #fafaff !important;
         }
     </style>
-
 {% endblock head %}
 {% block content %}
 
@@ -148,6 +147,14 @@
             //
         });
 
+        $('#ssh_key_bind_modal').on('show.bs.modal', function (me) {
+            refresh_multi_selector($('#bind_multi_selector'), false);
+        });
+
+        $('#ssh_key_unbind_modal').on('show.bs.modal', function (me) {
+            refresh_multi_selector($('#unbind_multi_selector'), true);
+        });
+
         $('#add_ssh_key_form').formValidation({
             framework: 'bootstrap4',
             icon: {
@@ -184,7 +191,10 @@
             if (data.fv.getInvalidFields().length > 0) {    // There is invalid field
                 data.fv.disableSubmitButtons(true);
             }
-        })
+        });
+
+        init_multi_selector($('#bind_multi_selector'));
+        init_multi_selector($('#unbind_multi_selector'));
     });
 
     function refresh() {
@@ -315,6 +325,138 @@
         remove(ssh_key_id);
         $('#delete_modal').modal('hide');
     }
+
+    function init_multi_selector(multi_selector) {
+
+        function button_disable() {
+
+            var the_button = $('#ssh_key_bind_button');
+            if (multi_selector.attr('id') !== 'bind_multi_selector'){
+                the_button = $('#ssh_key_unbind_button');
+            }
+
+            if (multi_selector.val() !== null) {
+                the_button.removeAttr('disabled');
+            } else {
+                the_button.prop('disabled', 'true');
+            }
+        }
+
+        multi_selector.multiSelect({
+            selectableHeader: "<input type='text' class='form-control' autocomplete='off' placeholder='关键字查找...'>",
+            selectionHeader: "<input type='text' class='form-control' autocomplete='off' placeholder='关键字查找...'>",
+            afterInit: function(ms){
+                var that = this,
+                    $selectableSearch = that.$selectableUl.prev(),
+                    $selectionSearch = that.$selectionUl.prev(),
+                    selectableSearchString = '#'+that.$container.attr('id')+' .ms-elem-selectable:not(.ms-selected)',
+                    selectionSearchString = '#'+that.$container.attr('id')+' .ms-elem-selection.ms-selected';
+
+                ms.css({ width: '100%' });
+
+                that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
+                    .on('keydown', function(e){
+                        if (e.which === 40){
+                            that.$selectableUl.focus();
+                            return false;
+                        }
+                    });
+
+                that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
+                    .on('keydown', function(e){
+                        if (e.which == 40){
+                            that.$selectionUl.focus();
+                            return false;
+                        }
+                    });
+            },
+            afterSelect: function(){
+                this.qs1.cache();
+                this.qs2.cache();
+
+                button_disable();
+            },
+            afterDeselect: function(){
+                this.qs1.cache();
+                this.qs2.cache();
+
+                button_disable();
+            }
+        });
+    }
+    
+    function refresh_multi_selector(multi_selector, is_bound) {
+        var ssh_key_id = $('#ssh_key_id').val();
+        var url = '/api/ssh_key/_bound/';
+        if (!is_bound) {
+            url = '/api/ssh_key/_unbound/';
+        }
+
+        $.ajax({
+            url : url + ssh_key_id,
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+                alter_danger('获取 SSH-Key 绑定信息失败!');
+            },
+            success : function(data, textStatus, xhr) {
+                multi_selector.multiSelect({});
+                $.each(data.data, function(k, v) {
+                    multi_selector.multiSelect('addOption', {value: v['uuid'], text: v['label'] + '/' + v['ip']});
+                });
+                multi_selector.multiSelect('refresh');
+            }
+        });
+    }
+
+    function ssh_key_bind() {
+        $('#ssh_key_bind_modal').modal('hide');
+
+        var ssh_key_id = $('#ssh_key_id').val();
+        var uuids = $('#bind_multi_selector').val();
+        var url = '/api/ssh_key/_bind/';
+
+        $.ajax({
+            url : url + ssh_key_id + '/' + uuids.join(','),
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+                alter_danger('绑定 SSH-Key 失败!');
+            },
+            success : function(data, textStatus, xhr) {
+                alter_success('绑定 SSH-Key 成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
+
+    function ssh_key_unbind() {
+        $('#ssh_key_unbind_modal').modal('hide');
+
+        var ssh_key_id = $('#ssh_key_id').val();
+        var uuids = $('#unbind_multi_selector').val();
+        var url = '/api/ssh_key/_unbind/';
+
+        $.ajax({
+            url : url + ssh_key_id + '/' + uuids.join(','),
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+                alter_danger('解绑 SSH-Key 失败!');
+            },
+            success : function(data, textStatus, xhr) {
+                alter_success('解绑 SSH-Key 成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
 </script>
 <div class="panel">
     <div class="panel-body">
@@ -367,9 +509,9 @@
                     </td>
                     <td>
                         <div class="dropdown inline-block">
-                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#ssh_key_bind_modal">绑定</a>
+                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#ssh_key_bind_modal" onclick="$('#ssh_key_id').val($(this).parent().parent().parent().children()[0].textContent)">绑定</a>
                             <span> | </span>
-                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#ssh_key_unbind_modal">解绑</a>
+                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#ssh_key_unbind_modal" onclick="$('#ssh_key_id').val($(this).parent().parent().parent().children()[0].textContent)">解绑</a>
                             <span> | </span>
                             <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
                                 更多
@@ -532,25 +674,19 @@
             </div>
             <div class="modal-body" style="padding-top: 0; padding-bottom: 0;">
                 <div class="example-box-wrapper">
-                    <form id="add_ssh_key_form" class="form-horizontal bordered-row" action="/ssh_key" method="post">
-                        <div class="form-group">
-                            <div class="col-sm-2"></div>
-                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-elusive-compass-circled"></span>&nbsp;&nbsp;名称</label>
-                            <div class="col-sm-6">
-                                <input id="label" name="label" type="text" title="SSH Key 名称" class="form-control">
-                            </div>
-                        </div>
+                    <form id="ssh_key_bind_form" class="form-horizontal bordered-row" action="javascript;">
                         <div class="form-group">
-                            <div class="col-sm-2"></div>
-                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-newspaper-o"></span>&nbsp;&nbsp;Public Key</label>
-                            <div class="col-sm-6">
-                                <textarea id="public_key" name="public_key" title="Public key" class="form-control"></textarea>
+                            <div class="col-sm-1"></div>
+                            <div class="col-sm-10">
+                                <select id='bind_multi_selector' multiple='multiple' title="绑定 SSH-Key">
+                                </select>
                             </div>
+                            <div class="col-sm-1"></div>
                         </div>
                         <div class="form-group">
                             <div class="col-sm-4"></div>
                             <div class="col-sm-6 pull-right">
-                                <button type="submit" class="btn btn-blue-alt" style="width: 180px; height: 40px; font-size: 16px;" disabled>创建</button>
+                                <button id="ssh_key_bind_button" type="button" class="btn btn-blue-alt" style="width: 180px; height: 40px; font-size: 16px;" disabled onclick="ssh_key_bind(this);">绑定</button>
                                 <button class="btn btn-default" style="width: 64px; height: 40px; font-size: 16px;" data-dismiss="modal">取消</button>
                             </div>
                         </div>
@@ -568,25 +704,19 @@
             </div>
             <div class="modal-body" style="padding-top: 0; padding-bottom: 0;">
                 <div class="example-box-wrapper">
-                    <form id="add_ssh_key_form" class="form-horizontal bordered-row" action="/ssh_key" method="post">
+                    <form id="ssh_key_unbind_form" class="form-horizontal bordered-row" action="javascript;">
                         <div class="form-group">
-                            <div class="col-sm-2"></div>
-                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-elusive-compass-circled"></span>&nbsp;&nbsp;名称</label>
-                            <div class="col-sm-6">
-                                <input id="label" name="label" type="text" title="SSH Key 名称" class="form-control">
-                            </div>
-                        </div>
-                        <div class="form-group">
-                            <div class="col-sm-2"></div>
-                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-newspaper-o"></span>&nbsp;&nbsp;Public Key</label>
-                            <div class="col-sm-6">
-                                <textarea id="public_key" name="public_key" title="Public key" class="form-control"></textarea>
+                            <div class="col-sm-1"></div>
+                            <div class="col-sm-10">
+                                <select id='unbind_multi_selector' multiple='multiple' title="解绑 SSH Key">
+                                </select>
                             </div>
+                            <div class="col-sm-1"></div>
                         </div>
                         <div class="form-group">
                             <div class="col-sm-4"></div>
                             <div class="col-sm-6 pull-right">
-                                <button type="submit" class="btn btn-blue-alt" style="width: 180px; height: 40px; font-size: 16px;" disabled>创建</button>
+                                <button id="ssh_key_unbind_button" type="button" class="btn btn-blue-alt" style="width: 180px; height: 40px; font-size: 16px;" disabled onclick="ssh_key_unbind(this);">解绑</button>
                                 <button class="btn btn-default" style="width: 64px; height: 40px; font-size: 16px;" data-dismiss="modal">取消</button>
                             </div>
                         </div>