<commit_before>minispade.register('cluster', function() {

  /**
   * @class
   *
   * Responsible for modeling one specific cluster node.
   */
  RiakControl.CurrentClusterNode = Ember.Object.extend(
    /** @scope RiakControl.CurrentClusterNode.prototype */ {});

  /**
   * @class
   *
   * Responsible for modeling one specific cluster node.
   */
  RiakControl.StagedClusterNode = Ember.Object.extend(
    /** @scope RiakControl.StagedClusterNode.prototype */ {

    /**
     * Does the node have a replacement?
     *
     * @returns {boolean}
     */
    isReplaced: function() {
      return this.get('replacement') !== "undefined" ? true : false;
    }.property('replacement'),

    /**
     * Is the node taking an action?
     *
     * @returns {boolean}
     */
    isAction: function() {
        return this.get('action') !== "undefined" ? true : false;
    }.property('action')

  });

  /**
   * @class
   *
   * Responsible for modeling the current and staged cluster.
   */
  RiakControl.CurrentAndPlannedCluster = Ember.Object.extend(
    /** @scope RiakControl.CurrentAndPlannedCluster.prototype */ {});

  /**
   * @class
   *
   * ClusterController is responsible for display the list of nodes
   * in the cluster.  This controller is basically a placeholder and
   * wrapper around the legacy cluster page until we rewrite it.
   */
  RiakControl.ClusterController = Ember.ObjectController.extend(
    /**
     * Shares properties with RiakControl.NodesController
     */
    RiakControl.ClusterAndNodeControls,
    /** @scope RiakControl.ClusterController.prototype */ {

    /**
     * Refresh a particular cluster, giving a cluster returned as JSON,
     * and a cluster modeled in Ember.
     *
     * Not computationally efficient at all, but explicit for debugging
     * the Ember bindings and propagation.
     *
     * @returns {void}
     */
    refresh: function(newCluster, existingCluster, nodeFactory) {
      newCluster.forEach(function(node) {
        var exists = existingCluster.findProperty('name', node.name);

        // If it doesn't exist yet, add it.  If it does, update it.
        if(exists !== undefined) {
          exists.setProperties(node);
        } else {
          existingCluster.pushObject(nodeFactory.create(node));
        }
      });

      // Iterate the cluster removing nodes that shouldn't
      // be there.
      var changesOccurred    = false;
      var replacementCluster = [];

      existingCluster.forEach(function(node, i) {
        var exists = newCluster.findProperty('name', node.name);

        if(exists === undefined) {
          node.destroy();
          changesOccurred = true;
        } else {
          replacementCluster.pushObject(node);
        }
      });

      if(changesOccurred) {
        existingCluster.set('[]', replacementCluster.get('[]'));
      }
    },

    /**
     * Load data from server.
     *
     * @returns {void}
     */
    load: function() {
      var self = this;

      $.ajax({
        type:     'GET',
        url:      '/admin/cluster',
        dataType: 'json',

        success: function(d) {
          var updatedCurrentCluster = d.cluster.current;
          var currentCurrentCluster = self.get('content.currentCluster');

          self.refresh(updatedCurrentCluster,
            currentCurrentCluster, RiakControl.CurrentClusterNode);

          var updatedStagedCluster = d.cluster.staged;

          if(updatedStagedCluster === 'ring_not_ready') {
            self.set('ringNotReady', true);
          } else {
            self.set('ringNotReady', false);
          }

          if(updatedStagedCluster === 'legacy') {
            self.set('legacyRing', true);
          } else {
            self.set('legacyRing', false);
          }

          if($.isArray(updatedStagedCluster)) {
            var currentStagedCluster = self.get('content.stagedCluster');

            self.refresh(updatedStagedCluster,
              currentStagedCluster, RiakControl.StagedClusterNode);
          }
        },

        error: function (jqXHR, textStatus, errorThrown) {
          if(jqXHR.status === 404 || jqXHR.status === 0) {
            self.get('displayError').call(self, undefined, undefined, "The node hosting Riak Control is unavailable.");
          } else {
            self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
          }
        }
      });
    },

    /**
     * Reload data from server.
     *
     * @returns {void}
     */
    reload: function() {
      this.load();
    },

    /**
     * Holds a boolean tracking if the ring is legacy.
     */
    legacyRing: false,

    /**
     * Holds a boolean tracking if the ring is not yet ready.
     */
    ringNotReady: false,

    /**
     * Returns whether this is a standalone node or not.
     */
    standalone: function() {
      return this.get('content.currentCluster.length') <= 1;
    }.property('content.currentCluster', 'content.currentCluster.@each'),

    /**
     * Return nodes from the current cluster which have not been deleted.
     *
     * @returns {Ember.Array}
     */
    activeCurrentCluster: function() {
      return this.get('content.currentCluster').filterProperty('isDestroyed', false);
    }.property('content.currentCluster', 'content.currentCluster.@each'),

    /**
     * Return list of nodes joining the cluster.
     *
     * @returns {Ember.Array}
     */
    joiningNodes: function() {
      return this.get('activeCurrentCluster').filterProperty('status', 'joining');
    }.property('activeCurrentCluster', 'activeCurrentCluster.@each'),

    /**
     * Determines whether or not we have any joining nodes.
     *
     * @returns {Boolean}
     */
    joiningNodesExist: function () {
      return this.get('joiningNodes').length ? true : false;
    }.property('joiningNodes'),

    /**
     * Holds a boolean tracking whether or not there are any stages in our plan.
     */
    emptyPlan: function() {
      return !this.get('activeStagedCluster.length') > 0;
    }.property('activeStagedCluster', 'activeStagedCluster.length'),

    /**
     * Return nodes from the staged cluster which have not been deleted.
     *
     * @returns {Ember.Array}
     */
    activeStagedCluster: function() {
      return this.get('content.stagedCluster').filterProperty('isDestroyed', false);
    }.property('content.stagedCluster', 'content.stagedCluster.@each'),

    commitPlan: function(ev) {
      ev.preventDefault();

      var self = this;
      var confirmed = $(document).find("[name='confirmed']:checked").val();

      if(confirmed) {
        $.ajax({
          type:     'POST',
          url:      '/admin/cluster',
          dataType: 'json',
          success:  function(d) { self.reload(); },
          error:    function (jqXHR, textStatus, errorThrown) {
            self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
          }
        });
      } else {
        self.get('displayError')(undefined, undefined, "Please confirm the plan.");
      }
    },

    /**
     * Clear the currently staged cluster plan.
     *
     * @returns {void}
     */
    clearPlan: function(ev) {
      ev.preventDefault();

      var self = this;

      $.ajax({
        type:     'DELETE',
        url:      '/admin/cluster',
        dataType: 'json',
        success:  function(d) { self.reload(); },
        error:    function (jqXHR, textStatus, errorThrown) {
          self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
        }
      });
    },

    /**
     * Join a node.
     *
     * @returns {void}
     */
    joinNode: function(ev) {
      ev.preventDefault();

      var self    = this;
      var node    = this.get('joinNodeField');
      var success = function() {
        self.set('joinNodeField', undefined);
      };

      this.stageChange(node, "join", "", success, undefined);
    },

    /**
     * There are various reasons we wouldn't want to display
     * the planned cluster.  If none of those reasons are present,
     * go ahead and show the whole planned cluster view.
     *
     * @returns {boolean}
     */
    displayPlan: function () {
      return !this.get('isLoading') && !this.get('ringNotReady') &&
             !this.get('emptyPlan') && !this.get('legacyRing');
    }.property('isLoading', 'ringNotReady', 'emptyPlan', 'legacyRing')

  });

  /**
   * @class
   *
   * Join Node text field.
   *
   */
  RiakControl.JoinNodeView = Ember.TextField.extend(
    /** @scope RiakControl.JoinNodeView.prototype */ {
    valueBinding: 'controller.joinNodeField',
    classNames: ['gui-input', 'gui-text'],

    /**
     * When the user presses the enter/return key in the
     * add nodes field, calls the 'joinNode' method of
     * the controller.
     *
     * @param {Object} ev - The keyup event.
     *
     * @returns {void}
     */
    keyUp: function (ev) {
      var controller = this.get('controller');
      if(ev.keyCode === 13){
        controller.get('joinNode').call(controller, ev);
      }
    }
  });

  /**
   * @class
   *
   * ClusterView is responsible for display the list of nodes
   * in the cluster.  This controller is basically a placeholder and
   * wrapper around the legacy cluster page until we rewrite it.
   */
  RiakControl.ClusterView = Ember.View.extend(
    /** @scope RiakControl.ClusterView.prototype */ {
    templateName: 'cluster'
  });

  /**
   * @class
   *
   * Toggle button for a current cluster node to expand actions.
   */
  RiakControl.CurrentClusterToggleView = Ember.View.extend(
    /** @scope RiakControl.CurrentClusterToggleView.prototype */ {

    /**
     * Handle click event on the action toggle.
     *
     * @returns {void}
     */
    click: function () {
      var prop = this.get('parentView.expanded');

      if (prop) {
        this.get('parentView').set('expanded', false);
      } else {
        this.get('parentView').set('expanded', true);
      }
    }
  });

  /**
   * @class
   *
   * One item in the collection of current cluster views.
   */
  RiakControl.CurrentClusterItemView = Ember.View.extend(
    /**
     * Shares properties with other views that display lists of nodes.
     */
    RiakControl.NodeProperties,
    /** @scope RiakControl.CurrentClusterItemView.prototype */ {

    /* Bindings from the model */

    templateName:       'current_cluster_item',
    nameBinding:        'content.name',
    reachableBinding:   'content.reachable',
    statusBinding:      'content.status',
    ring_pctBinding:    'content.ring_pct',
    pending_pctBinding: 'content.pending_pct',
    mem_totalBinding:   'content.mem_total',
    mem_usedBinding:    'content.mem_used',
    mem_erlangBinding:  'content.mem_erlang',
    meBinding:          'content.me',

    classNameBindings:  ['expanded:open'],

    /**
     * Stage a change for a given node.
     *
     * @returns {void}
     */
    stageChange: function(ev) {
      ev.preventDefault();

      var self = this;
      var controller = this.get('controller');

      var name = this.get('name');

      var action = this.$().
        find("input[type='radio']:checked").val();
      var forced = this.$().
        find("input[type='checkbox']:checked").val();
      var replacement = this.$().
        find("input[type='select']:selected").val();

      // Make sure we handle the force replace correctly.
      //
      if(action === 'replace' && forced === 'true') {
        action = 'force_replace';
      }

      // Empty string instead of undefined for null.
      if(replacement === undefined) {
        replacement = '';
      }

      controller.stageChange(name, action, replacement);
    },

    /**
     * In order for labels to be clickable, they need to be bound to checks/radios
     * by ID.  However, since these nodes are cloned by Ember, we need a way to make
     * sure all of those elements get id's that don't override each other. This
     * function gives us an ID string we can use as a prefix for id's on these other
     * elements.
     *
     * @returns {String}
     */
    nodeID: function() {
      return Ember.guidFor(this);
    }.property(),

    /**
     * An ID value for the leave normally radio button and corresponding label.
     */
    normalLeaveRadio: function () {
      return this.get('nodeID') + '_normal_leave';
    }.property('nodeID'),

    /**
     * An ID value for the force leave radio button and corresponding label.
     */
    forceLeaveRadio: function () {
      return this.get('nodeID') + '_force_leave';
    }.property('nodeID'),

    /**
     * An ID value for the replace node radio button and corresponding label.
     */
    replaceRadio: function () {
      return this.get('nodeID') + '_replace';
    }.property('nodeID'),

    /**
     * An ID value for the force replace check box and corresponding label.
     */
    forceReplaceCheck: function () {
      return this.get('nodeID') + '_force_replace';
    }.property('nodeID'),

    /**
     * When there are no joining nodes, the radio button for selecting
     * a node should be grayed out.  This will put the proper classes
     * on that radio button to gray it out when there are no joining nodes.
     */
    replaceRadioClasses: function () {
      return 'gui-radio-wrapper' + (this.get('controller.joiningNodesExist') ? '' : ' semi-transparent');
    }.property('controller.joiningNodesExist')

  });

  /**
   * @class
   *
   * Collection view for showing the current cluster.
   */
  RiakControl.CurrentClusterView = Ember.CollectionView.extend(
    /** @scope RiakControl.CurrentClusterView.prototype */ {
    itemViewClass: RiakControl.CurrentClusterItemView
  });

  /**
   * @class
   *
   * One item in the collection of current cluster views.
   */
  RiakControl.StagedClusterItemView = Ember.View.extend(
    /**
     * Shares properties with other views that display lists of nodes.
     */
    RiakControl.NodeProperties,
    /** @scope RiakControl.StagedClusterItemView.prototype */ {

    /* Bindings from the model */

    templateName:       'staged_cluster_item',
    nameBinding:        'content.name',
    statusBinding:      'content.status',
    ring_pctBinding:    'content.ring_pct',
    isReplacedBinding:  'content.isReplaced',
    isActionBinding:    'content.isAction',

    /* Necessary rename to avoid collision */
    node_actionBinding: 'content.action'

  });

  /**
   * @class
   *
   * Collection view for showing the staged cluster.
   */
  RiakControl.StagedClusterView = Ember.CollectionView.extend(
    /** @scope RiakControl.StagedClusterView.prototype */ {
    itemViewClass: RiakControl.StagedClusterItemView
  });

  /**
   * @class
   *
   * View for select menus in cluster item actions boxes.
   */
  RiakControl.ClusterItemSelectView = Ember.Select.extend({

    /*
     * When the select menu changes, update its display.
     */
    change: function () {
      var item = this.$(),
          val  = item.val();
      Ember.run.next(function() {
        item.prev().prev().text(val);
      });
    }
  });

});
<commit_msg>select menus for cluster items now have a less error-prone jquery selection<commit_after>minispade.register('cluster', function() {

  /**
   * @class
   *
   * Responsible for modeling one specific cluster node.
   */
  RiakControl.CurrentClusterNode = Ember.Object.extend(
    /** @scope RiakControl.CurrentClusterNode.prototype */ {});

  /**
   * @class
   *
   * Responsible for modeling one specific cluster node.
   */
  RiakControl.StagedClusterNode = Ember.Object.extend(
    /** @scope RiakControl.StagedClusterNode.prototype */ {

    /**
     * Does the node have a replacement?
     *
     * @returns {boolean}
     */
    isReplaced: function() {
      return this.get('replacement') !== "undefined" ? true : false;
    }.property('replacement'),

    /**
     * Is the node taking an action?
     *
     * @returns {boolean}
     */
    isAction: function() {
        return this.get('action') !== "undefined" ? true : false;
    }.property('action')

  });

  /**
   * @class
   *
   * Responsible for modeling the current and staged cluster.
   */
  RiakControl.CurrentAndPlannedCluster = Ember.Object.extend(
    /** @scope RiakControl.CurrentAndPlannedCluster.prototype */ {});

  /**
   * @class
   *
   * ClusterController is responsible for display the list of nodes
   * in the cluster.  This controller is basically a placeholder and
   * wrapper around the legacy cluster page until we rewrite it.
   */
  RiakControl.ClusterController = Ember.ObjectController.extend(
    /**
     * Shares properties with RiakControl.NodesController
     */
    RiakControl.ClusterAndNodeControls,
    /** @scope RiakControl.ClusterController.prototype */ {

    /**
     * Refresh a particular cluster, giving a cluster returned as JSON,
     * and a cluster modeled in Ember.
     *
     * Not computationally efficient at all, but explicit for debugging
     * the Ember bindings and propagation.
     *
     * @returns {void}
     */
    refresh: function(newCluster, existingCluster, nodeFactory) {
      newCluster.forEach(function(node) {
        var exists = existingCluster.findProperty('name', node.name);

        // If it doesn't exist yet, add it.  If it does, update it.
        if(exists !== undefined) {
          exists.setProperties(node);
        } else {
          existingCluster.pushObject(nodeFactory.create(node));
        }
      });

      // Iterate the cluster removing nodes that shouldn't
      // be there.
      var changesOccurred    = false;
      var replacementCluster = [];

      existingCluster.forEach(function(node, i) {
        var exists = newCluster.findProperty('name', node.name);

        if(exists === undefined) {
          node.destroy();
          changesOccurred = true;
        } else {
          replacementCluster.pushObject(node);
        }
      });

      if(changesOccurred) {
        existingCluster.set('[]', replacementCluster.get('[]'));
      }
    },

    /**
     * Load data from server.
     *
     * @returns {void}
     */
    load: function() {
      var self = this;

      $.ajax({
        type:     'GET',
        url:      '/admin/cluster',
        dataType: 'json',

        success: function(d) {
          var updatedCurrentCluster = d.cluster.current;
          var currentCurrentCluster = self.get('content.currentCluster');

          self.refresh(updatedCurrentCluster,
            currentCurrentCluster, RiakControl.CurrentClusterNode);

          var updatedStagedCluster = d.cluster.staged;

          if(updatedStagedCluster === 'ring_not_ready') {
            self.set('ringNotReady', true);
          } else {
            self.set('ringNotReady', false);
          }

          if(updatedStagedCluster === 'legacy') {
            self.set('legacyRing', true);
          } else {
            self.set('legacyRing', false);
          }

          if($.isArray(updatedStagedCluster)) {
            var currentStagedCluster = self.get('content.stagedCluster');

            self.refresh(updatedStagedCluster,
              currentStagedCluster, RiakControl.StagedClusterNode);
          }
        },

        error: function (jqXHR, textStatus, errorThrown) {
          if(jqXHR.status === 404 || jqXHR.status === 0) {
            self.get('displayError').call(self, undefined, undefined, "The node hosting Riak Control is unavailable.");
          } else {
            self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
          }
        }
      });
    },

    /**
     * Reload data from server.
     *
     * @returns {void}
     */
    reload: function() {
      this.load();
    },

    /**
     * Holds a boolean tracking if the ring is legacy.
     */
    legacyRing: false,

    /**
     * Holds a boolean tracking if the ring is not yet ready.
     */
    ringNotReady: false,

    /**
     * Returns whether this is a standalone node or not.
     */
    standalone: function() {
      return this.get('content.currentCluster.length') <= 1;
    }.property('content.currentCluster', 'content.currentCluster.@each'),

    /**
     * Return nodes from the current cluster which have not been deleted.
     *
     * @returns {Ember.Array}
     */
    activeCurrentCluster: function() {
      return this.get('content.currentCluster').filterProperty('isDestroyed', false);
    }.property('content.currentCluster', 'content.currentCluster.@each'),

    /**
     * Return list of nodes joining the cluster.
     *
     * @returns {Ember.Array}
     */
    joiningNodes: function() {
      return this.get('activeCurrentCluster').filterProperty('status', 'joining');
    }.property('activeCurrentCluster', 'activeCurrentCluster.@each'),

    /**
     * Determines whether or not we have any joining nodes.
     *
     * @returns {Boolean}
     */
    joiningNodesExist: function () {
      return this.get('joiningNodes').length ? true : false;
    }.property('joiningNodes'),

    /**
     * Holds a boolean tracking whether or not there are any stages in our plan.
     */
    emptyPlan: function() {
      return !this.get('activeStagedCluster.length') > 0;
    }.property('activeStagedCluster', 'activeStagedCluster.length'),

    /**
     * Return nodes from the staged cluster which have not been deleted.
     *
     * @returns {Ember.Array}
     */
    activeStagedCluster: function() {
      return this.get('content.stagedCluster').filterProperty('isDestroyed', false);
    }.property('content.stagedCluster', 'content.stagedCluster.@each'),

    commitPlan: function(ev) {
      ev.preventDefault();

      var self = this;
      var confirmed = $(document).find("[name='confirmed']:checked").val();

      if(confirmed) {
        $.ajax({
          type:     'POST',
          url:      '/admin/cluster',
          dataType: 'json',
          success:  function(d) { self.reload(); },
          error:    function (jqXHR, textStatus, errorThrown) {
            self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
          }
        });
      } else {
        self.get('displayError')(undefined, undefined, "Please confirm the plan.");
      }
    },

    /**
     * Clear the currently staged cluster plan.
     *
     * @returns {void}
     */
    clearPlan: function(ev) {
      ev.preventDefault();

      var self = this;

      $.ajax({
        type:     'DELETE',
        url:      '/admin/cluster',
        dataType: 'json',
        success:  function(d) { self.reload(); },
        error:    function (jqXHR, textStatus, errorThrown) {
          self.get('displayError').call(self, jqXHR, textStatus, errorThrown);
        }
      });
    },

    /**
     * Join a node.
     *
     * @returns {void}
     */
    joinNode: function(ev) {
      ev.preventDefault();

      var self    = this;
      var node    = this.get('joinNodeField');
      var success = function() {
        self.set('joinNodeField', undefined);
      };

      this.stageChange(node, "join", "", success, undefined);
    },

    /**
     * There are various reasons we wouldn't want to display
     * the planned cluster.  If none of those reasons are present,
     * go ahead and show the whole planned cluster view.
     *
     * @returns {boolean}
     */
    displayPlan: function () {
      return !this.get('isLoading') && !this.get('ringNotReady') &&
             !this.get('emptyPlan') && !this.get('legacyRing');
    }.property('isLoading', 'ringNotReady', 'emptyPlan', 'legacyRing')

  });

  /**
   * @class
   *
   * Join Node text field.
   *
   */
  RiakControl.JoinNodeView = Ember.TextField.extend(
    /** @scope RiakControl.JoinNodeView.prototype */ {
    valueBinding: 'controller.joinNodeField',
    classNames: ['gui-input', 'gui-text'],

    /**
     * When the user presses the enter/return key in the
     * add nodes field, calls the 'joinNode' method of
     * the controller.
     *
     * @param {Object} ev - The keyup event.
     *
     * @returns {void}
     */
    keyUp: function (ev) {
      var controller = this.get('controller');
      if(ev.keyCode === 13){
        controller.get('joinNode').call(controller, ev);
      }
    }
  });

  /**
   * @class
   *
   * ClusterView is responsible for display the list of nodes
   * in the cluster.  This controller is basically a placeholder and
   * wrapper around the legacy cluster page until we rewrite it.
   */
  RiakControl.ClusterView = Ember.View.extend(
    /** @scope RiakControl.ClusterView.prototype */ {
    templateName: 'cluster'
  });

  /**
   * @class
   *
   * Toggle button for a current cluster node to expand actions.
   */
  RiakControl.CurrentClusterToggleView = Ember.View.extend(
    /** @scope RiakControl.CurrentClusterToggleView.prototype */ {

    /**
     * Handle click event on the action toggle.
     *
     * @returns {void}
     */
    click: function () {
      var prop = this.get('parentView.expanded');

      if (prop) {
        this.get('parentView').set('expanded', false);
      } else {
        this.get('parentView').set('expanded', true);
      }
    }
  });

  /**
   * @class
   *
   * One item in the collection of current cluster views.
   */
  RiakControl.CurrentClusterItemView = Ember.View.extend(
    /**
     * Shares properties with other views that display lists of nodes.
     */
    RiakControl.NodeProperties,
    /** @scope RiakControl.CurrentClusterItemView.prototype */ {

    /* Bindings from the model */

    templateName:       'current_cluster_item',
    nameBinding:        'content.name',
    reachableBinding:   'content.reachable',
    statusBinding:      'content.status',
    ring_pctBinding:    'content.ring_pct',
    pending_pctBinding: 'content.pending_pct',
    mem_totalBinding:   'content.mem_total',
    mem_usedBinding:    'content.mem_used',
    mem_erlangBinding:  'content.mem_erlang',
    meBinding:          'content.me',

    classNameBindings:  ['expanded:open'],

    /**
     * Stage a change for a given node.
     *
     * @returns {void}
     */
    stageChange: function(ev) {
      ev.preventDefault();

      var self = this;
      var controller = this.get('controller');

      var name = this.get('name');

      var action = this.$().
        find("input[type='radio']:checked").val();
      var forced = this.$().
        find("input[type='checkbox']:checked").val();
      var replacement = this.$().
        find("input[type='select']:selected").val();

      // Make sure we handle the force replace correctly.
      //
      if(action === 'replace' && forced === 'true') {
        action = 'force_replace';
      }

      // Empty string instead of undefined for null.
      if(replacement === undefined) {
        replacement = '';
      }

      controller.stageChange(name, action, replacement);
    },

    /**
     * In order for labels to be clickable, they need to be bound to checks/radios
     * by ID.  However, since these nodes are cloned by Ember, we need a way to make
     * sure all of those elements get id's that don't override each other. This
     * function gives us an ID string we can use as a prefix for id's on these other
     * elements.
     *
     * @returns {String}
     */
    nodeID: function() {
      return Ember.guidFor(this);
    }.property(),

    /**
     * An ID value for the leave normally radio button and corresponding label.
     */
    normalLeaveRadio: function () {
      return this.get('nodeID') + '_normal_leave';
    }.property('nodeID'),

    /**
     * An ID value for the force leave radio button and corresponding label.
     */
    forceLeaveRadio: function () {
      return this.get('nodeID') + '_force_leave';
    }.property('nodeID'),

    /**
     * An ID value for the replace node radio button and corresponding label.
     */
    replaceRadio: function () {
      return this.get('nodeID') + '_replace';
    }.property('nodeID'),

    /**
     * An ID value for the force replace check box and corresponding label.
     */
    forceReplaceCheck: function () {
      return this.get('nodeID') + '_force_replace';
    }.property('nodeID'),

    /**
     * When there are no joining nodes, the radio button for selecting
     * a node should be grayed out.  This will put the proper classes
     * on that radio button to gray it out when there are no joining nodes.
     */
    replaceRadioClasses: function () {
      return 'gui-radio-wrapper' + (this.get('controller.joiningNodesExist') ? '' : ' semi-transparent');
    }.property('controller.joiningNodesExist')

  });

  /**
   * @class
   *
   * Collection view for showing the current cluster.
   */
  RiakControl.CurrentClusterView = Ember.CollectionView.extend(
    /** @scope RiakControl.CurrentClusterView.prototype */ {
    itemViewClass: RiakControl.CurrentClusterItemView
  });

  /**
   * @class
   *
   * One item in the collection of current cluster views.
   */
  RiakControl.StagedClusterItemView = Ember.View.extend(
    /**
     * Shares properties with other views that display lists of nodes.
     */
    RiakControl.NodeProperties,
    /** @scope RiakControl.StagedClusterItemView.prototype */ {

    /* Bindings from the model */

    templateName:       'staged_cluster_item',
    nameBinding:        'content.name',
    statusBinding:      'content.status',
    ring_pctBinding:    'content.ring_pct',
    isReplacedBinding:  'content.isReplaced',
    isActionBinding:    'content.isAction',

    /* Necessary rename to avoid collision */
    node_actionBinding: 'content.action'

  });

  /**
   * @class
   *
   * Collection view for showing the staged cluster.
   */
  RiakControl.StagedClusterView = Ember.CollectionView.extend(
    /** @scope RiakControl.StagedClusterView.prototype */ {
    itemViewClass: RiakControl.StagedClusterItemView
  });

  /**
   * @class
   *
   * View for select menus in cluster item actions boxes.
   */
  RiakControl.ClusterItemSelectView = Ember.Select.extend({

    /*
     * When the select menu changes, update its display.
     */
    change: function () {
      var item = this.$(),
          val  = item.val();
      Ember.run.next(function() {
        item.parent().find('.gui-dropdown-bg').text(val);
      });
    }
  });

});
<|endoftext|>


<commit_before>/**
 * @flow
 */

import 'instapromise';

import ProgressBar from 'progress';
import _ from 'lodash';
import bunyan from '@expo/bunyan';
import crayon from '@ccheever/crayon';
import glob from 'glob';
import path from 'path';
import simpleSpinner from '@expo/simple-spinner';
import url from 'url';

import program, { Command } from 'commander';
import {
  Analytics,
  Binaries,
  Config,
  Doctor,
  Logger,
  PackagerLogsStream,
  NotificationCode,
  Project,
  ProjectUtils,
  User as UserManager,
} from 'xdl';

import { loginOrRegisterIfLoggedOut } from './accounts';
import log from './log';
import update from './update';
import urlOpts from './urlOpts';

if (process.env.NODE_ENV === 'development') {
  require('source-map-support').install();
}

// The following prototyped functions are not used here, but within in each file found in `./commands`
// Extending commander to easily add more options to certain command line arguments
Command.prototype.urlOpts = function() {
  urlOpts.addOptions(this);
  return this;
};

Command.prototype.allowOffline = function() {
  this.option('--offline', 'Allows this command to run while offline');
  return this;
};

Command.prototype.allowNonInteractive = function() {
  this.option(
    '--non-interactive',
    'Fails if an interactive prompt would be required to continue.'
  );
  return this;
};

// asyncAction is a wrapper for all commands/actions to be executed after commander is done
// parsing the command input
Command.prototype.asyncAction = function(asyncFn, skipUpdateCheck) {
  return this.action(async (...args) => {
    if (!skipUpdateCheck) {
      try {
        await checkForUpdateAsync();
      } catch (e) {}
    }

    try {
      let options = _.last(args);
      if (options.output === 'raw') {
        log.config.raw = true;
      }
      if (options.offline) {
        Config.offline = true;
      }
      await asyncFn(...args);
      // After a command, flush the analytics queue so the program will not have any active timers
      // This allows node js to exit immediately
      Analytics.flush();
    } catch (err) {
      // TODO: Find better ways to consolidate error messages
      if (err._isCommandError) {
        log.error(err.message);
      } else if (err._isApiError) {
        log.error(crayon.red(err.message));
      } else if (err.isXDLError) {
        log.error(err.message);
      } else {
        log.error(err.message);
        // TODO: Is there a better way to do this? EXPO_DEBUG needs to be set to view the stack trace
        if (process.env.EXPO_DEBUG) {
          crayon.gray.error(err.stack);
        } else {
          log.error(
            crayon.grey(
              'Set EXPO_DEBUG=true in your env to view the stack trace.'
            )
          );
        }
      }

      process.exit(1);
    }
  });
};

// asyncActionProjectDir captures the projectDirectory from the command line,
// setting it to cwd if it is not provided.
// Commands such as `exp start` and `exp publish` use this.
// It does several things:
// - Everything in asyncAction
// - Checks if the user is logged in or out
// - Checks for updates
// - Attaches the bundling logger
// - Checks if the project directory is valid or not
// - Runs AsyncAction with the projectDir as an argument
Command.prototype.asyncActionProjectDir = function(
  asyncFn,
  skipProjectValidation,
  skipAuthCheck
) {
  return this.asyncAction(async (projectDir, ...args) => {
    try {
      await checkForUpdateAsync();
    } catch (e) {}

    const opts = args[0];
    if (!skipAuthCheck && !opts.nonInteractive && !opts.offline) {
      await loginOrRegisterIfLoggedOut();
    }

    if (!skipAuthCheck) {
      await UserManager.ensureLoggedInAsync();
    }

    if (!projectDir) {
      projectDir = process.cwd();
    } else {
      projectDir = path.resolve(process.cwd(), projectDir);
    }

    const logLines = (msg, logFn) => {
      for (let line of msg.split('\n')) {
        logFn(line);
      }
    };

    const logWithLevel = chunk => {
      if (!chunk.msg) {
        return;
      }
      if (chunk.level <= bunyan.INFO) {
        logLines(chunk.msg, log);
      } else if (chunk.level === bunyan.WARN) {
        logLines(chunk.msg, log.warn);
      } else {
        logLines(chunk.msg, log.error);
      }
    };

    let bar;
    let packagerLogsStream = new PackagerLogsStream({
      projectRoot: projectDir,
      onStartBuildBundle: () => {
        bar = new ProgressBar('Building JavaScript bundle [:bar] :percent', {
          total: 100,
          clear: true,
          complete: '=',
          incomplete: ' ',
        });

        log.setBundleProgressBar(bar);
      },
      onProgressBuildBundle: percent => {
        if (!bar || bar.complete) return;
        let ticks = percent - bar.curr;
        ticks > 0 && bar.tick(ticks);
      },
      onFinishBuildBundle: (err, startTime: Date, endTime: Date) => {
        if (bar && !bar.complete) {
          bar.tick(100 - bar.curr);
        }

        if (bar) {
          log.setBundleProgressBar(null);
          bar = null;

          if (err) {
            log(crayon.red('Failed building JavaScript bundle.'));
          } else {
            log(
              crayon.green(
                `Finished building JavaScript bundle in ${endTime -
                  startTime}ms.`
              )
            );
          }
        }
      },
      updateLogs: updater => {
        let newLogChunks = updater([]);
        newLogChunks.forEach(newLogChunk => {
          logWithLevel(newLogChunk);
        });
      },
    });

    // needed for validation logging to function
    ProjectUtils.attachLoggerStream(projectDir, {
      stream: {
        write: chunk => {
          if (chunk.tag === 'device') {
            logWithLevel(chunk);
          }
        },
      },
      type: 'raw',
    });

    // The existing CLI modules only pass one argument to this function, so skipProjectValidation
    // will be undefined in most cases. we can explicitly pass a truthy value here to avoid
    // validation (eg for init)
    //
    // If the packager/manifest server is running and healthy, there is no need
    // to rerun Doctor because the directory was already checked previously
    // This is relevant for command such as `exp send`
    if (
      !skipProjectValidation &&
      (await Project.currentStatus(projectDir)) !== 'running'
    ) {
      log('Making sure project is set up correctly...');
      simpleSpinner.start();
      // validate that this is a good projectDir before we try anything else

      let status = await Doctor.validateLowLatencyAsync(projectDir);
      if (status === Doctor.FATAL) {
        throw new Error(
          `There is an error with your project. See above logs for information.`
        );
      }
      simpleSpinner.stop();
      log('Your project looks good!');
    }

    return asyncFn(projectDir, ...args);
  }, true);
};

function runAsync() {
  try {
    // Setup analytics
    Analytics.setSegmentNodeKey('vGu92cdmVaggGA26s3lBX6Y5fILm8SQ7');
    Analytics.setVersionName(require('../package.json').version);
    _registerLogs();

    if (process.env.SERVER_URL) {
      let serverUrl = process.env.SERVER_URL;
      if (!serverUrl.startsWith('http')) {
        serverUrl = `http://${serverUrl}`;
      }
      let parsedUrl = url.parse(serverUrl);
      Config.api.host = parsedUrl.hostname;
      Config.api.port = parsedUrl.port;
    }

    Config.developerTool = 'exp';

    // Setup our commander instance
    program.name = 'exp';
    program
      .version(require('../package.json').version)
      .option('-o, --output [format]', 'Output format. pretty (default), raw');

    // Load each module found in ./commands by 'registering' it with our commander instance
    glob
      .sync('commands/*.js', {
        cwd: __dirname,
      })
      .forEach(file => {
        const commandModule = require(`./${file}`);
        if (typeof commandModule === 'function') {
          commandModule(program);
        } else if (typeof commandModule.default === 'function') {
          commandModule.default(program);
        } else {
          log.error(`'${file}.js' is not a properly formatted command.`);
        }
      });

    if (process.env.EXPO_DEBUG) {
      glob
        .sync('debug_commands/*.js', {
          cwd: __dirname,
        })
        .forEach(file => {
          require(`./${file}`)(program);
        });
    }

    // Commander will now parse argv/argc
    program.parse(process.argv);

    // Display a message if the user does not input a valid command
    let subCommand = process.argv[2];
    if (subCommand) {
      let commands = [];
      program.commands.forEach(command => {
        commands.push(command['_name']);
        let alias = command['_alias'];
        if (alias) {
          commands.push(alias);
        }
      });
      if (!_.includes(commands, subCommand)) {
        console.log(
          `"${subCommand}" is not an exp command. See "exp --help" for the full list of commands.`
        );
      }
    } else {
      program.help();
    }
  } catch (e) {
    console.error(e);
    throw e;
  }
}

async function checkForUpdateAsync() {
  let { state, current, latest } = await update.checkForExpUpdateAsync();
  let message;
  switch (state) {
    case 'up-to-date':
      break;

    case 'out-of-date':
      message = `There is a new version of exp available (${latest}).
You are currently using exp ${current}
Run \`npm install -g exp\` to get the latest version`;
      crayon.green.error(message);
      break;

    case 'ahead-of-published':
      // if the user is ahead of npm, we're going to assume they know what they're doing
      break;

    default:
      log.error('Confused about what version of exp you have?');
  }
}

function _registerLogs() {
  let stream = {
    stream: {
      write: chunk => {
        if (chunk.code) {
          switch (chunk.code) {
            case NotificationCode.START_LOADING:
              simpleSpinner.start();
              return;
            case NotificationCode.STOP_LOADING:
              simpleSpinner.stop();
              return;
            case NotificationCode.DOWNLOAD_CLI_PROGRESS:
              return;
          }
        }

        if (chunk.level === bunyan.INFO) {
          log(chunk.msg);
        } else if (chunk.level === bunyan.WARN) {
          log.warn(chunk.msg);
        } else if (chunk.level >= bunyan.ERROR) {
          log.error(chunk.msg);
        }
      },
    },
    type: 'raw',
  };

  Logger.notifications.addStream(stream);
  Logger.global.addStream(stream);
}

async function writePathAsync() {
  let subCommand = process.argv[2];
  if (subCommand === 'prepare-detached-build') {
    // This is being run from Android Studio or Xcode. Don't want to write PATH in this case.
    return;
  }

  await Binaries.writePathToUserSettingsAsync();
}

// This is the entry point of the CLI
export function run() {
  (async function() {
    await Promise.all([writePathAsync(), runAsync()]);
  })().catch(e => {
    console.error('Uncaught Error', e);
    process.exit(1);
  });
}
<commit_msg>Revert "Revert "[schemer][xdl] Add schema validation library""<commit_after>/**
 * @flow
 */

import 'instapromise';

import ProgressBar from 'progress';
import _ from 'lodash';
import bunyan from '@expo/bunyan';
import crayon from '@ccheever/crayon';
import glob from 'glob';
import path from 'path';
import simpleSpinner from '@expo/simple-spinner';
import url from 'url';

import program, { Command } from 'commander';
import {
  Analytics,
  Binaries,
  Config,
  Doctor,
  Logger,
  PackagerLogsStream,
  NotificationCode,
  Project,
  ProjectUtils,
  User as UserManager,
} from 'xdl';

import { loginOrRegisterIfLoggedOut } from './accounts';
import log from './log';
import update from './update';
import urlOpts from './urlOpts';

if (process.env.NODE_ENV === 'development') {
  require('source-map-support').install();
}

// The following prototyped functions are not used here, but within in each file found in `./commands`
// Extending commander to easily add more options to certain command line arguments
Command.prototype.urlOpts = function() {
  urlOpts.addOptions(this);
  return this;
};

Command.prototype.allowOffline = function() {
  this.option('--offline', 'Allows this command to run while offline');
  return this;
};

Command.prototype.allowNonInteractive = function() {
  this.option(
    '--non-interactive',
    'Fails if an interactive prompt would be required to continue.'
  );
  return this;
};

// asyncAction is a wrapper for all commands/actions to be executed after commander is done
// parsing the command input
Command.prototype.asyncAction = function(asyncFn, skipUpdateCheck) {
  return this.action(async (...args) => {
    if (!skipUpdateCheck) {
      try {
        await checkForUpdateAsync();
      } catch (e) {}
    }

    try {
      let options = _.last(args);
      if (options.output === 'raw') {
        log.config.raw = true;
      }
      if (options.offline) {
        Config.offline = true;
      }
      await asyncFn(...args);
      // After a command, flush the analytics queue so the program will not have any active timers
      // This allows node js to exit immediately
      Analytics.flush();
    } catch (err) {
      // TODO: Find better ways to consolidate error messages
      if (err._isCommandError) {
        log.error(err.message);
      } else if (err._isApiError) {
        log.error(crayon.red(err.message));
      } else if (err.isXDLError) {
        log.error(err.message);
      } else {
        log.error(err.message);
        // TODO: Is there a better way to do this? EXPO_DEBUG needs to be set to view the stack trace
        if (process.env.EXPO_DEBUG) {
          crayon.gray.error(err.stack);
        } else {
          log.error(
            crayon.grey(
              'Set EXPO_DEBUG=true in your env to view the stack trace.'
            )
          );
        }
      }

      process.exit(1);
    }
  });
};

// asyncActionProjectDir captures the projectDirectory from the command line,
// setting it to cwd if it is not provided.
// Commands such as `exp start` and `exp publish` use this.
// It does several things:
// - Everything in asyncAction
// - Checks if the user is logged in or out
// - Checks for updates
// - Attaches the bundling logger
// - Checks if the project directory is valid or not
// - Runs AsyncAction with the projectDir as an argument
Command.prototype.asyncActionProjectDir = function(
  asyncFn,
  skipProjectValidation,
  skipAuthCheck
) {
  return this.asyncAction(async (projectDir, ...args) => {
    try {
      await checkForUpdateAsync();
    } catch (e) {}

    const opts = args[0];
    if (!skipAuthCheck && !opts.nonInteractive && !opts.offline) {
      await loginOrRegisterIfLoggedOut();
    }

    if (!skipAuthCheck) {
      await UserManager.ensureLoggedInAsync();
    }

    if (!projectDir) {
      projectDir = process.cwd();
    } else {
      projectDir = path.resolve(process.cwd(), projectDir);
    }

    const logLines = (msg, logFn) => {
      for (let line of msg.split('\n')) {
        logFn(line);
      }
    };

    const logWithLevel = chunk => {
      if (!chunk.msg) {
        return;
      }
      if (chunk.level <= bunyan.INFO) {
        logLines(chunk.msg, log);
      } else if (chunk.level === bunyan.WARN) {
        logLines(chunk.msg, log.warn);
      } else {
        logLines(chunk.msg, log.error);
      }
    };

    let bar;
    let packagerLogsStream = new PackagerLogsStream({
      projectRoot: projectDir,
      onStartBuildBundle: () => {
        bar = new ProgressBar('Building JavaScript bundle [:bar] :percent', {
          total: 100,
          clear: true,
          complete: '=',
          incomplete: ' ',
        });

        log.setBundleProgressBar(bar);
      },
      onProgressBuildBundle: percent => {
        if (!bar || bar.complete) return;
        let ticks = percent - bar.curr;
        ticks > 0 && bar.tick(ticks);
      },
      onFinishBuildBundle: (err, startTime: Date, endTime: Date) => {
        if (bar && !bar.complete) {
          bar.tick(100 - bar.curr);
        }

        if (bar) {
          log.setBundleProgressBar(null);
          bar = null;

          if (err) {
            log(crayon.red('Failed building JavaScript bundle.'));
          } else {
            log(
              crayon.green(
                `Finished building JavaScript bundle in ${endTime -
                  startTime}ms.`
              )
            );
          }
        }
      },
      updateLogs: updater => {
        let newLogChunks = updater([]);
        newLogChunks.forEach(newLogChunk => {
          logWithLevel(newLogChunk);
        });
      },
    });

    // needed for validation logging to function
    ProjectUtils.attachLoggerStream(projectDir, {
      stream: {
        write: chunk => {
          if (chunk.tag === 'device') {
            logWithLevel(chunk);
          }
        },
      },
      type: 'raw',
    });

    // The existing CLI modules only pass one argument to this function, so skipProjectValidation
    // will be undefined in most cases. we can explicitly pass a truthy value here to avoid
    // validation (eg for init)
    //
    // If the packager/manifest server is running and healthy, there is no need
    // to rerun Doctor because the directory was already checked previously
    // This is relevant for command such as `exp send`
    if (
      !skipProjectValidation &&
      (await Project.currentStatus(projectDir)) !== 'running'
    ) {
      log('Making sure project is set up correctly...');
      simpleSpinner.start();
      // validate that this is a good projectDir before we try anything else

      let status = await Doctor.validateLowLatencyAsync(projectDir);
      if (status === Doctor.FATAL) {
        throw new Error(
          `There is an error with your project. See above logs for information.`
        );
      }
      simpleSpinner.stop();
      log('Your project looks good!');
    }

    // the existing CLI modules only pass one argument to this function, so skipProjectValidation
    // will be undefined in most cases. we can explicitly pass a truthy value here to avoid validation (eg for init)

    return asyncFn(projectDir, ...args);
  }, true);
};

function runAsync() {
  try {
    // Setup analytics
    Analytics.setSegmentNodeKey('vGu92cdmVaggGA26s3lBX6Y5fILm8SQ7');
    Analytics.setVersionName(require('../package.json').version);
    _registerLogs();

    if (process.env.SERVER_URL) {
      let serverUrl = process.env.SERVER_URL;
      if (!serverUrl.startsWith('http')) {
        serverUrl = `http://${serverUrl}`;
      }
      let parsedUrl = url.parse(serverUrl);
      Config.api.host = parsedUrl.hostname;
      Config.api.port = parsedUrl.port;
    }

    Config.developerTool = 'exp';

    // Setup our commander instance
    program.name = 'exp';
    program
      .version(require('../package.json').version)
      .option('-o, --output [format]', 'Output format. pretty (default), raw');

    // Load each module found in ./commands by 'registering' it with our commander instance
    glob
      .sync('commands/*.js', {
        cwd: __dirname,
      })
      .forEach(file => {
        const commandModule = require(`./${file}`);
        if (typeof commandModule === 'function') {
          commandModule(program);
        } else if (typeof commandModule.default === 'function') {
          commandModule.default(program);
        } else {
          log.error(`'${file}.js' is not a properly formatted command.`);
        }
      });

    if (process.env.EXPO_DEBUG) {
      glob
        .sync('debug_commands/*.js', {
          cwd: __dirname,
        })
        .forEach(file => {
          require(`./${file}`)(program);
        });
    }

    // Commander will now parse argv/argc
    program.parse(process.argv);

    // Display a message if the user does not input a valid command
    let subCommand = process.argv[2];
    if (subCommand) {
      let commands = [];
      program.commands.forEach(command => {
        commands.push(command['_name']);
        let alias = command['_alias'];
        if (alias) {
          commands.push(alias);
        }
      });
      if (!_.includes(commands, subCommand)) {
        console.log(
          `"${subCommand}" is not an exp command. See "exp --help" for the full list of commands.`
        );
      }
    } else {
      program.help();
    }
  } catch (e) {
    console.error(e);
    throw e;
  }
}

async function checkForUpdateAsync() {
  let { state, current, latest } = await update.checkForExpUpdateAsync();
  let message;
  switch (state) {
    case 'up-to-date':
      break;

    case 'out-of-date':
      message = `There is a new version of exp available (${latest}).
You are currently using exp ${current}
Run \`npm install -g exp\` to get the latest version`;
      crayon.green.error(message);
      break;

    case 'ahead-of-published':
      // if the user is ahead of npm, we're going to assume they know what they're doing
      break;

    default:
      log.error('Confused about what version of exp you have?');
  }
}

function _registerLogs() {
  let stream = {
    stream: {
      write: chunk => {
        if (chunk.code) {
          switch (chunk.code) {
            case NotificationCode.START_LOADING:
              simpleSpinner.start();
              return;
            case NotificationCode.STOP_LOADING:
              simpleSpinner.stop();
              return;
            case NotificationCode.DOWNLOAD_CLI_PROGRESS:
              return;
          }
        }

        if (chunk.level === bunyan.INFO) {
          log(chunk.msg);
        } else if (chunk.level === bunyan.WARN) {
          log.warn(chunk.msg);
        } else if (chunk.level >= bunyan.ERROR) {
          log.error(chunk.msg);
        }
      },
    },
    type: 'raw',
  };

  Logger.notifications.addStream(stream);
  Logger.global.addStream(stream);
}

async function writePathAsync() {
  let subCommand = process.argv[2];
  if (subCommand === 'prepare-detached-build') {
    // This is being run from Android Studio or Xcode. Don't want to write PATH in this case.
    return;
  }

  await Binaries.writePathToUserSettingsAsync();
}

// This is the entry point of the CLI
export function run() {
  (async function() {
    await Promise.all([writePathAsync(), runAsync()]);
  })().catch(e => {
    console.error('Uncaught Error', e);
    process.exit(1);
  });
}
<|endoftext|>


<commit_before>var async = require('async');
var stringify;

	var stringifyArray = function (array, callback, lvl) {
		process.nextTick(function() {

			if (array.length === 0) {
				callback(null, "[]");
			} else {
				try {
					async.mapSeries(array,
						function(element, callback){
							setImmediate(function() {
								stringify(element, function (err, value) {
									if (!err) {
										if (typeof value === 'undefined') {
											callback(null, "null");
										} else {
											callback(null, value);
										}
									} else {
										callback(err);
									}
								}, lvl+1);
							});
						},
						function(err, results){
							if (!err) {
								callback(null, "[" + results.join() + "]");
							} else {
								callback(err);
							}
						}
					);
				} catch (err) {
					callback(err);
				}
			}
		});
	};

	var stringifyObject = function (object, callback, lvl) {
		process.nextTick(function() {

			var keys = Object.keys(object);

			if (keys.length === 0) {
				callback(null, "{}");
			} else {
				try {
					async.mapSeries(keys,
						function(key, callback){
							setImmediate(function() {
								stringify(object[key], function (err, value) {
									if (!err) {
										if (typeof value === 'undefined') {
											callback(null, "null");
										} else {
											callback(null, JSON.stringify(key) + ":" + value);
										}
									} else {
										callback(err);
									}
								}, lvl+1);
							});
						},
						function(err, results){
							if (!err) {
								callback(null, "{" + results.join() + "}");
							} else {
								callback(err);
							}
						}
					);
				} catch (err) {
					callback(err);
				}
			}
		});
	};

	exports.stringify = stringify = function (data, callback, lvl) {
		lvl = lvl || 0;
		process.nextTick(function() {
			if (data === undefined) {
				setImmediate(function() {
					callback(null, undefined);
				});
			} else if (lvl > 1) {
				setImmediate(function() {
					callback(null, JSON.stringify(data));
				});
			} else {
				try {
					if (typeof data == "object") {
						if (data === null || typeof data.toJSON === "function" || data.constructor === String || data.constructor === Number || data.constructor === Boolean) {
							// 1) why is typeof null === "object"?
							// 2) used by Date and possibly some others.
							// 3) horrible, someone used the new String(), new Number(), or new Boolean() syntax.
							setImmediate(function() {
								callback(null, JSON.stringify(data));
							});
						} else if (Array.isArray(data)) {
							setImmediate(function() {
								stringifyArray(data, callback, lvl);
							});
						} else {
							setImmediate(function() {
								stringifyObject(data, callback, lvl);
							});
						}
					} else {
						setImmediate(function() {
							callback(null, JSON.stringify(data));
						});
					}
				} catch (err) {
					setImmediate(function() {
						callback(err);
					});
				}
			}
		});
	};
<commit_msg>Fixed long time saving<commit_after>var async = require('async');
var stringify;

	var stringifyArray = function (array, callback, lvl) {
		process.nextTick(function() {

			if (array.length === 0) {
				callback(null, "[]");
			} else if (array.length > 50){
				callback(null, JSON.stringify(array));
			} else {
				try {
					async.mapSeries(array,
						function(element, callback){
							setImmediate(function() {
								stringify(element, function (err, value) {
									if (!err) {
										if (typeof value === 'undefined') {
											callback(null, "null");
										} else {
											callback(null, value);
										}
									} else {
										callback(err);
									}
								}, lvl+1);
							});
						},
						function(err, results){
							if (!err) {
								callback(null, "[" + results.join() + "]");
							} else {
								callback(err);
							}
						}
					);
				} catch (err) {
					callback(err);
				}
			}
		});
	};

	var stringifyObject = function (object, callback, lvl) {
		process.nextTick(function() {

			var keys = Object.keys(object);

			if (keys.length === 0) {
				callback(null, "{}");
			} else if (keys.length > 50){
				callback(null, JSON.stringify(object));
			} else {
				try {
					async.mapSeries(keys,
						function(key, callback){
							setImmediate(function() {
								stringify(object[key], function (err, value) {
									if (!err) {
										if (typeof value === 'undefined') {
											callback(null, "null");
										} else {
											callback(null, JSON.stringify(key) + ":" + value);
										}
									} else {
										callback(err);
									}
								}, lvl+1);
							});
						},
						function(err, results){
							if (!err) {
								callback(null, "{" + results.join() + "}");
							} else {
								callback(err);
							}
						}
					);
				} catch (err) {
					callback(err);
				}
			}
		});
	};

	exports.stringify = stringify = function (data, callback, lvl) {
		lvl = lvl || 0;
		process.nextTick(function() {
			if (data === undefined) {
				setImmediate(function() {
					callback(null, undefined);
				});
			} else if (lvl > 1) {
				setImmediate(function() {
					callback(null, JSON.stringify(data));
				});
			} else {
				try {
					if (typeof data == "object") {
						if (data === null || typeof data.toJSON === "function" || data.constructor === String || data.constructor === Number || data.constructor === Boolean) {
							// 1) why is typeof null === "object"?
							// 2) used by Date and possibly some others.
							// 3) horrible, someone used the new String(), new Number(), or new Boolean() syntax.
							setImmediate(function() {
								callback(null, JSON.stringify(data));
							});
						} else if (Array.isArray(data)) {
							setImmediate(function() {
								stringifyArray(data, callback, lvl);
							});
						} else {
							setImmediate(function() {
								stringifyObject(data, callback, lvl);
							});
						}
					} else {
						setImmediate(function() {
							callback(null, JSON.stringify(data));
						});
					}
				} catch (err) {
					setImmediate(function() {
						callback(err);
					});
				}
			}
		});
	};
<|endoftext|>


<commit_before>import React from 'react';
import { render, mount } from 'enzyme';
import Table from '..';

describe('Table.filter', () => {
  const filterFn = (value, record) => record.name.indexOf(value) !== -1;
  const column = {
    title: 'Name',
    dataIndex: 'name',
    filters: [
      { text: 'Boy', value: 'boy' },
      { text: 'Girl', value: 'girl' },
      {
        text: 'Title',
        value: 'title',
        children: [
          { text: 'Designer', value: 'designer' },
          { text: 'Coder', value: 'coder' },
        ],
      },
    ],
    onFilter: filterFn,
  };

  const data = [
    { key: 0, name: 'Jack' },
    { key: 1, name: 'Lucy' },
    { key: 2, name: 'Tom' },
    { key: 3, name: 'Jerry' },
  ];

  function createTable(props) {
    return (

    );
  }

  function renderedNames(wrapper) {
    return wrapper.find('TableRow').map(row => row.props().record.name);
  }

  it('renders filter correctly', () => {
    const wrapper = render(createTable());

    expect(wrapper).toMatchSnapshot();
  });

  it('renders menu correctly', () => {
    const wrapper = mount(createTable());
    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('renders radio filter correctly', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterMultiple: false,
      }],
    }));
    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('renders custom content correctly', () => {
    const filter = (

        custom filter

    );
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdown: filter,
      }],
    }));

    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('override custom filter correctly', () => {
    const filter = ({ prefixCls, setSelectedKeys, confirm, clearFilters }) => {
      return (
        <div className={`${prefixCls}-view`} id="customFilter">
           setSelectedKeys([42])} id="setSelectedKeys">setSelectedKeys
           confirm()} id="confirm">Confirm
           clearFilters()} id="reset">Reset

      );
    };

    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdown: filter,
      }],
    }));

    const filterMenu = wrapper.find('FilterMenu').instance();

    // check if renderer well
    wrapper.find('Trigger').simulate('click');
    expect(wrapper.find('#customFilter')).toMatchSnapshot();

    // try to use reset btn
    expect(filterMenu.state.selectedKeys).toEqual([]);
    wrapper.find('#setSelectedKeys').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([42]);
    wrapper.find('#reset').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([]);

    // try to use confirm btn
    wrapper.find('Trigger').simulate('click');
    wrapper.find('#setSelectedKeys').simulate('click');
    expect(filterMenu.state.visible).toBe(true);
    wrapper.find('#confirm').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([42]);
    expect(filterMenu.state.visible).toBe(false);
  });

  it('can be controlled by filterDropdownVisible', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdownVisible: true,
      }],
    }));

    let dropdown = wrapper.find('Dropdown').first();
    expect(dropdown.props().visible).toBe(true);

    wrapper.setProps({
      columns: [{
        ...column,
        filterDropdownVisible: false,
      }],
    });

    dropdown = wrapper.find('Dropdown').first();
    expect(dropdown.props().visible).toBe(false);
  });

  it('if the filter is visible it should ignore the selectedKeys changes', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdownVisible: true,
      }],
    }));

    const filterMenu = wrapper.find('FilterMenu').instance();
    expect(filterMenu.state.selectedKeys).toEqual([]);
    wrapper.find('FilterMenu').find('input[type="checkbox"]').first().simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual(['boy']);
    wrapper.setProps({ dataSource: [...data, { key: 999, name: 'Chris' }] });
    expect(filterMenu.state.selectedKeys).toEqual(['boy']);
  });

  it('fires change event when visible change', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        onFilterDropdownVisibleChange: handleChange,
      }],
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');

    expect(handleChange).toBeCalledWith(true);
  });

  it('can be controlled by filteredValue', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filteredValue: ['Lucy'],
      }],
    }));

    expect(wrapper.find('tbody tr').length).toBe(1);
    wrapper.setProps({
      columns: [{
        ...column,
        filteredValue: [],
      }],
    });
    expect(wrapper.find('tbody tr').length).toBe(4);
  });

  it('can be controlled by filteredValue null', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filteredValue: ['Lucy'],
      }],
    }));

    expect(wrapper.find('tbody tr').length).toBe(1);
    wrapper.setProps({
      columns: [{
        ...column,
        filteredValue: null,
      }],
    });
    expect(wrapper.find('tbody tr').length).toBe(4);
  });

  it('fires change event', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({ onChange: handleChange }));
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());

    dropdownWrapper.find('MenuItem').first().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');

    expect(handleChange).toBeCalledWith({}, { name: ['boy'] }, {});
  });

  it('three levels menu', () => {
    const filters = [
      { text: 'Upper', value: 'Upper' },
      { text: 'Lower', value: 'Lower' },
      {
        text: 'Level2',
        value: 'Level2',
        children: [
          { text: 'Large', value: 'Large' },
          { text: 'Small', value: 'Small' },
          {
            text: 'Level3',
            value: 'Level3',
            children: [
              { text: 'Black', value: 'Black' },
              { text: 'White', value: 'White' },
              { text: 'Jack', value: 'Jack' },
            ],
          },
        ],
      },
    ];
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filters,
      }],
    }));
    jest.useFakeTimers();
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
    dropdownWrapper.find('.ant-dropdown-menu-submenu-title').at(0).simulate('mouseEnter');
    jest.runAllTimers();
    dropdownWrapper.update();
    dropdownWrapper.find('.ant-dropdown-menu-submenu-title').at(1).simulate('mouseEnter');
    jest.runAllTimers();
    dropdownWrapper.update();
    dropdownWrapper.find('MenuItem').last().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack']);
    jest.useRealTimers();
  });

  it('works with JSX in controlled mode', () => {
    const { Column } = Table;

    class App extends React.Component {
      state = {
        filters: {},
      }

      handleChange = (pagination, filters) => {
        this.setState({ filters });
      }

      render() {
        const { filters } = this.state;
        return (

            <Column
              title="name"
              dataIndex="name"
              key="name"
              filters={[
                { text: 'Jack', value: 'Jack' },
                { text: 'Lucy', value: 'Lucy' },
              ]}
              filteredValue={filters.name}
              onFilter={filterFn}
            />

        );
      }
    }

    const wrapper = mount(<App />);
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());

    dropdownWrapper.find('MenuItem').first().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack']);

    dropdownWrapper.find('.clear').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
  });

  it('works with grouping columns in controlled mode', () => {
    const columns = [
      {
        title: 'group',
        key: 'group',
        children: [
          {
            title: 'Name',
            dataIndex: 'name',
            key: 'name',
            filters: [
              { text: 'Jack', value: 'Jack' },
              { text: 'Lucy', value: 'Lucy' },
            ],
            onFilter: filterFn,
            filteredValue: ['Jack'],
          },
          {
            title: 'Age',
            dataIndex: 'age',
            key: 'age',
          },
        ],
      },
    ];
    const testData = [
      { key: 0, name: 'Jack', age: 11 },
      { key: 1, name: 'Lucy', age: 20 },
      { key: 2, name: 'Tom', age: 21 },
      { key: 3, name: 'Jerry', age: 22 },
    ];
    const wrapper = mount(

    );

    expect(renderedNames(wrapper)).toEqual(['Jack']);
  });

  it('confirm filter when dropdown hidden', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filters: [
          { text: 'Jack', value: 'Jack' },
          { text: 'Lucy', value: 'Lucy' },
        ],
      }],
      onChange: handleChange,
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');

    expect(handleChange).toBeCalled();
  });

  it('renders custom filter icon correctly', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterIcon: filtered => {filtered ? 'filtered' : 'unfiltered'},
      }],
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot();

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot();
  });
});
<commit_msg>fix test case in react 15<commit_after>import React from 'react';
import { render, mount } from 'enzyme';
import Table from '..';

describe('Table.filter', () => {
  const filterFn = (value, record) => record.name.indexOf(value) !== -1;
  const column = {
    title: 'Name',
    dataIndex: 'name',
    filters: [
      { text: 'Boy', value: 'boy' },
      { text: 'Girl', value: 'girl' },
      {
        text: 'Title',
        value: 'title',
        children: [
          { text: 'Designer', value: 'designer' },
          { text: 'Coder', value: 'coder' },
        ],
      },
    ],
    onFilter: filterFn,
  };

  const data = [
    { key: 0, name: 'Jack' },
    { key: 1, name: 'Lucy' },
    { key: 2, name: 'Tom' },
    { key: 3, name: 'Jerry' },
  ];

  function createTable(props) {
    return (

    );
  }

  function renderedNames(wrapper) {
    return wrapper.find('TableRow').map(row => row.props().record.name);
  }

  it('renders filter correctly', () => {
    const wrapper = render(createTable());

    expect(wrapper).toMatchSnapshot();
  });

  it('renders menu correctly', () => {
    const wrapper = mount(createTable());
    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('renders radio filter correctly', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterMultiple: false,
      }],
    }));
    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('renders custom content correctly', () => {
    const filter = (

        custom filter

    );
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdown: filter,
      }],
    }));

    const dropdownWrapper = render(wrapper.find('Trigger').instance().getComponent());
    expect(dropdownWrapper).toMatchSnapshot();
  });

  it('override custom filter correctly', () => {
    const filter = ({ prefixCls, setSelectedKeys, confirm, clearFilters }) => {
      return (
        <div className={`${prefixCls}-view`} id="customFilter">
           setSelectedKeys([42])} id="setSelectedKeys">setSelectedKeys
           confirm()} id="confirm">Confirm
           clearFilters()} id="reset">Reset

      );
    };

    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdown: filter,
      }],
    }));

    const filterMenu = wrapper.find('FilterMenu').instance();

    // check if renderer well
    wrapper.find('i.ant-dropdown-trigger').simulate('click');
    expect(wrapper.find('#customFilter')).toMatchSnapshot();

    // try to use reset btn
    expect(filterMenu.state.selectedKeys).toEqual([]);
    wrapper.find('#setSelectedKeys').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([42]);
    wrapper.find('#reset').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([]);

    // try to use confirm btn
    wrapper.find('i.ant-dropdown-trigger').simulate('click');
    wrapper.find('#setSelectedKeys').simulate('click');
    expect(filterMenu.state.visible).toBe(true);
    wrapper.find('#confirm').simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual([42]);
    expect(filterMenu.state.visible).toBe(false);
  });

  it('can be controlled by filterDropdownVisible', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdownVisible: true,
      }],
    }));

    let dropdown = wrapper.find('Dropdown').first();
    expect(dropdown.props().visible).toBe(true);

    wrapper.setProps({
      columns: [{
        ...column,
        filterDropdownVisible: false,
      }],
    });

    dropdown = wrapper.find('Dropdown').first();
    expect(dropdown.props().visible).toBe(false);
  });

  it('if the filter is visible it should ignore the selectedKeys changes', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterDropdownVisible: true,
      }],
    }));

    const filterMenu = wrapper.find('FilterMenu').instance();
    expect(filterMenu.state.selectedKeys).toEqual([]);
    wrapper.find('FilterMenu').find('input[type="checkbox"]').first().simulate('click');
    expect(filterMenu.state.selectedKeys).toEqual(['boy']);
    wrapper.setProps({ dataSource: [...data, { key: 999, name: 'Chris' }] });
    expect(filterMenu.state.selectedKeys).toEqual(['boy']);
  });

  it('fires change event when visible change', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        onFilterDropdownVisibleChange: handleChange,
      }],
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');

    expect(handleChange).toBeCalledWith(true);
  });

  it('can be controlled by filteredValue', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filteredValue: ['Lucy'],
      }],
    }));

    expect(wrapper.find('tbody tr').length).toBe(1);
    wrapper.setProps({
      columns: [{
        ...column,
        filteredValue: [],
      }],
    });
    expect(wrapper.find('tbody tr').length).toBe(4);
  });

  it('can be controlled by filteredValue null', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filteredValue: ['Lucy'],
      }],
    }));

    expect(wrapper.find('tbody tr').length).toBe(1);
    wrapper.setProps({
      columns: [{
        ...column,
        filteredValue: null,
      }],
    });
    expect(wrapper.find('tbody tr').length).toBe(4);
  });

  it('fires change event', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({ onChange: handleChange }));
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());

    dropdownWrapper.find('MenuItem').first().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');

    expect(handleChange).toBeCalledWith({}, { name: ['boy'] }, {});
  });

  it('three levels menu', () => {
    const filters = [
      { text: 'Upper', value: 'Upper' },
      { text: 'Lower', value: 'Lower' },
      {
        text: 'Level2',
        value: 'Level2',
        children: [
          { text: 'Large', value: 'Large' },
          { text: 'Small', value: 'Small' },
          {
            text: 'Level3',
            value: 'Level3',
            children: [
              { text: 'Black', value: 'Black' },
              { text: 'White', value: 'White' },
              { text: 'Jack', value: 'Jack' },
            ],
          },
        ],
      },
    ];
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filters,
      }],
    }));
    jest.useFakeTimers();
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
    dropdownWrapper.find('.ant-dropdown-menu-submenu-title').at(0).simulate('mouseEnter');
    jest.runAllTimers();
    dropdownWrapper.update();
    dropdownWrapper.find('.ant-dropdown-menu-submenu-title').at(1).simulate('mouseEnter');
    jest.runAllTimers();
    dropdownWrapper.update();
    dropdownWrapper.find('MenuItem').last().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack']);
    jest.useRealTimers();
  });

  it('works with JSX in controlled mode', () => {
    const { Column } = Table;

    class App extends React.Component {
      state = {
        filters: {},
      }

      handleChange = (pagination, filters) => {
        this.setState({ filters });
      }

      render() {
        const { filters } = this.state;
        return (

            <Column
              title="name"
              dataIndex="name"
              key="name"
              filters={[
                { text: 'Jack', value: 'Jack' },
                { text: 'Lucy', value: 'Lucy' },
              ]}
              filteredValue={filters.name}
              onFilter={filterFn}
            />

        );
      }
    }

    const wrapper = mount(<App />);
    const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());

    dropdownWrapper.find('MenuItem').first().simulate('click');
    dropdownWrapper.find('.confirm').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack']);

    dropdownWrapper.find('.clear').simulate('click');
    wrapper.update();
    expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
  });

  it('works with grouping columns in controlled mode', () => {
    const columns = [
      {
        title: 'group',
        key: 'group',
        children: [
          {
            title: 'Name',
            dataIndex: 'name',
            key: 'name',
            filters: [
              { text: 'Jack', value: 'Jack' },
              { text: 'Lucy', value: 'Lucy' },
            ],
            onFilter: filterFn,
            filteredValue: ['Jack'],
          },
          {
            title: 'Age',
            dataIndex: 'age',
            key: 'age',
          },
        ],
      },
    ];
    const testData = [
      { key: 0, name: 'Jack', age: 11 },
      { key: 1, name: 'Lucy', age: 20 },
      { key: 2, name: 'Tom', age: 21 },
      { key: 3, name: 'Jerry', age: 22 },
    ];
    const wrapper = mount(

    );

    expect(renderedNames(wrapper)).toEqual(['Jack']);
  });

  it('confirm filter when dropdown hidden', () => {
    const handleChange = jest.fn();
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filters: [
          { text: 'Jack', value: 'Jack' },
          { text: 'Lucy', value: 'Lucy' },
        ],
      }],
      onChange: handleChange,
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');

    expect(handleChange).toBeCalled();
  });

  it('renders custom filter icon correctly', () => {
    const wrapper = mount(createTable({
      columns: [{
        ...column,
        filterIcon: filtered => {filtered ? 'filtered' : 'unfiltered'},
      }],
    }));

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot();

    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    wrapper.find('.ant-dropdown-menu-item').first().simulate('click');
    wrapper.find('.ant-dropdown-trigger').first().simulate('click');
    expect(wrapper.find('.ant-table-filter-icon').render()).toMatchSnapshot();
  });
});
<|endoftext|>


<commit_before>var CalendarManager = CalendarManager || {};

CalendarManager.isAuthorized = false;

CalendarManager.authorizationScopes = ['https://www.googleapis.com/auth/calendar'];

CalendarManager.handleAuthenticationResut = function(authResult, callback) {
  if(authResult && !authResult.error) {
	  this.isAuthorized = true;
  }
  if(callback) {
	  callback(this.isAuthroized, authResult);
  }
};

CalendarManager.obtainAuthorization = function(googleApi, clientId, callback){
  var _this = this;
  this.gapi = googleApi;
  googleApi.auth.authorize({
    client_id: clientId,
    scope: this.authorizationScopes,
    immediate: true
  }, function(result) {
	  _this.handleAuthenticationResut(result, callback);
  });
};

CalendarManager.sync = function(method, model, options){
  switch(method) {
    case 'read':
      if(model.model) {
    	  //collection read
        this.gapi.client.load('calendar', 'v3', function() {
    	    var request = gapi.client.calendar.events.list({
    	      calendarId: model.calendarId,
            timeMin: options.timeMin,
    	      timeMax: options.timeMax
          });

    	    request.then(function(resp) {
    	      options.success(resp);
    	    }, function(reason) {
    	      options.failure(reason);
    	    });
        });
      } else {
    	  //model read
      }
	    break;
    case 'create':
      break;
    case 'update':
    case 'patch':
	    break;
    case 'delete':
	    break;
  }
};

CalendarManager.makeEventModel = function(calendarId) {
  return Backbone.Model.extend({
	  calendarId: calendarId,
	  sync: this.sync
  });
};

CalendarManager.makeCalendar = function(calendarId) {
  var BackboneGoogleCalendar = Backbone.Collection.extend({
	  calendarId: calendarId,
	  model: this.makeEventModel(calendarId),
	  sync: this.sync
  });
  return new BackboneGoogleCalendar();
};<commit_msg>fixed misspelling<commit_after>var CalendarManager = CalendarManager || {};

CalendarManager.isAuthorized = false;

CalendarManager.authorizationScopes = ['https://www.googleapis.com/auth/calendar'];

CalendarManager.handleAuthenticationResut = function(authResult, callback) {
  if(authResult && !authResult.error) {
	  this.isAuthorized = true;
  } else {
    this.isAuthorized = false;
  }
  if(callback) {
	  callback(this.isAuthorized, authResult);
  }
};

CalendarManager.obtainAuthorization = function(googleApi, clientId, callback){
  var _this = this;
  this.gapi = googleApi;
  googleApi.auth.authorize({
    client_id: clientId,
    scope: this.authorizationScopes,
    immediate: true
  }, function(result) {
	  _this.handleAuthenticationResut(result, callback);
  });
};

CalendarManager.sync = function(method, model, options){
  switch(method) {
    case 'read':
      if(model.model) {
    	  //collection read
        this.gapi.client.load('calendar', 'v3', function() {
    	    var request = gapi.client.calendar.events.list({
    	      calendarId: model.calendarId,
            timeMin: options.timeMin,
    	      timeMax: options.timeMax
          });

    	    request.then(function(resp) {
    	      options.success(resp);
    	    }, function(reason) {
    	      options.failure(reason);
    	    });
        });
      } else {
    	  //model read
      }
	    break;
    case 'create':
      break;
    case 'update':
    case 'patch':
	    break;
    case 'delete':
	    break;
  }
};

CalendarManager.makeEventModel = function(calendarId) {
  return Backbone.Model.extend({
	  calendarId: calendarId,
	  sync: this.sync
  });
};

CalendarManager.makeCalendar = function(calendarId) {
  var BackboneGoogleCalendar = Backbone.Collection.extend({
	  calendarId: calendarId,
	  model: this.makeEventModel(calendarId),
	  sync: this.sync
  });
  return new BackboneGoogleCalendar();
};<|endoftext|>


<commit_before>import React, {Component, PropTypes} from 'react'

import _ from 'lodash'
import uuid from 'node-uuid'

import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
import QueryBuilder from 'src/data_explorer/components/QueryBuilder'
import Visualization from 'src/data_explorer/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import * as queryModifiers from 'src/utils/queryTransitions'

import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import buildInfluxQLQuery from 'utils/influxql'
import {getQueryConfig} from 'shared/apis'

class CellEditorOverlay extends Component {
  constructor(props) {
    super(props)

    this.queryStateReducer = ::this.queryStateReducer

    this.handleAddQuery = ::this.handleAddQuery
    this.handleDeleteQuery = ::this.handleDeleteQuery

    this.handleSaveCell = ::this.handleSaveCell

    this.handleSelectGraphType = ::this.handleSelectGraphType
    this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
    this.handleEditRawText = ::this.handleEditRawText

    const {cell: {name, type, queries}} = props

    const queriesWorkingDraft = _.cloneDeep(queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})))

    this.state = {
      cellWorkingName: name,
      cellWorkingType: type,
      queriesWorkingDraft,
      activeQueryIndex: 0,
    }
  }

  componentWillReceiveProps(nextProps) {
    const {status, queryID} = this.props.queryStatus
    const nextStatus = nextProps.queryStatus
    if (nextStatus.status && nextStatus.queryID) {
      if (nextStatus.queryID !== queryID || nextStatus.status !== status) {
        const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === queryID ? ({...q, status: nextStatus.status}) : q)
        this.setState({queriesWorkingDraft: nextQueries})
      }
    }
  }

  queryStateReducer(queryModifier) {
    return (queryID, payload) => {
      const {queriesWorkingDraft} = this.state
      const query = queriesWorkingDraft.find((q) => q.id === queryID)

      const nextQuery = queryModifier(query, payload)

      const nextQueries = queriesWorkingDraft.map((q) => q.id === query.id ? nextQuery : q)
      this.setState({queriesWorkingDraft: nextQueries})
    }
  }

  handleAddQuery(options) {
    const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
    const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
    this.setState({queriesWorkingDraft: nextQueries})
  }

  handleDeleteQuery(index) {
    const nextQueries = this.state.queriesWorkingDraft.filter((__, i) => i !== index)
    this.setState({queriesWorkingDraft: nextQueries})
  }

  handleSaveCell() {
    const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state
    const {cell, timeRange} = this.props

    const newCell = _.cloneDeep(cell)
    newCell.name = cellWorkingName
    newCell.type = cellWorkingType
    newCell.queries = queriesWorkingDraft.map((q) => {
      const query = q.rawText || buildInfluxQLQuery(timeRange, q)
      const label = q.rawText ?
        "" :
        `${q.measurement}.${q.fields[0].field}`

      return {
        queryConfig: q,
        query,
        label,
      }
    })

    this.props.onSave(newCell)
  }

  handleSelectGraphType(graphType) {
    this.setState({cellWorkingType: graphType})
  }

  handleSetActiveQueryIndex(activeQueryIndex) {
    this.setState({activeQueryIndex})
  }

  async handleEditRawText(url, id, text) {
    // use this as the handler passed into fetchTimeSeries to update a query status
    try {
      const {data} = await getQueryConfig(url, [{query: text, id}])
      const config = data.queries.find(q => q.id === id)
      const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === id ? config.queryConfig : q)
      this.setState({queriesWorkingDraft: nextQueries})
    } catch (error) {
      console.error(error)
    }
  }

  render() {
    const {
      source,
      onCancel,
      timeRange,
      autoRefresh,
      editQueryStatus,
    } = this.props

    const {
      activeQueryIndex,
      cellWorkingName,
      cellWorkingType,
      queriesWorkingDraft,
    } = this.state

    const queryActions = {
      addQuery: this.handleAddQuery,
      editRawTextAsync: this.handleEditRawText,
      ..._.mapValues(queryModifiers, (qm) => this.queryStateReducer(qm)),
    }

    return (

        <ResizeContainer>
          <Visualization
            autoRefresh={autoRefresh}
            timeRange={timeRange}
            queryConfigs={queriesWorkingDraft}
            activeQueryIndex={0}
            cellType={cellWorkingType}
            cellName={cellWorkingName}
            editQueryStatus={editQueryStatus}
          />
          <ResizeBottom>

              <OverlayControls
                selectedGraphType={cellWorkingType}
                onSelectGraphType={this.handleSelectGraphType}
                onCancel={onCancel}
                onSave={this.handleSaveCell}
              />
              <QueryBuilder
                source={source}
                queries={queriesWorkingDraft}
                actions={queryActions}
                autoRefresh={autoRefresh}
                timeRange={timeRange}
                setActiveQueryIndex={this.handleSetActiveQueryIndex}
                onDeleteQuery={this.handleDeleteQuery}
                activeQueryIndex={activeQueryIndex}
              />

          </ResizeBottom>
        </ResizeContainer>

    )
  }
}

const {
  func,
  number,
  shape,
  string,
} = PropTypes

CellEditorOverlay.propTypes = {
  onCancel: func.isRequired,
  onSave: func.isRequired,
  cell: shape({}).isRequired,
  timeRange: shape({
    upper: string,
    lower: string,
  }).isRequired,
  autoRefresh: number.isRequired,
  source: shape({
    links: shape({
      queries: string.isRequired,
    }),
  }),
  editQueryStatus: func.isRequired,
  queryStatus: shape({
    queryID: string,
    status: shape({}),
  }).isRequired,
}

export default CellEditorOverlay
<commit_msg>Add required proxy link prop to CEO<commit_after>import React, {Component, PropTypes} from 'react'

import _ from 'lodash'
import uuid from 'node-uuid'

import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
import QueryBuilder from 'src/data_explorer/components/QueryBuilder'
import Visualization from 'src/data_explorer/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import * as queryModifiers from 'src/utils/queryTransitions'

import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import buildInfluxQLQuery from 'utils/influxql'
import {getQueryConfig} from 'shared/apis'

class CellEditorOverlay extends Component {
  constructor(props) {
    super(props)

    this.queryStateReducer = ::this.queryStateReducer

    this.handleAddQuery = ::this.handleAddQuery
    this.handleDeleteQuery = ::this.handleDeleteQuery

    this.handleSaveCell = ::this.handleSaveCell

    this.handleSelectGraphType = ::this.handleSelectGraphType
    this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
    this.handleEditRawText = ::this.handleEditRawText

    const {cell: {name, type, queries}} = props

    const queriesWorkingDraft = _.cloneDeep(queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})))

    this.state = {
      cellWorkingName: name,
      cellWorkingType: type,
      queriesWorkingDraft,
      activeQueryIndex: 0,
    }
  }

  componentWillReceiveProps(nextProps) {
    const {status, queryID} = this.props.queryStatus
    const nextStatus = nextProps.queryStatus
    if (nextStatus.status && nextStatus.queryID) {
      if (nextStatus.queryID !== queryID || nextStatus.status !== status) {
        const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === queryID ? ({...q, status: nextStatus.status}) : q)
        this.setState({queriesWorkingDraft: nextQueries})
      }
    }
  }

  queryStateReducer(queryModifier) {
    return (queryID, payload) => {
      const {queriesWorkingDraft} = this.state
      const query = queriesWorkingDraft.find((q) => q.id === queryID)

      const nextQuery = queryModifier(query, payload)

      const nextQueries = queriesWorkingDraft.map((q) => q.id === query.id ? nextQuery : q)
      this.setState({queriesWorkingDraft: nextQueries})
    }
  }

  handleAddQuery(options) {
    const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
    const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
    this.setState({queriesWorkingDraft: nextQueries})
  }

  handleDeleteQuery(index) {
    const nextQueries = this.state.queriesWorkingDraft.filter((__, i) => i !== index)
    this.setState({queriesWorkingDraft: nextQueries})
  }

  handleSaveCell() {
    const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state
    const {cell, timeRange} = this.props

    const newCell = _.cloneDeep(cell)
    newCell.name = cellWorkingName
    newCell.type = cellWorkingType
    newCell.queries = queriesWorkingDraft.map((q) => {
      const query = q.rawText || buildInfluxQLQuery(timeRange, q)
      const label = q.rawText ?
        "" :
        `${q.measurement}.${q.fields[0].field}`

      return {
        queryConfig: q,
        query,
        label,
      }
    })

    this.props.onSave(newCell)
  }

  handleSelectGraphType(graphType) {
    this.setState({cellWorkingType: graphType})
  }

  handleSetActiveQueryIndex(activeQueryIndex) {
    this.setState({activeQueryIndex})
  }

  async handleEditRawText(url, id, text) {
    // use this as the handler passed into fetchTimeSeries to update a query status
    try {
      const {data} = await getQueryConfig(url, [{query: text, id}])
      const config = data.queries.find(q => q.id === id)
      const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === id ? config.queryConfig : q)
      this.setState({queriesWorkingDraft: nextQueries})
    } catch (error) {
      console.error(error)
    }
  }

  render() {
    const {
      source,
      onCancel,
      timeRange,
      autoRefresh,
      editQueryStatus,
    } = this.props

    const {
      activeQueryIndex,
      cellWorkingName,
      cellWorkingType,
      queriesWorkingDraft,
    } = this.state

    const queryActions = {
      addQuery: this.handleAddQuery,
      editRawTextAsync: this.handleEditRawText,
      ..._.mapValues(queryModifiers, (qm) => this.queryStateReducer(qm)),
    }

    return (

        <ResizeContainer>
          <Visualization
            autoRefresh={autoRefresh}
            timeRange={timeRange}
            queryConfigs={queriesWorkingDraft}
            activeQueryIndex={0}
            cellType={cellWorkingType}
            cellName={cellWorkingName}
            editQueryStatus={editQueryStatus}
          />
          <ResizeBottom>

              <OverlayControls
                selectedGraphType={cellWorkingType}
                onSelectGraphType={this.handleSelectGraphType}
                onCancel={onCancel}
                onSave={this.handleSaveCell}
              />
              <QueryBuilder
                source={source}
                queries={queriesWorkingDraft}
                actions={queryActions}
                autoRefresh={autoRefresh}
                timeRange={timeRange}
                setActiveQueryIndex={this.handleSetActiveQueryIndex}
                onDeleteQuery={this.handleDeleteQuery}
                activeQueryIndex={activeQueryIndex}
              />

          </ResizeBottom>
        </ResizeContainer>

    )
  }
}

const {
  func,
  number,
  shape,
  string,
} = PropTypes

CellEditorOverlay.propTypes = {
  onCancel: func.isRequired,
  onSave: func.isRequired,
  cell: shape({}).isRequired,
  timeRange: shape({
    upper: string,
    lower: string,
  }).isRequired,
  autoRefresh: number.isRequired,
  source: shape({
    links: shape({
      proxy: string.isRequired,
      queries: string.isRequired,
    }).isRequired,
  }).isRequired,
  editQueryStatus: func.isRequired,
  queryStatus: shape({
    queryID: string,
    status: shape({}),
  }).isRequired,
}

export default CellEditorOverlay
<|endoftext|>


<commit_before><commit_msg>Fix https://github.com/BabylonJS/Babylon.js/issues/3391<commit_after><|endoftext|>