crowbar/crowbar-core

View on GitHub
crowbar_framework/vendor/assets/javascripts/jquery/nodeList.js

Summary

Maintainability
F
3 days
Test Coverage
;(function($, doc, win) {
  'use strict';

  function NodeList(el) {
    this.$root = $(el);

    this.dataBag = {
      removedOld: false,
      roleTarget: '.dropzone ul[data-id=\'{0}\']'
    };

    this.init();
  }

  NodeList.prototype.init = function() {
    this.errorTemplate = Handlebars.compile(
      $('#nodelist-alert').html()
    );

    this.warningTemplate = Handlebars.compile(
      $('#nodelist-warning').html()
    );

    this.itemTemplate = Handlebars.compile(
      $('#nodelist-item').html()
    );

    this.retrieveAvailable();
    this.retrieveConstraints();
    this.retrieveInput();
    this.retrieveAllocated();

    this.initDraggable();
    this.initDroppable();

    this.registerEvents();

    this.$root.find("ul, li").disableSelection();
  };

  NodeList.prototype.retrieveAvailable = function() {
    this.available = this.$root.find('.available[data-draggable=true]');
    this.handles = $.map(this.available, function(node, index) { return $(node).data('id'); });
  };

  NodeList.prototype.retrieveConstraints = function() {
    this.constraints = this.$root.data('constraints');
  };

  NodeList.prototype.retrieveInput = function() {
    this.input = $('#proposal_deployment');
    this.json = JSON.parse(this.input.val());
  };

  NodeList.prototype.retrieveAllocated = function() {
    var self = this;

    $.each(self.json.elements, function(role, nodes) {
      var $role = $(self.dataBag.roleTarget.format(role));
      var toRemove = [];

      $.each(nodes, function(index, node) {
        var source = self.$root.find(
          '.dragzone li[data-id=\'{0}\']'.format(node)
        );

        if ($.inArray(node, self.handles) >= 0) {
          self.insertNode(
            role,
            node,
            source.data('alias'),
            source.data('admin'),
            source.data('platform'),
            source.data('platform-version'),
            source.data('cluster'),
            source.data('remotes'),
            true
          );
        } else {
          // We need to handle the case when the referenced node has been
          // removed by another proposal, and there is no corresponding element
          // in the node list. In that case, the alias is no longer available.
          if (source.length == 0) {
            $(document).trigger(
              'nodeListNodeUnallocated',
              {
                role: role,
                id: node,
                alias: undefined
              }
            );
          } else {
            $(document).trigger(
              'nodeListNodeUnallocated',
              {
                role: role,
                id: source.data('id'),
                alias: source.data('alias')
              }
            );
          }
          toRemove.push(index);
        }
      });

      if (toRemove.length > 0) {
        $.each(toRemove.reverse(), function(index, value) {
          self.json.elements[role].remove(value);
        });

        self.updateJson();
        self.removedNodes();
      }
    });
  };

  NodeList.prototype.initDraggable = function() {
    var self = this;

    this.$root.find("[data-draggable=true]").draggable({
      opacity: 0.7,
      helper: "clone",
      revert: "invalid"
    });
  };

  NodeList.prototype.initDroppable = function() {
    var self = this;

    this.$root.find("[data-droppable=true]").droppable({
      hoverClass: 'targeted',
      drop: function(event, ui) {
        var $role = $(event.target);
        var $node = $(ui.draggable.context);

        return self.insertNode(
          $role.data('id'),
          $node.data('id'),
          $node.data('alias'),
          $node.data('admin'),
          $node.data('platform'),
          $node.data('platform-version'),
          $node.data('cluster'),
          $node.data('remotes'),
          false
        );
      }
    });
  };

  NodeList.prototype.registerEvents = function() {
    var self = this;

    $('.dropzone .delete').live('click', function(event) {
      event.preventDefault();
      var $node = $(this).parent();

      var id = $node.data('id');
      var alias = $node.data('alias');
      var role = $node.data('role');

      if (self.json.elements[role]) {
        $(document).trigger(
          'nodeListNodeUnallocated',
          {
            role: role,
            id: id,
            alias: alias
          }
        );

        self.json.elements[role].removeValue(id);
      }

      self.updateJson();
      $node.remove();
    });

    $('.dropzone .unassign').live('click', function(event) {
      event.preventDefault();
      var role = $(this).data('id');

      var $role = $(
        'ul[data-droppable=true][data-id={0}]'.format(
          role
        )
      );

      var nodes = $role.find('[data-role={0}]'.format(role));

      $.each(nodes, function(index, node) {
        var $node = $(node);
        var id = $node.data('id');
        var alias = $node.data('alias');

        $(document).trigger(
          'nodeListNodeUnallocated',
          {
            role: role,
            id: id,
            alias: alias
          }
        );
      });

      $role.html('');
      self.json.elements[role] = [];
      self.updateJson();
    });
  };

  NodeList.prototype.updateJson = function() {
    var self = this;

    self.input.val(
      JSON.stringify(self.json)
    ).trigger('change');
  };

  NodeList.prototype.errorMessage = function(message) {
    var self = this;

    self.$root.before(
      self.errorTemplate({
        message: message
      })
    );

    var message = $('.alert-danger.alert-dismissable.disappear:last');

    win.setTimeout(
      function() {
        message.slideUp(500, function() {
          $(this).remove();
        });
      },
      10000
    );
  };

  NodeList.prototype.insertNode = function(role, id, alias, admin, platform, platform_version, cluster, remotes, initial) {
    var self = this;
    var $role = $(self.dataBag.roleTarget.format(role));

    if (self.constraints) {
      var constraints = self.constraints[role];
    } else {
      var constraints = {};
    }

    if (self.json.elements[role] == undefined) {
      self.json.elements[role] = [];
    }

    if (!initial) {
      if ($.inArray(id, self.json.elements[role]) >= 0) {
        switch([cluster, remotes]) {
          case [true, false]:
            var key = 'barclamp.node_selector.cluster_duplicate';
            break;
          case [false, true]:
            var key = 'barclamp.node_selector.remotes_duplicate';
            break;
          default:
            var key = 'barclamp.node_selector.node_duplicate';
            break;
        }

        return self.errorMessage(
          key.localize().format(
            alias,
            role
          )
        );
      }
    }

    if (!initial && constraints) {
      if (constraints.admin == undefined || !constraints.admin) {
        if (admin) {
          return self.errorMessage(
            'barclamp.node_selector.no_admin'.localize().format(
              alias,
              role
            )
          );
        }
      }

      if (constraints.cluster == undefined || !constraints.cluster) {
        if (cluster) {
          return self.errorMessage(
            'barclamp.node_selector.no_cluster'.localize().format(
              alias,
              role
            )
          );
        }
      }

      if (constraints.remotes == undefined || !constraints.remotes) {
        if (remotes) {
          return self.errorMessage(
            'barclamp.node_selector.no_remotes'.localize().format(
              alias,
              role
            )
          );
        }
      }

      if (constraints.conflicts_with !== undefined && constraints.conflicts_with) {
        var conflicts = $.grep(constraints.conflicts_with, function(conflicting_role) {
          return !!self.json.elements[conflicting_role] && ($.inArray(id, self.json.elements[conflicting_role]) >= 0);
        });

        if (conflicts.length > 0) {
          return self.errorMessage(
            'barclamp.node_selector.conflicting_roles'.localize().format(
              alias,
              role,
              constraints.conflicts_with.join(', ')
            )
          );
        }
      }

      if (constraints.unique !== undefined && constraints.unique)  {
        var inserted = $.unique(
          $.map(
            self.json.elements,
            function(nodes, role) {
              return nodes;
            }
          )
        );

        if ($.inArray(id, inserted) >= 0) {
          return self.errorMessage(
            'barclamp.node_selector.unique'.localize().format(
              alias,
              role
            )
          );
        }
      }

      if (constraints.count !== undefined && constraints.count >= 0) {
        switch (constraints.count) {
          case 0:
            return self.errorMessage(
              'barclamp.node_selector.zero'.localize().format(
                alias,
                role
              )
            );

            break;
          default:
            if (self.json.elements[role].length >= constraints.count) {
              return self.errorMessage(
                'barclamp.node_selector.max_count'.localize().format(
                  alias,
                  role
                )
              );
            }

            break;
        }
      }

      if (!cluster && !remotes) {
        // Don't do platform checks for clusters; clusters don't include platform information
        // so, for example, if a constraint specifies a specific platform requirement, the
        // check will always fail because there's no platform set for the cluster.

        if (constraints.platform !== undefined && constraints.platform) {
          var platforms = $.map(constraints.platform, function(c_version, c_platform) {
            return (platform === c_platform) && self.equalOrMatchVersion(platform_version, c_version);
          });
          var is_any_true = platforms.some(function (element, index, array) {
            return element;
          });
          if (!is_any_true) {
            return self.errorMessage(
              'barclamp.node_selector.platform'.localize().format(
                alias,
                role
              )
            );
          }
        }

        if (constraints.exclude_platform !== undefined && constraints.exclude_platform) {
          var platforms = $.map(constraints.exclude_platform, function(c_version, c_platform) {
            return (platform === c_platform) && self.equalOrMatchVersion(platform_version, c_version);
          });
          var is_any_true = platforms.some(function (element, index, array) {
            return element;
          });
          if (is_any_true) {
            return self.errorMessage(
              'barclamp.node_selector.exclude_platform'.localize().format(
                alias,
                role
              )
            );
          }
        }
      }
    }

    $role.append(
      self.itemTemplate({
        role: role,
        id: id,
        alias: alias,
        admin: admin,
        cluster: cluster,
        remotes: remotes
      })
    );

    $role.html(
      $role.children().sort(function(a, b) {
        return $(a).text().toUpperCase().localeCompare($(b).text().toUpperCase());
      })
    );

    if ($.inArray(id, self.json.elements[role]) < 0) {
      self.json.elements[role].push(id);
      self.updateJson();
    }

    $(document).trigger('nodeListNodeAllocated', { role: role, id: id, alias: alias });

    return true;
  };

  NodeList.prototype.removedNodes = function() {
    if (this.dataBag.removedOld) {
      return;
    }

    this.$root.before(
      this.warningTemplate({
        message: 'barclamp.node_selector.outdated'.localize()
      })
    );

    this.dataBag.removedOld = true;
  };

  NodeList.prototype.equalOrMatchVersion = function(value, maybe_regexp) {
    var to_version = function(version) {
      var match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
      if (match) {
        return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
      }

      match = /^(\d+)\.(\d+)$/.exec(version);
      if (match) {
        return [parseInt(match[1]), parseInt(match[2]), 0];
      }

      match = /^(\d+)$/.exec(version);
      if (match) {
        return [parseInt(match[1]), 0, 0];
      }

      return [0, 0, 0];
    };

    var cmp = function(version1, version2) {
      var ans = 0;
      // version1 and version2 are arrays of the same length.  At this
      // point this assertion is true:
      // assert(version1.length == version2.length)
      for (var i=0; i<version1.length; i++) {
        if (version1[i] < version2[i]) {
          ans = -1;
        } else if (version1[i] > version2[i]) {
          ans = 1;
        }
        if (ans != 0) {
          return ans;
        }
      }
      return 0;
    };

    var oper = function(op, version1, version2) {
      var ans = cmp(to_version(version1), to_version(version2));
      switch (op) {
        case ">":
          return ans === 1;
        case ">=":
          return ans === 1 || ans === 0;
        case "<":
          return ans === -1;
        case "<=":
          return ans === -1 || ans === 0;
        case "==":
          return ans === 0;
        default:
          return false;
      }
    };

    // First group contains the RegExp without pre and post '/'
    var is_re = /^\/(.*)\/$/.exec(maybe_regexp);
    if (is_re) {
      var re = new RegExp(is_re[1]);
      return re.test(value)
    }

    var op_value = /^(>=?|<=?)\s*([.\d]+)$/.exec(maybe_regexp);
    if (op_value) {
      return oper(op_value[1], value, op_value[2]);
    } else {
      return oper('==', value, maybe_regexp);
    }
  };

  $.fn.nodeList = function() {
    return this.each(function() {
      new NodeList(this);
    });
  };
}(jQuery, document, window));