<commit_before>/* ========================================================================
 * Bootstrap (plugin): validator.js v0.8.1
 * ========================================================================
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Cina Saffary.
 * Made by @1000hz in the style of Bootstrap 3 era @fat
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * ======================================================================== */

+function ($) {
  'use strict';

  var inputSelector = ':input:not([type="submit"], button):enabled:visible,.input-custom'
  var nativeInputSelector =  ':input:not([type="submit"], button):enabled:visible'
  // VALIDATOR CLASS DEFINITION
  // ==========================

  var Validator = function (element, options) {
    this.$element = $(element)
    this.options  = options

    options.errors = $.extend({}, Validator.DEFAULTS.errors, options.errors)

    for (var custom in options.custom) {
      if (!options.errors[custom]) throw new Error('Missing default error message for custom validator: ' + custom)
    }

    $.extend(Validator.VALIDATORS, options.custom)

    this.$element.attr('novalidate', true) // disable automatic native validation
    this.toggleSubmit()

    this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.validateInput, this))
    this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))

    this.$element.find('[data-match]').each(function () {
      var $this  = $(this)
      var target = $this.data('match')

      $(target).on('input.bs.validator', function (e) {
        $this.val() && $this.trigger('input.bs.validator')
      })
    })
  }

  Validator.DEFAULTS = {
    delay: 500,
    html: false,
    disable: true,
    custom: {},
    errors: {
      match: 'Does not match',
      minlength: 'Not long enough'
    }
  }

  Validator.VALIDATORS = {
    native: function ($el) {
      var el = $el[0]
      return el.checkValidity ? el.checkValidity() : true
    },
    match: function ($el) {
      var target = $el.data('match')
      return !$el.val() || $el.val() === $(target).val()
    },
    minlength: function ($el) {
      var minlength = $el.data('minlength')
      return !$el.val() || $el.val().length >= minlength
    },
    required: function($el) {
      if ($el.is('[required]')) {
        return !!($el.val() && $el.val() !== '');
      }
      return true;
    }
  }

  Validator.prototype.validateInput = function (e) {
    var self       = this;
    var $el        = $(e.target)
    var prevErrors = $el.data('bs.validator.errors')
    var errors

    /**
     * [if custom input element]
     * @param  {[type]} #el.is('.input-custom') [description]
     * @return {[type]}                         [description]
     */

    if (!$el.is(inputSelector) && !$el.is('input')) {
      $el = $el.parents('.input-custom');
    }

    if ($el.length < 1) {
      return
    }

    if ($el.is('.input-custom')) {
      $el.val = function() {
        return $el.attr('data-value');
      }
      doValidate();
      return
    }

    if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]')

    doValidate();

    function doValidate() {
      self.$element.trigger(e = $.Event('validate.bs.validator', {relatedTarget: $el[0]}))
      if (e.isDefaultPrevented()) return

      self.runValidators($el).done(function (errors) {
        $el.data('bs.validator.errors', errors)

        errors.length ? self.showErrors($el) : self.clearErrors($el)

        if (!prevErrors || errors.toString() !== prevErrors.toString()) {
          e = errors.length
            ? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})
            : $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})

          self.$element.trigger(e)
        }

        self.toggleSubmit()

        self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))
      })
    }
  }

  Validator.prototype.runValidators = function ($el) {
    var errors   = []
    var deferred = $.Deferred()
    var options  = this.options

    $el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()
    $el.data('bs.validator.deferred', deferred)

    function getErrorMessage(key) {
      return $el.data(key + '-error')
        || $el.data('error')
        || key == 'native' && $el[0].validationMessage
        || options.errors[key]
    }

    $.each(Validator.VALIDATORS, $.proxy(function (key, validator) {
      if (($el.data(key) || key == 'required' || key == 'native') && !validator.call(this, $el)) {
        var error = getErrorMessage(key)
        !~errors.indexOf(error) && errors.push(error)
      }
    }, this))

    if (!errors.length && $el.val() && $el.data('remote')) {
      this.defer($el, function () {
        var data = {}
        data[$el.attr('name')] = $el.val()
        $.get($el.data('remote'), data)
          .fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })
          .always(function () { deferred.resolve(errors)})
      })
    } else deferred.resolve(errors)

    return deferred.promise()
  }

  Validator.prototype.validate = function () {
    var delay = this.options.delay

    this.options.delay = 0
    this.$element.find(inputSelector).trigger('input.bs.validator')
    this.options.delay = delay

    return this
  }

  Validator.prototype.showErrors = function ($el) {
    var method = this.options.html ? 'html' : 'text'

    this.defer($el, function () {
      var $group = $el.closest('.form-group')
      var $block = $group.find('.help-block.with-errors')
      var $feedback = $group.find('.form-control-feedback')
      var errors = $el.data('bs.validator.errors')

      if (!errors.length) return

      errors = $('<ul/>')
        .addClass('list-unstyled')
        .append($.map(errors, function (error) { return $('<li/>')[method](error) }))

      $block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())
      $block.empty().append(errors)
      $group.addClass('has-error')

      $feedback.length
        && $feedback.removeClass('glyphicon-ok')
        && $feedback.addClass('glyphicon-warning-sign')
        && $group.removeClass('has-success')
    })
  }

  Validator.prototype.clearErrors = function ($el) {
    var $group = $el.closest('.form-group')
    var $block = $group.find('.help-block.with-errors')
    var $feedback = $group.find('.form-control-feedback')

    $block.html($block.data('bs.validator.originalContent'))
    $group.removeClass('has-error')

    $feedback.length
      && $feedback.removeClass('glyphicon-warning-sign')
      && $feedback.addClass('glyphicon-ok')
      && $group.addClass('has-success')
  }

  Validator.prototype.hasErrors = function () {
    function fieldErrors() {
      return !!($(this).data('bs.validator.errors') || []).length
    }

    return !!this.$element.find(inputSelector).filter(fieldErrors).length
  }

  Validator.prototype.isIncomplete = function () {
    function fieldIncomplete() {
      return this.type === 'checkbox' ? !this.checked                                   :
             this.type === 'radio'    ? !$('[name="' + this.name + '"]:checked').length :
                                        $.trim(this.value) === ''
    }

    return !!this.$element.find(nativeInputSelector).filter('[required]').filter(fieldIncomplete).length
  }

  Validator.prototype.onSubmit = function (e) {
    this.validate()
    if (this.isIncomplete() || this.hasErrors()) e.preventDefault()
  }

  Validator.prototype.toggleSubmit = function () {
    if(!this.options.disable) return
    var $btn = $('button[type="submit"], input[type="submit"]')
      .filter('[form="' + this.$element.attr('id') + '"]')
      .add(this.$element.find('input[type="submit"], button[type="submit"]'))
    $btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
      .css({'pointer-events': 'all', 'cursor': 'pointer'})
  }

  Validator.prototype.defer = function ($el, callback) {
    if (!this.options.delay) return callback()
    window.clearTimeout($el.data('bs.validator.timeout'))
    $el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))
  }

  Validator.prototype.destroy = function () {
    this.$element
      .removeAttr('novalidate')
      .removeData('bs.validator')
      .off('.bs.validator')

    this.$element.find(inputSelector)
      .off('.bs.validator')
      .removeData(['bs.validator.errors', 'bs.validator.deferred'])
      .each(function () {
        var $this = $(this)
        var timeout = $this.data('bs.validator.timeout')
        window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')
      })

    this.$element.find('.help-block.with-errors').each(function () {
      var $this = $(this)
      var originalContent = $this.data('bs.validator.originalContent')

      $this
        .removeData('bs.validator.originalContent')
        .html(originalContent)
    })

    this.$element.find('input[type="submit"], button[type="submit"]').removeClass('disabled')

    this.$element.find('.has-error').removeClass('has-error')

    return this
  }

  // VALIDATOR PLUGIN DEFINITION
  // ===========================

  function Plugin(option) {
    return this.each(function () {
      var $this   = $(this)
      var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)
      var data    = $this.data('bs.validator')

      if (!data && option == 'destroy') return
      if (!data) $this.data('bs.validator', (data = new Validator(this, options)))
      if (typeof option == 'string') data[option]()
    })
  }

  var old = $.fn.validator

  $.fn.validator             = Plugin
  $.fn.validator.Constructor = Validator

  // VALIDATOR NO CONFLICT
  // =====================

  $.fn.validator.noConflict = function () {
    $.fn.validator = old
    return this
  }

  // VALIDATOR DATA-API
  // ==================

  $(window).on('load', function () {
    $('form[data-toggle="validator"]').each(function () {
      var $form = $(this)
      Plugin.call($form, $form.data())
    })
  })

}(jQuery);
<commit_msg>native 校验做 pollyfill， 对 number 同时支持 min max 和 pattern<commit_after>/* ========================================================================
 * Bootstrap (plugin): validator.js v0.8.1
 * ========================================================================
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Cina Saffary.
 * Made by @1000hz in the style of Bootstrap 3 era @fat
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * ======================================================================== */

+function ($) {
  'use strict';

  var inputSelector = ':input:not([type="submit"], button):enabled:visible,.input-custom'
  var nativeInputSelector =  ':input:not([type="submit"], button):enabled:visible'
  // VALIDATOR CLASS DEFINITION
  // ==========================

  var Validator = function (element, options) {
    this.$element = $(element)
    this.options  = options

    options.errors = $.extend({}, Validator.DEFAULTS.errors, options.errors)

    for (var custom in options.custom) {
      if (!options.errors[custom]) throw new Error('Missing default error message for custom validator: ' + custom)
    }

    $.extend(Validator.VALIDATORS, options.custom)

    this.$element.attr('novalidate', true) // disable automatic native validation
    this.toggleSubmit()

    this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.validateInput, this))
    this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))

    this.$element.find('[data-match]').each(function () {
      var $this  = $(this)
      var target = $this.data('match')

      $(target).on('input.bs.validator', function (e) {
        $this.val() && $this.trigger('input.bs.validator')
      })
    })
  }

  Validator.DEFAULTS = {
    delay: 500,
    html: false,
    disable: true,
    custom: {},
    errors: {
      match: 'Does not match',
      minlength: 'Not long enough'
    }
  }

  Validator.VALIDATORS = {
    native: function ($el) {
      var el = $el[0]
      var value = el.value;
      var isNumber = $el.attr('type') === 'number';

      // if (el.checkValidity) {
      //   return el.checkValidity()
      // }

      // pattern
      var pattern = $el.attr('pattern');
      if (pattern) {
        var regex = new RegExp('^' + pattern + '$', 'g');
        var matched = value && (value + '').match(regex);
        if (!isNumber || !matched) {
          return matched;
        }
      }

      //  native pollyfill
      if (isNumber) {
        var min = ($el.attr('min') - 0) || -1000000000000;
        var max = ($el.attr('max') - 0) || 100000000000;
        value = value - 0;
        if (value < min || value > max) {
          return false;
        } else {
          return true;
        }
      }

      return true
    },
    match: function ($el) {
      var target = $el.data('match')
      return !$el.val() || $el.val() === $(target).val()
    },
    minlength: function ($el) {
      var minlength = $el.data('minlength')
      return !$el.val() || $el.val().length >= minlength
    },
    required: function($el) {
      if ($el.is('[required]')) {
        return !!($el.val() && $el.val() !== '');
      }
      return true;
    }
  }

  Validator.prototype.validateInput = function (e) {
    var self       = this;
    var $el        = $(e.target)
    var prevErrors = $el.data('bs.validator.errors')
    var errors

    /**
     * [if custom input element]
     * @param  {[type]} #el.is('.input-custom') [description]
     * @return {[type]}                         [description]
     */

    if (!$el.is(inputSelector) && !$el.is('input')) {
      $el = $el.parents('.input-custom');
    }

    if ($el.length < 1) {
      return
    }

    if ($el.is('.input-custom')) {
      $el.val = function() {
        return $el.attr('data-value');
      }
      doValidate();
      return
    }

    if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]')

    doValidate();

    function doValidate() {
      self.$element.trigger(e = $.Event('validate.bs.validator', {relatedTarget: $el[0]}))
      if (e.isDefaultPrevented()) return

      self.runValidators($el).done(function (errors) {
        $el.data('bs.validator.errors', errors)

        errors.length ? self.showErrors($el) : self.clearErrors($el)

        if (!prevErrors || errors.toString() !== prevErrors.toString()) {
          e = errors.length
            ? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})
            : $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})

          self.$element.trigger(e)
        }

        self.toggleSubmit()

        self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))
      })
    }
  }

  Validator.prototype.runValidators = function ($el) {
    var errors   = []
    var deferred = $.Deferred()
    var options  = this.options

    $el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()
    $el.data('bs.validator.deferred', deferred)

    function getErrorMessage(key) {
      return $el.data(key + '-error')
        || $el.data('error')
        || key == 'native' && $el[0].validationMessage
        || options.errors[key]
    }

    $.each(Validator.VALIDATORS, $.proxy(function (key, validator) {
      if (($el.data(key) || key == 'required' || key == 'native') && !validator.call(this, $el)) {
        var error = getErrorMessage(key)
        !~errors.indexOf(error) && errors.push(error)
      }
    }, this))

    if (!errors.length && $el.val() && $el.data('remote')) {
      this.defer($el, function () {
        var data = {}
        data[$el.attr('name')] = $el.val()
        $.get($el.data('remote'), data)
          .fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })
          .always(function () { deferred.resolve(errors)})
      })
    } else deferred.resolve(errors)

    return deferred.promise()
  }

  Validator.prototype.validate = function () {
    var delay = this.options.delay

    this.options.delay = 0
    this.$element.find(inputSelector).trigger('input.bs.validator')
    this.options.delay = delay

    return this
  }

  Validator.prototype.showErrors = function ($el) {
    var method = this.options.html ? 'html' : 'text'

    this.defer($el, function () {
      var $group = $el.closest('.form-group')
      var $block = $group.find('.help-block.with-errors')
      var $feedback = $group.find('.form-control-feedback')
      var errors = $el.data('bs.validator.errors')

      if (!errors.length) return

      errors = $('<ul/>')
        .addClass('list-unstyled')
        .append($.map(errors, function (error) { return $('<li/>')[method](error) }))

      $block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())
      $block.empty().append(errors)
      $group.addClass('has-error')

      $feedback.length
        && $feedback.removeClass('glyphicon-ok')
        && $feedback.addClass('glyphicon-warning-sign')
        && $group.removeClass('has-success')
    })
  }

  Validator.prototype.clearErrors = function ($el) {
    var $group = $el.closest('.form-group')
    var $block = $group.find('.help-block.with-errors')
    var $feedback = $group.find('.form-control-feedback')

    $block.html($block.data('bs.validator.originalContent'))
    $group.removeClass('has-error')

    $feedback.length
      && $feedback.removeClass('glyphicon-warning-sign')
      && $feedback.addClass('glyphicon-ok')
      && $group.addClass('has-success')
  }

  Validator.prototype.hasErrors = function () {
    function fieldErrors() {
      return !!($(this).data('bs.validator.errors') || []).length
    }

    return !!this.$element.find(inputSelector).filter(fieldErrors).length
  }

  Validator.prototype.isIncomplete = function () {
    function fieldIncomplete() {
      return this.type === 'checkbox' ? !this.checked                                   :
             this.type === 'radio'    ? !$('[name="' + this.name + '"]:checked').length :
                                        $.trim(this.value) === ''
    }

    return !!this.$element.find(nativeInputSelector).filter('[required]').filter(fieldIncomplete).length
  }

  Validator.prototype.onSubmit = function (e) {
    this.validate()
    if (this.isIncomplete() || this.hasErrors()) e.preventDefault()
  }

  Validator.prototype.toggleSubmit = function () {
    if(!this.options.disable) return
    var $btn = $('button[type="submit"], input[type="submit"]')
      .filter('[form="' + this.$element.attr('id') + '"]')
      .add(this.$element.find('input[type="submit"], button[type="submit"]'))
    $btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
      .css({'pointer-events': 'all', 'cursor': 'pointer'})
  }

  Validator.prototype.defer = function ($el, callback) {
    if (!this.options.delay) return callback()
    window.clearTimeout($el.data('bs.validator.timeout'))
    $el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))
  }

  Validator.prototype.destroy = function () {
    this.$element
      .removeAttr('novalidate')
      .removeData('bs.validator')
      .off('.bs.validator')

    this.$element.find(inputSelector)
      .off('.bs.validator')
      .removeData(['bs.validator.errors', 'bs.validator.deferred'])
      .each(function () {
        var $this = $(this)
        var timeout = $this.data('bs.validator.timeout')
        window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')
      })

    this.$element.find('.help-block.with-errors').each(function () {
      var $this = $(this)
      var originalContent = $this.data('bs.validator.originalContent')

      $this
        .removeData('bs.validator.originalContent')
        .html(originalContent)
    })

    this.$element.find('input[type="submit"], button[type="submit"]').removeClass('disabled')

    this.$element.find('.has-error').removeClass('has-error')

    return this
  }

  // VALIDATOR PLUGIN DEFINITION
  // ===========================

  function Plugin(option) {
    return this.each(function () {
      var $this   = $(this)
      var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)
      var data    = $this.data('bs.validator')

      if (!data && option == 'destroy') return
      if (!data) $this.data('bs.validator', (data = new Validator(this, options)))
      if (typeof option == 'string') data[option]()
    })
  }

  var old = $.fn.validator

  $.fn.validator             = Plugin
  $.fn.validator.Constructor = Validator

  // VALIDATOR NO CONFLICT
  // =====================

  $.fn.validator.noConflict = function () {
    $.fn.validator = old
    return this
  }

  // VALIDATOR DATA-API
  // ==================

  $(window).on('load', function () {
    $('form[data-toggle="validator"]').each(function () {
      var $form = $(this)
      Plugin.call($form, $form.data())
    })
  })

}(jQuery);
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc service
 * @name flowsimUiApp.ports
 * @description
 * # ports
 * Service in the flowsimUiApp.
 */
angular.module('flowsimUiApp')
  .factory('Ports', function(fgConstraints, ETHERNET) {

var defaultPorts  = 24;
var defaultMAC    = '00:00:00:00:00:01';
var defaultName   = 'eth';
var defaultSpeed  = '1_gbps';
var defaultMedium = 'Fiber';
var defaultMode   = 'full_duplex';
var defaultSpeed  = '10_gbps';

var Port = {};

Port.TIPS = {
  port_id: 'Ethernet port identifier',
  mac:     'MAC address used by port in spanning tree',
  name:    'display name of the port',
  speed:   'physical layer speed of port',
  mode:    'physical layer transmission mode',
  medium:  'physical layer medium of port'
};

Port.TESTS = {
  mac: ETHERNET.isMac,
  name: function(v) { return /[a-zA-Z_][a-zA-Z_0-9]*/.test(v) }
}

Port.Capabilities = function(port) {
  if(typeof port === 'number') {
    // default construction
    this.port_id = port;
    this.mac     = defaultMAC;
    this.name    = defaultName + this.port_id;
    this.speed   = defaultSpeed;
    this.mode    = defaultMode;
    this.medium  = defaultMedium;
  } else {
    // copy construction
    if(!port instanceof Port.Capabilities) {
      _.extend(this, port);
    }
    this.port_id = port.port_id;
    this.mac     = port.mac;
    this.name    = port.name;
    this.speed   = port.speed;
    this.mode    = port.mode;
    this.medium  = port.medium;
  }
}

Port.Configuration = function(port) {
  if(port) {
    if(port instanceof Port.Capabilities) {
      // Capability constructor
      this.capabilities = port;
    } else if(port instanceof Port.Configuration) {
      // Copy constructor
    } else {
      // JSON constructor
      _.extend(this, port);
    }
  } else {
    // default constructor
  }
}

Port.Speeds = [{
  label: '10 Mbps',
  value: '10_mbps'
}, {
  label: '100 Mbps',
  value: '100_mbps'
}, {
  label: '1 Gbps',
  value: '1_gbps'
}, {
  label: '10 Gbps',
  value: '10_gbps'
}, {
  label: '40 Gbps',
  value: '40_gbps'
}, {
  label: '100 Gbps',
  value: '100_gbps'
}, {
  label: '1 Tbps',
  value: '1_tbps'
}];

Port.Modes = [{
  label: 'Half Duplex',
  value: 'half_duplex'
}, {
  label: 'Full Duplex',
  value: 'full_duplex'
}];

Port.Mediums = [
  'Copper',
  'Fiber'
];

function Capabilities(ports) {
  if(ports) {
    // copy constructor
    if(!ports instanceof Ports) {
      _.extend(this, ports);
    }
    this.n_ports = ports.n_ports;
    this.ports = _.map(ports.ports, function(port) {
      return new Port.Capabilities(port);
    });
    this.speeds = _.map(ports.speeds, function(speed) {
      return _.clone(speed);
    });
    this.modes = _.map(ports.speeds, function(mode) {
      return _.clone(mode);
    });
    this.mediums = _.clone(ports.mediums);
    this.vports = _.clone(ports.vports);
  } else {
    // default constructor
    this.n_ports = defaultPorts;
    this.ports = _.map(_.range(this.n_ports), function(id) {
      return new Port.Capabilities(id);
    });
    this.vports = {
      port_stats: true,
      stp:        false,
      in_port:    true,
      table:      true,
      normal:     true,
      flood:      true,
      all:        true,
      controller: true,
      local:      true,
      any:        true,
      none:       true
    };
    this.speeds = _.map(Port.Speeds, function(speed) {
      return _.clone(speed);
    });
    this.modes = _.map(Port.Modes, function(mode) {
      return _.clone(mode);
    });
    this.mediums = _.clone(Port.Mediums);
  }
}

Capabilities.prototype.rebuild = function() {
  var i;
  if(this.n_ports === this.ports.length) {
    return;
  } else if(this.n_ports < this.ports.length) {
    this.ports.splice(this.n_ports, this.ports.length-this.n_ports);
  } else {
    for(i=this.ports.length; i<this.n_ports; ++i) {
      this.ports.push(new Port.Capabilities(i));
    }
  }
}

Capabilities.prototype.openflow_1_0 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        true, // optional
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true,
    any:        false,
    none:       true
  };

  // set number of ports?
  // construct ports?
};

Capabilities.prototype.openflow_1_1 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_2 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_3 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_4 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

function Configuration(ports) {
  if(ports) {
    if(ports instanceof Capabilities) {
      // capability constructor
    } else if(ports instanceof Configuration) {
      // copy constructor
    } else {
      // JSON constructor
      _.extend(this, ports);
    }
  } else {
    // default constructor
  }
}

var TIPS = {
  Port: Port.TIPS,
};

var TESTS = {
  Port: Port.TESTS,
};

return {
  Capabilities: Capabilities,
  Configuration: Configuration,
  TIPS: TIPS,
  TESTS: TESTS,
  MEDIUMS: Port.MEDIUMS,
  MODES: Port.MODES,
  SPEEDS: Port.SPEEDS
};

});
<commit_msg>latest<commit_after>'use strict';

/**
 * @ngdoc service
 * @name flowsimUiApp.ports
 * @description
 * # ports
 * Service in the flowsimUiApp.
 */
angular.module('flowsimUiApp')
  .factory('Ports', function(fgConstraints, ETHERNET) {

var defaultPorts  = 24;
var defaultMAC    = '00:00:00:00:00:01';
var defaultName   = 'eth';
var defaultSpeed  = '1_gbps';
var defaultMedium = 'Fiber';
var defaultMode   = 'full_duplex';
var defaultSpeed  = '10_gbps';

var Port = {};

Port.TIPS = {
  port_id: 'Ethernet port identifier',
  mac:     'MAC address used by port in spanning tree',
  name:    'display name of the port',
  speed:   'physical layer speed of port',
  mode:    'physical layer transmission mode',
  medium:  'physical layer medium of port'
};

Port.TESTS = {
  mac: ETHERNET.isMac,
  name: function(v) { return /[a-zA-Z_][a-zA-Z_0-9]*/.test(v) }
}

Port.Capabilities = function(port) {
  if(typeof port === 'number') {
    // default construction
    this.port_id = port;
    this.mac     = defaultMAC;
    this.name    = defaultName + this.port_id;
    this.speed   = defaultSpeed;
    this.mode    = defaultMode;
    this.medium  = defaultMedium;
  } else {
    // copy construction
    if(!port instanceof Port.Capabilities) {
      _.extend(this, port);
    }
    this.port_id = port.port_id;
    this.mac     = port.mac;
    this.name    = port.name;
    this.speed   = port.speed;
    this.mode    = port.mode;
    this.medium  = port.medium;
  }
}

Port.Configuration = function(port) {
  if(port) {
    if(port instanceof Port.Capabilities) {
      // Capability constructor
      this.capabilities = port;
    } else if(port instanceof Port.Configuration) {
      // Copy constructor
    } else {
      // JSON constructor
      _.extend(this, port);
    }
  } else {
    // default constructor
  }
}

Port.Speeds = [{
  label: '10 Mbps',
  value: '10_mbps'
}, {
  label: '100 Mbps',
  value: '100_mbps'
}, {
  label: '1 Gbps',
  value: '1_gbps'
}, {
  label: '10 Gbps',
  value: '10_gbps'
}, {
  label: '40 Gbps',
  value: '40_gbps'
}, {
  label: '100 Gbps',
  value: '100_gbps'
}, {
  label: '1 Tbps',
  value: '1_tbps'
}];

Port.Modes = [{
  label: 'Half Duplex',
  value: 'half_duplex'
}, {
  label: 'Full Duplex',
  value: 'full_duplex'
}];

Port.Mediums = [
  'Copper',
  'Fiber'
];

function Capabilities(ports) {
  if(ports) {
    // copy constructor
    if(!ports instanceof Ports) {
      _.extend(this, ports);
    }
    this.n_ports = ports.n_ports;
    this.ports = _.map(ports.ports, function(port) {
      return new Port.Capabilities(port);
    });
    this.speeds = _.clone(ports.speeds);
    this.modes = _.clone(ports.modes);
    this.mediums = _.clone(ports.mediums);
    this.vports = _.clone(ports.vports);
  } else {
    // default constructor
    this.n_ports = defaultPorts;
    this.ports = _.map(_.range(this.n_ports), function(id) {
      return new Port.Capabilities(id);
    });
    this.vports = {
      port_stats: true,
      stp:        false,
      in_port:    true,
      table:      true,
      normal:     true,
      flood:      true,
      all:        true,
      controller: true,
      local:      true,
      any:        true,
      none:       true
    };
    this.speeds = {
      '10_mbps': true,
      '100_mbps': true,
      '1_gbps': true,
      '10_gbps': true,
      '40_gbps': true,
      '100_bgps': true,
      '1_tbps': true
    };
    this.mediums = {
      'Copper': true,
      'Fiber': true
    };
    this.modes = {
      'half_duplex': true,
      'full_duplex': true
    };
  }
}

Capabilities.prototype.rebuild = function() {
  var i;
  if(this.n_ports === this.ports.length) {
    return;
  } else if(this.n_ports < this.ports.length) {
    this.ports.splice(this.n_ports, this.ports.length-this.n_ports);
  } else {
    for(i=this.ports.length; i<this.n_ports; ++i) {
      this.ports.push(new Port.Capabilities(i));
    }
  }
}

Capabilities.prototype.openflow_1_0 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        true, // optional
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true,
    any:        false,
    none:       true
  };

  // set number of ports?
  // construct ports?
};

Capabilities.prototype.openflow_1_1 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_2 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_3 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

Capabilities.prototype.openflow_1_4 = function() {
  // Ports
  this.vports = {
    port_stats: true,
    stp:        false,
    in_port:    true,
    table:      true,
    normal:     true, // optional
    flood:      true, // optional
    all:        true,
    controller: true,
    local:      true, // optional
    any:        true,
    none:       false
  };
};

function Configuration(ports) {
  if(ports) {
    if(ports instanceof Capabilities) {
      // capability constructor
    } else if(ports instanceof Configuration) {
      // copy constructor
    } else {
      // JSON constructor
      _.extend(this, ports);
    }
  } else {
    // default constructor
  }
}

var TIPS = {
  Port: Port.TIPS,
};

var TESTS = {
  Port: Port.TESTS,
};

return {
  Capabilities: Capabilities,
  Configuration: Configuration,
  TIPS: TIPS,
  TESTS: TESTS,
  MEDIUMS: Port.MEDIUMS,
  MODES: Port.MODES,
  SPEEDS: Port.SPEEDS
};

});
<|endoftext|>


<commit_before>// @flow
import { Select, Card, Form, Row, Col, DatePicker, Spin } from "antd";
import * as React from "react";
import ReactDOMServer from "react-dom/server";
import { connect } from "react-redux";
import _ from "lodash";
import moment from "moment";
import FormattedDate from "components/formatted_date";
import { type OxalisState } from "oxalis/store";
import type { APIUser, APITimeTracking } from "admin/api_flow_types";
import { formatMilliseconds, formatDurationToMinutesAndSeconds } from "libs/format_utils";
import { isUserAdmin } from "libs/utils";
import { getEditableUsers, getTimeTrackingForUser } from "admin/admin_rest_api";
import Toast from "libs/toast";
import messages from "messages";
import { enforceActiveUser } from "oxalis/model/accessors/user_accessor";
import TimeTrackingChart, {
  type DateRange,
  type ColumnDefinition,
  type RowContent,
} from "./time_line_chart_view";

const FormItem = Form.Item;
const { Option } = Select;
const { RangePicker } = DatePicker;

const dayFormat = "dd, MMM, YYYY";
const hourFormat = "HH:mm";

type TimeTrackingStats = {
  totalTime: number,
  numberTasks: number,
  averageTimePerTask: number,
};

type StateProps = {|
  activeUser: APIUser,
|};

type Props = {| ...StateProps |};

type State = {
  user: ?APIUser,
  users: Array<APIUser>,
  dateRange: DateRange,
  timeTrackingData: Array<APITimeTracking>,
  stats: TimeTrackingStats,
  isLoading: boolean,
};

function compressTimeLogs(logs) {
  logs.sort((a, b) => a.timestamp - b.timestamp);

  const compressedLogs = [];
  let previousLog = null;
  for (const timeLog of logs) {
    // If the current log is within 1s of the previous log, merge these two logs
    const previousDuration = previousLog != null ? moment.duration(previousLog.time) : null;
    if (
      previousDuration != null &&
      previousLog != null &&
      Math.abs(timeLog.timestamp - (previousLog.timestamp + previousDuration.asMilliseconds())) <
        1000 &&
      timeLog.task_id === previousLog.task_id
    ) {
      const newDuration = previousDuration.add(moment.duration(timeLog.time));
      const copiedLog = { ...compressedLogs[compressedLogs.length - 1] };
      copiedLog.time = newDuration.toISOString();
      compressedLogs[compressedLogs.length - 1] = copiedLog;
    } else {
      compressedLogs.push(timeLog);
    }

    previousLog = compressedLogs[compressedLogs.length - 1];
  }

  return compressedLogs;
}

class TimeLineView extends React.PureComponent<Props, State> {
  state = {
    user: null,
    users: [],
    dateRange: [moment().startOf("day"), moment().endOf("day")],
    timeTrackingData: [],
    stats: {
      totalTime: 0,
      numberTasks: 0,
      averageTimePerTask: 0,
    },
    isLoading: false,
  };

  componentDidMount() {
    const isAdminOrTeamManger = isUserAdmin(this.props.activeUser);
    if (isAdminOrTeamManger) {
      this.fetchData();
    } else {
      this.fetchDataFromLoggedInUser();
    }
  }

  async fetchData() {
    const users = await getEditableUsers();
    this.setState({ users });
  }

  async fetchTimeTrackingData() {
    this.setState({ isLoading: true });
    if (this.state.user != null) {
      /* eslint-disable react/no-access-state-in-setstate */
      const timeTrackingData = compressTimeLogs(
        await getTimeTrackingForUser(
          this.state.user.id,
          this.state.dateRange[0],
          this.state.dateRange[1],
        ),
      );

      this.setState({ timeTrackingData }, this.calculateStats);
      /* eslint-enable react/no-access-state-in-setstate */
    }
    this.setState({ isLoading: false });
  }

  calculateStats() {
    this.setState(prevState => {
      const totalTime = _.sumBy(prevState.timeTrackingData, timeSpan =>
        moment.duration(timeSpan.time).asMilliseconds(),
      );
      const numberTasks = _.uniq(prevState.timeTrackingData.map(timeSpan => timeSpan.annotation))
        .length;

      // prevent division by zero
      const averageTimePerTask = numberTasks === 0 ? 0 : totalTime / numberTasks;

      return {
        stats: {
          totalTime,
          numberTasks,
          averageTimePerTask,
        },
      };
    });
  }

  fetchDataFromLoggedInUser = async () => {
    await this.setState({ user: this.props.activeUser });
    this.fetchTimeTrackingData();
  };

  handleUserChange = async (userId: number) => {
    await this.setState(prevState => ({ user: prevState.users.find(u => u.id === userId) }));
    this.fetchTimeTrackingData();
  };

  handleDateChange = async (dates: DateRange) => {
    // to ease the load on the server restrict date range selection to a month
    if (Math.abs(dates[0].diff(dates[1], "days")) > 31) {
      Toast.error(messages["timetracking.date_range_too_long"]);
      return;
    }

    // for same day use start and end timestamps
    const dateRange = dates[0].isSame(dates[1], "day")
      ? [dates[0].startOf("day"), dates[1].endOf("day")]
      : dates;

    await this.setState({ dateRange });
    this.fetchTimeTrackingData();
  };

  getTooltipForEntry(taskId: string, start: Date, end: Date) {
    const isSameDay = start.getUTCDate() === end.getUTCDate();
    const duration = end - start;
    const durationAsString = formatDurationToMinutesAndSeconds(duration);
    const dayFormatForMomentJs = "DD, MMM, YYYY";
    const tooltip = (

          Task ID: {taskId}

              Date:

                {isSameDay ? (
                  <FormattedDate timestamp={start} format={dayFormatForMomentJs} />
                ) : (
                  <React.Fragment>
                    <FormattedDate timestamp={start} format={dayFormatForMomentJs} /> -{" "}
                    <FormattedDate timestamp={end} format={dayFormatForMomentJs} />
                  </React.Fragment>
                )}

              Time:

                <FormattedDate timestamp={start} format={hourFormat} /> -{" "}
                <FormattedDate timestamp={end} format={hourFormat} />

              Duration (min:sec):
              {durationAsString}

    );
    return ReactDOMServer.renderToStaticMarkup(tooltip);
  }

  render() {
    const columns: Array<ColumnDefinition> = [
      { id: "AnnotationId", type: "string" },
      // This label columns is somehow needed to make the custom tooltip work.
      // See https://developers.google.com/chart/interactive/docs/gallery/timeline#customizing-tooltips.
      { type: "string", id: "empty label" },
      { type: "string", role: "tooltip", p: { html: true } },
      { id: "Start", type: "date" },
      { id: "End", type: "date" },
    ];

    const { dateRange, isLoading, timeTrackingData } = this.state;
    const timeTrackingRowGrouped: Array<RowContent> = []; // shows each time span grouped by annotation id
    const timeTrackingRowTotal: Array<RowContent> = []; // show all times spans in a single row

    const totalSumColumnLabel = "Sum Tracking Time";

    this.state.timeTrackingData.forEach((datum: APITimeTracking) => {
      const duration = moment.duration(datum.time).asMilliseconds();
      const start = new Date(datum.timestamp);
      const end = new Date(datum.timestamp + duration);
      const individualTooltipAsString = this.getTooltipForEntry(datum.task_id, start, end);
      const totalTooltipAsString = this.getTooltipForEntry(totalSumColumnLabel, start, end);

      timeTrackingRowGrouped.push([datum.task_id, "", individualTooltipAsString, start, end]);
      timeTrackingRowTotal.push([totalSumColumnLabel, "", totalTooltipAsString, start, end]);
    });

    const rows = timeTrackingRowTotal.concat(timeTrackingRowGrouped);

    const formItemLayout = {
      labelCol: { span: 5 },
      wrapperCol: { span: 19 },
    };

    const paddingBottom = {
      paddingBottom: 5,
    };

    const displayInDays = Math.abs(dateRange[0].diff(dateRange[1], "days")) >= 1;
    const timeAxisFormat = displayInDays ? dayFormat : hourFormat;

    const { firstName, lastName, email } = this.props.activeUser;
    const isAdminOrTeamManger = isUserAdmin(this.props.activeUser);

    return (

Time Tracking

        <Card>
          <Row gutter={40}>
            <Col span={12}>
              <FormItem {...formItemLayout} label="User">
                {isAdminOrTeamManger ? (
                  <Select
                    allowClear
                    showSearch
                    placeholder="Select a User"
                    optionFilterProp="children"
                    style={{ width: "100%" }}
                    onChange={this.handleUserChange}
                  >
                    {this.state.users
                      .filter(u => u.isActive)
                      .map((user: APIUser) => (
                        <Option key={user.id} value={user.id}>
                          {`${user.lastName}, ${user.firstName} (${user.email})`}
                        </Option>
                      ))}
                  </Select>
                ) : (

                          {lastName}, {firstName}

                         {email}

                )}
              </FormItem>
              <FormItem {...formItemLayout} label="Date">
                <RangePicker
                  allowClear={false}
                  style={{ width: "100%" }}
                  value={dateRange}
                  onChange={this.handleDateChange}
                />
              </FormItem>
            </Col>
            <Col span={12}>
              <Row>
                <Col span={8}>

Total Time:

Number of Tasks:

Average Time per Task:

                </Col>
                <Col span={16}>

{formatMilliseconds(this.state.stats.totalTime)}

{this.state.stats.numberTasks}

{formatMilliseconds(this.state.stats.averageTimePerTask)}

                </Col>
              </Row>
              The time tracking information display here only includes data acquired when working on
              "tasks" and not explorative annotations.
            </Col>
          </Row>
        </Card>

        <Spin size="large" spinning={isLoading}>
          {timeTrackingData.length > 0 ? (
            <TimeTrackingChart
              columns={columns}
              rows={rows}
              timeAxisFormat={timeAxisFormat}
              dateRange={dateRange}
              timeTrackingData={timeTrackingData}
            />
          ) : (

              No Time Tracking Data for the Selected User or Date Range.

          )}
        </Spin>

    );
  }
}

const mapStateToProps = (state: OxalisState) => ({
  activeUser: enforceActiveUser(state.activeUser),
});

export default connect<Props, {||}, _, _, _, _>(mapStateToProps)(TimeLineView);
<commit_msg>show seconds in timeline view tooltip (#4574)<commit_after>// @flow
import { Select, Card, Form, Row, Col, DatePicker, Spin } from "antd";
import * as React from "react";
import ReactDOMServer from "react-dom/server";
import { connect } from "react-redux";
import _ from "lodash";
import moment from "moment";
import FormattedDate from "components/formatted_date";
import { type OxalisState } from "oxalis/store";
import type { APIUser, APITimeTracking } from "admin/api_flow_types";
import { formatMilliseconds, formatDurationToMinutesAndSeconds } from "libs/format_utils";
import { isUserAdmin } from "libs/utils";
import { getEditableUsers, getTimeTrackingForUser } from "admin/admin_rest_api";
import Toast from "libs/toast";
import messages from "messages";
import { enforceActiveUser } from "oxalis/model/accessors/user_accessor";
import TimeTrackingChart, {
  type DateRange,
  type ColumnDefinition,
  type RowContent,
} from "./time_line_chart_view";

const FormItem = Form.Item;
const { Option } = Select;
const { RangePicker } = DatePicker;

const dayFormat = "dd, MMM, YYYY";
const hourFormat = "HH:mm";
const hourFormatPrecise = "HH:mm:ss";

type TimeTrackingStats = {
  totalTime: number,
  numberTasks: number,
  averageTimePerTask: number,
};

type StateProps = {|
  activeUser: APIUser,
|};

type Props = {| ...StateProps |};

type State = {
  user: ?APIUser,
  users: Array<APIUser>,
  dateRange: DateRange,
  timeTrackingData: Array<APITimeTracking>,
  stats: TimeTrackingStats,
  isLoading: boolean,
};

function compressTimeLogs(logs) {
  logs.sort((a, b) => a.timestamp - b.timestamp);

  const compressedLogs = [];
  let previousLog = null;
  for (const timeLog of logs) {
    // If the current log is within 1s of the previous log, merge these two logs
    const previousDuration = previousLog != null ? moment.duration(previousLog.time) : null;
    if (
      previousDuration != null &&
      previousLog != null &&
      Math.abs(timeLog.timestamp - (previousLog.timestamp + previousDuration.asMilliseconds())) <
        1000 &&
      timeLog.task_id === previousLog.task_id
    ) {
      const newDuration = previousDuration.add(moment.duration(timeLog.time));
      const copiedLog = { ...compressedLogs[compressedLogs.length - 1] };
      copiedLog.time = newDuration.toISOString();
      compressedLogs[compressedLogs.length - 1] = copiedLog;
    } else {
      compressedLogs.push(timeLog);
    }

    previousLog = compressedLogs[compressedLogs.length - 1];
  }

  return compressedLogs;
}

class TimeLineView extends React.PureComponent<Props, State> {
  state = {
    user: null,
    users: [],
    dateRange: [moment().startOf("day"), moment().endOf("day")],
    timeTrackingData: [],
    stats: {
      totalTime: 0,
      numberTasks: 0,
      averageTimePerTask: 0,
    },
    isLoading: false,
  };

  componentDidMount() {
    const isAdminOrTeamManger = isUserAdmin(this.props.activeUser);
    if (isAdminOrTeamManger) {
      this.fetchData();
    } else {
      this.fetchDataFromLoggedInUser();
    }
  }

  async fetchData() {
    const users = await getEditableUsers();
    this.setState({ users });
  }

  async fetchTimeTrackingData() {
    this.setState({ isLoading: true });
    if (this.state.user != null) {
      /* eslint-disable react/no-access-state-in-setstate */
      const timeTrackingData = compressTimeLogs(
        await getTimeTrackingForUser(
          this.state.user.id,
          this.state.dateRange[0],
          this.state.dateRange[1],
        ),
      );

      this.setState({ timeTrackingData }, this.calculateStats);
      /* eslint-enable react/no-access-state-in-setstate */
    }
    this.setState({ isLoading: false });
  }

  calculateStats() {
    this.setState(prevState => {
      const totalTime = _.sumBy(prevState.timeTrackingData, timeSpan =>
        moment.duration(timeSpan.time).asMilliseconds(),
      );
      const numberTasks = _.uniq(prevState.timeTrackingData.map(timeSpan => timeSpan.annotation))
        .length;

      // prevent division by zero
      const averageTimePerTask = numberTasks === 0 ? 0 : totalTime / numberTasks;

      return {
        stats: {
          totalTime,
          numberTasks,
          averageTimePerTask,
        },
      };
    });
  }

  fetchDataFromLoggedInUser = async () => {
    await this.setState({ user: this.props.activeUser });
    this.fetchTimeTrackingData();
  };

  handleUserChange = async (userId: number) => {
    await this.setState(prevState => ({ user: prevState.users.find(u => u.id === userId) }));
    this.fetchTimeTrackingData();
  };

  handleDateChange = async (dates: DateRange) => {
    // to ease the load on the server restrict date range selection to a month
    if (Math.abs(dates[0].diff(dates[1], "days")) > 31) {
      Toast.error(messages["timetracking.date_range_too_long"]);
      return;
    }

    // for same day use start and end timestamps
    const dateRange = dates[0].isSame(dates[1], "day")
      ? [dates[0].startOf("day"), dates[1].endOf("day")]
      : dates;

    await this.setState({ dateRange });
    this.fetchTimeTrackingData();
  };

  getTooltipForEntry(taskId: string, start: Date, end: Date) {
    const isSameDay = start.getUTCDate() === end.getUTCDate();
    const duration = end - start;
    const durationAsString = formatDurationToMinutesAndSeconds(duration);
    const dayFormatForMomentJs = "DD MMM, YYYY";
    const tooltip = (

          Task ID: {taskId}

              Date:

                {isSameDay ? (
                  <FormattedDate timestamp={start} format={dayFormatForMomentJs} />
                ) : (
                  <React.Fragment>
                    <FormattedDate timestamp={start} format={dayFormatForMomentJs} /> –{" "}
                    <FormattedDate timestamp={end} format={dayFormatForMomentJs} />
                  </React.Fragment>
                )}

              Time:

                <FormattedDate timestamp={start} format={hourFormatPrecise} /> –{" "}
                <FormattedDate timestamp={end} format={hourFormatPrecise} />

              Duration (min:sec):
              {durationAsString}

    );
    return ReactDOMServer.renderToStaticMarkup(tooltip);
  }

  render() {
    const columns: Array<ColumnDefinition> = [
      { id: "AnnotationId", type: "string" },
      // This label columns is somehow needed to make the custom tooltip work.
      // See https://developers.google.com/chart/interactive/docs/gallery/timeline#customizing-tooltips.
      { type: "string", id: "empty label" },
      { type: "string", role: "tooltip", p: { html: true } },
      { id: "Start", type: "date" },
      { id: "End", type: "date" },
    ];

    const { dateRange, isLoading, timeTrackingData } = this.state;
    const timeTrackingRowGrouped: Array<RowContent> = []; // shows each time span grouped by annotation id
    const timeTrackingRowTotal: Array<RowContent> = []; // show all times spans in a single row

    const totalSumColumnLabel = "Sum Tracking Time";

    this.state.timeTrackingData.forEach((datum: APITimeTracking) => {
      const duration = moment.duration(datum.time).asMilliseconds();
      const start = new Date(datum.timestamp);
      const end = new Date(datum.timestamp + duration);
      const individualTooltipAsString = this.getTooltipForEntry(datum.task_id, start, end);
      const totalTooltipAsString = this.getTooltipForEntry(totalSumColumnLabel, start, end);

      timeTrackingRowGrouped.push([datum.task_id, "", individualTooltipAsString, start, end]);
      timeTrackingRowTotal.push([totalSumColumnLabel, "", totalTooltipAsString, start, end]);
    });

    const rows = timeTrackingRowTotal.concat(timeTrackingRowGrouped);

    const formItemLayout = {
      labelCol: { span: 5 },
      wrapperCol: { span: 19 },
    };

    const paddingBottom = {
      paddingBottom: 5,
    };

    const displayInDays = Math.abs(dateRange[0].diff(dateRange[1], "days")) >= 1;
    const timeAxisFormat = displayInDays ? dayFormat : hourFormat;

    const { firstName, lastName, email } = this.props.activeUser;
    const isAdminOrTeamManger = isUserAdmin(this.props.activeUser);

    return (

Time Tracking

        <Card>
          <Row gutter={40}>
            <Col span={12}>
              <FormItem {...formItemLayout} label="User">
                {isAdminOrTeamManger ? (
                  <Select
                    allowClear
                    showSearch
                    placeholder="Select a User"
                    optionFilterProp="children"
                    style={{ width: "100%" }}
                    onChange={this.handleUserChange}
                  >
                    {this.state.users
                      .filter(u => u.isActive)
                      .map((user: APIUser) => (
                        <Option key={user.id} value={user.id}>
                          {`${user.lastName}, ${user.firstName} (${user.email})`}
                        </Option>
                      ))}
                  </Select>
                ) : (

                          {lastName}, {firstName}

                         {email}

                )}
              </FormItem>
              <FormItem {...formItemLayout} label="Date">
                <RangePicker
                  allowClear={false}
                  style={{ width: "100%" }}
                  value={dateRange}
                  onChange={this.handleDateChange}
                />
              </FormItem>
            </Col>
            <Col span={12}>
              <Row>
                <Col span={8}>

Total Time:

Number of Tasks:

Average Time per Task:

                </Col>
                <Col span={16}>

{formatMilliseconds(this.state.stats.totalTime)}

{this.state.stats.numberTasks}

{formatMilliseconds(this.state.stats.averageTimePerTask)}

                </Col>
              </Row>
              The time tracking information display here only includes data acquired when working on
              "tasks" and not explorative annotations.
            </Col>
          </Row>
        </Card>

        <Spin size="large" spinning={isLoading}>
          {timeTrackingData.length > 0 ? (
            <TimeTrackingChart
              columns={columns}
              rows={rows}
              timeAxisFormat={timeAxisFormat}
              dateRange={dateRange}
              timeTrackingData={timeTrackingData}
            />
          ) : (

              No Time Tracking Data for the Selected User or Date Range.

          )}
        </Spin>

    );
  }
}

const mapStateToProps = (state: OxalisState) => ({
  activeUser: enforceActiveUser(state.activeUser),
});

export default connect<Props, {||}, _, _, _, _>(mapStateToProps)(TimeLineView);
<|endoftext|>


<commit_before>const stopASGs = require('./asgs/stopASGs');
const listASGsToStop = require('./asgs/listASGsToStop');
const tagASGs = require('./asgs/tagASGs');
const listInstancesToStop = require('./instances/listInstancesToStop');
const tagInstances = require('./instances/tagInstances');
const stopInstances = require('./instances/stopInstances');

function spinDownASGs() {
  return listASGsToStop()
    .then((stoppableASGs) => {
      console.log(`Found the following ${stoppableASGs.length} instances to spin down...`);
      if (stoppableASGs.length === 0) {
        console.log('None! Moving on.');
        return 'No ASGs to spin down';
      }

      stoppableASGs.forEach((asg) => {
        console.log(asg.AutoScalingGroupName);
      });
      return tagASGs(stoppableASGs);
    })
    .then((taggedASGs) => {
      console.log(`Finished tagging ASGs. Moving on to spin down ${taggedASGs.length} of them.`);
      return stopASGs(taggedASGs);
    });
}

function stopAllInstances() {
  return listInstancesToStop()
    .then((stoppableInstances) => {
      console.log('Found the following instances to shut down...');
      if (stoppableInstances.length === 0) {
        console.log('None! Moving on.');
        return 'No instances to shut down';
      }

      stoppableInstances.forEach((instance) => {
        console.log(instance);
      });
      return tagInstances(stoppableInstances);
    })
    .then((taggedInstances) => {
      console.log('Finished tagging instances. Moving on to stop them.');
      return stopInstances(taggedInstances);
    });
}

module.exports = function stop(event, context, callback) {
  console.log('Stop. Hammertime!');
  Promise.all([
    stopAllInstances(),
    spinDownASGs(),
  ]).then(() => {
    console.log('All instances and ASGs stopped successfully. Good night!');
    callback(null, { message: 'Stop: Hammertime successfully completed.' }, event);
  }).catch((err) => {
    console.error(err);
    callback(err);
  });
};
<commit_msg>:bug: Handles zero instances or ASGs to spin down elegantly with empty promise<commit_after>const stopASGs = require('./asgs/stopASGs');
const listASGsToStop = require('./asgs/listASGsToStop');
const tagASGs = require('./asgs/tagASGs');
const listInstancesToStop = require('./instances/listInstancesToStop');
const tagInstances = require('./instances/tagInstances');
const stopInstances = require('./instances/stopInstances');

function spinDownASGs() {
  return listASGsToStop()
    .then((stoppableASGs) => {
      console.log(`Found the following ${stoppableASGs.length} instances to spin down...`);
      if (stoppableASGs.length === 0) {
        console.log('None! Moving on.');
        return [];
      }

      stoppableASGs.forEach((asg) => {
        console.log(asg.AutoScalingGroupName);
      });
      return tagASGs(stoppableASGs);
    })
    .then((taggedASGs) => {
      if (taggedASGs.length > 0) {
        console.log(`Finished tagging ASGs. Moving on to spin down ${taggedASGs.length} of them.`);
        return stopASGs(taggedASGs);
      }
    });
}

function stopAllInstances() {
  return listInstancesToStop()
    .then((stoppableInstances) => {
      console.log('Found the following instances to shut down...');
      if (stoppableInstances.length === 0) {
        console.log('None! Moving on.');
        return [];
      }

      stoppableInstances.forEach((instance) => {
        console.log(instance);
      });
      return tagInstances(stoppableInstances);
    })
    .then((taggedInstances) => {
      if (taggedInstances.length > 0) {
        console.log('Finished tagging instances. Moving on to stop them.');
        return stopInstances(taggedInstances);
      }
    });
}

module.exports = function stop(event, context, callback) {
  console.log('Stop. Hammertime!');
  Promise.all([
    stopAllInstances(),
    spinDownASGs(),
  ]).then(() => {
    console.log('All instances and ASGs stopped successfully. Good night!');
    callback(null, { message: 'Stop: Hammertime successfully completed.' }, event);
  }).catch((err) => {
    console.error(err);
    callback(err);
  });
};
<|endoftext|>


<commit_before>(function () {
    var utils = {
        getIcon: function (question) {
            if (question.tag === 'upload') {
                return ' ';
            }
            return '';
        },
        getDisplay: function (question, MAXLEN) {
            return utils.getIcon(question) + utils.getLabel(question, MAXLEN)
                    + " (" + (question.hashtagValue || question.value) + ")";
        },
        getTruncatedDisplay: function (question, MAXLEN) {
            return utils.getIcon(question) + utils.getLabel(question, MAXLEN)
                    + " (" + utils.truncateValue(question.hashtagValue || question.value, MAXLEN) + ")";
        },
        getLabel: function (question, MAXLEN) {
            return utils.truncateLabel((question.repeat ? '- ' : '')
                    + question.label, question.tag === 'hidden' ? ' (Hidden)' : '', MAXLEN);
        },
        truncateLabel: function (label, suffix, MAXLEN) {
            suffix = suffix || "";
            var MAXLEN = MAXLEN || 40,
                maxlen = MAXLEN - suffix.length;
            return ((label.length <= maxlen) ? (label) : (label.slice(0, maxlen) + "...")) + suffix;
        },
        truncateValue: function (value, MAXLEN) {
            MAXLEN = MAXLEN || 40;
            return (value.length <= MAXLEN) ? (value) : (value.slice(0, MAXLEN / 2) + "..." + value.slice(value.length - MAXLEN / 2));
        },
    };

    ko.bindingHandlers.questionsSelect = {
        /*
            The value used with this binding should be in the form:
            [
                {value: "someValue", label: "someLabel"},
                ...
            ]
         */
        init: function (element, valueAccessor, allBindingsAccessor) {
            var optionObjects = ko.utils.unwrapObservable(valueAccessor()),
                allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
                value = ko.utils.unwrapObservable(allBindings.value);

            // Add a warning if the current value isn't a legitimate question
            if (value && !_.some(optionObjects, function (option) {
                return option.value === value;
            })) {
                var option = {
                    label: gettext('Unidentified Question') + ' (' + value + ')',
                    value: value,
                };
                optionObjects = [option].concat(optionObjects);
                var $warning = $('

').text(gettext(
                    'We cannot find this question in the allowed questions for this field. ' +
                    'It is likely that you deleted or renamed the question. ' +
                    'Please choose a valid question from the dropdown.'
                ));
                $(element).after($warning);
                $(element).parent().addClass('has-error');
            }

            // Initialize select2
            var options = {
                    placeholder: gettext('Select a Question'),
                    dropdownCssClass: 'bigdrop',
                },
                data = _(optionObjects).map(function (o) {
                    return _.extend(o, {
                        id: o.value,
                        text: utils.getDisplay(o),
                    });
                }),
                templateSelection = function (o) {
                    if (!o.id) {
                        // This is the placeholder
                        return o.text;
                    }
                    return utils.getTruncatedDisplay(o);
                },
                templateResult = function (o) {
                    if (!o.value) {
                        // This is some select2 system option, like 'Searching...' text
                        return o.text;
                    }
                    return utils.getTruncatedDisplay(o, 90);
                };
            // Imperfect way to determine whether we're looking at select2 v3 or v4
            // v4 must use a <select>; v3 can but usually uses an <input>
            // Right now userreports are the only v3 usage of this binding, and they're all <input>
            if (element.nodeName.toLowerCase() === 'select') {
                options = _.extend(options, {
                    escapeMarkup: function (m) { return m; },
                    width: '100%',
                    data: [{id: '', text: ''}].concat(data),    // prepend option for placeholder
                    templateSelection: templateSelection,
                    templateResult: templateResult,
                });
            } else {
                options = _.extend(options, {
                    data: { results: data },
                    formatSelection: templateSelection,
                    formatResult: templateResult,
                });
            }
            $(element).select2(options);
            $(element).val(value).trigger('change.select2');
        },
        update: function (element, valueAccessor, allBindingsAccessor) {
            var optionObjects = ko.utils.unwrapObservable(valueAccessor()),
                allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
                value = ko.utils.unwrapObservable(allBindings.value),
                allowedValues = _.pluck(optionObjects, 'value'),
                $element = $(element),
                $container = $element.parent();

            if ($container.hasClass("has-error") && _.contains(allowedValues, value)) {
                // There was an error but it's fixed now, so remove the error and the bad option
                $container.removeClass("has-error");
                $container.find(".help-block").remove();
                _.each($element.find("option"), function (option) {
                    if (option.value && !_.contains(allowedValues, option.value)) {
                        $(option).remove();
                    }
                })
                $element.trigger('change.select2');
            }
        },
    };
}());

ko.bindingHandlers.casePropertyAutocomplete = {
    /*
     * Strip "attachment:" prefix and show icon for attachment properties.
     * Replace any spaces in free text with underscores.
     */
    init: function (element, valueAccessor) {
        $(element).on('textchange', function () {
            var $el = $(this);
            if ($el.val().match(/\s/)) {
                var pos = $el.caret('pos');
                $el.val($el.val().replace(/\s/g, '_'));
                $el.caret('pos', pos);
            }
        });
        ko.bindingHandlers.autocompleteAtwho.init(element, valueAccessor);
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        function wrappedValueAccessor() {
            return _.map(ko.unwrap(valueAccessor()), function (value) {
                if (value.indexOf("attachment:") === 0) {
                    var text = value.substring(11),
                        html = ' ' + text;
                    return {name: text, content: html};
                }
                return {name: value, content: value};
            });
        }
        ko.bindingHandlers.autocompleteAtwho.update(element, wrappedValueAccessor, allBindingsAccessor);
    },
};

// Originally from http://stackoverflow.com/a/17998880
ko.extenders.withPrevious = function (target) {
    // Define new properties for previous value and whether it's changed
    target.previous = ko.observable();

    // Subscribe to observable to update previous, before change.
    target.subscribe(function (v) {
        target.previous(v);
    }, null, 'beforeChange');

    // Return modified observable
    return target;
};

ko.bindingHandlers.numericValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var underlyingObservable = valueAccessor();
        var interceptor = ko.dependentObservable({
            read: underlyingObservable,
            write: function (value) {
                if ($.isNumeric(value)) {
                    underlyingObservable(parseFloat(value));
                } else if (value === '') {
                    underlyingObservable(null);
                }
            },
        });
        ko.bindingHandlers.value.init(element, function () { return interceptor; }, allBindingsAccessor);
    },
    update: ko.bindingHandlers.value.update,
};
<commit_msg>Fixed lint error<commit_after>(function () {
    var utils = {
        getIcon: function (question) {
            if (question.tag === 'upload') {
                return ' ';
            }
            return '';
        },
        getDisplay: function (question, MAXLEN) {
            return utils.getIcon(question) + utils.getLabel(question, MAXLEN)
                    + " (" + (question.hashtagValue || question.value) + ")";
        },
        getTruncatedDisplay: function (question, MAXLEN) {
            return utils.getIcon(question) + utils.getLabel(question, MAXLEN)
                    + " (" + utils.truncateValue(question.hashtagValue || question.value, MAXLEN) + ")";
        },
        getLabel: function (question, MAXLEN) {
            return utils.truncateLabel((question.repeat ? '- ' : '')
                    + question.label, question.tag === 'hidden' ? ' (Hidden)' : '', MAXLEN);
        },
        truncateLabel: function (label, suffix, MAXLEN) {
            suffix = suffix || "";
            var MAXLEN = MAXLEN || 40,
                maxlen = MAXLEN - suffix.length;
            return ((label.length <= maxlen) ? (label) : (label.slice(0, maxlen) + "...")) + suffix;
        },
        truncateValue: function (value, MAXLEN) {
            MAXLEN = MAXLEN || 40;
            return (value.length <= MAXLEN) ? (value) : (value.slice(0, MAXLEN / 2) + "..." + value.slice(value.length - MAXLEN / 2));
        },
    };

    ko.bindingHandlers.questionsSelect = {
        /*
            The value used with this binding should be in the form:
            [
                {value: "someValue", label: "someLabel"},
                ...
            ]
         */
        init: function (element, valueAccessor, allBindingsAccessor) {
            var optionObjects = ko.utils.unwrapObservable(valueAccessor()),
                allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
                value = ko.utils.unwrapObservable(allBindings.value);

            // Add a warning if the current value isn't a legitimate question
            if (value && !_.some(optionObjects, function (option) {
                return option.value === value;
            })) {
                var option = {
                    label: gettext('Unidentified Question') + ' (' + value + ')',
                    value: value,
                };
                optionObjects = [option].concat(optionObjects);
                var $warning = $('

').text(gettext(
                    'We cannot find this question in the allowed questions for this field. ' +
                    'It is likely that you deleted or renamed the question. ' +
                    'Please choose a valid question from the dropdown.'
                ));
                $(element).after($warning);
                $(element).parent().addClass('has-error');
            }

            // Initialize select2
            var options = {
                    placeholder: gettext('Select a Question'),
                    dropdownCssClass: 'bigdrop',
                },
                data = _(optionObjects).map(function (o) {
                    return _.extend(o, {
                        id: o.value,
                        text: utils.getDisplay(o),
                    });
                }),
                templateSelection = function (o) {
                    if (!o.id) {
                        // This is the placeholder
                        return o.text;
                    }
                    return utils.getTruncatedDisplay(o);
                },
                templateResult = function (o) {
                    if (!o.value) {
                        // This is some select2 system option, like 'Searching...' text
                        return o.text;
                    }
                    return utils.getTruncatedDisplay(o, 90);
                };
            // Imperfect way to determine whether we're looking at select2 v3 or v4
            // v4 must use a <select>; v3 can but usually uses an <input>
            // Right now userreports are the only v3 usage of this binding, and they're all <input>
            if (element.nodeName.toLowerCase() === 'select') {
                options = _.extend(options, {
                    escapeMarkup: function (m) { return m; },
                    width: '100%',
                    data: [{id: '', text: ''}].concat(data),    // prepend option for placeholder
                    templateSelection: templateSelection,
                    templateResult: templateResult,
                });
            } else {
                options = _.extend(options, {
                    data: { results: data },
                    formatSelection: templateSelection,
                    formatResult: templateResult,
                });
            }
            $(element).select2(options);
            $(element).val(value).trigger('change.select2');
        },
        update: function (element, valueAccessor, allBindingsAccessor) {
            var optionObjects = ko.utils.unwrapObservable(valueAccessor()),
                allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
                value = ko.utils.unwrapObservable(allBindings.value),
                allowedValues = _.pluck(optionObjects, 'value'),
                $element = $(element),
                $container = $element.parent();

            if ($container.hasClass("has-error") && _.contains(allowedValues, value)) {
                // There was an error but it's fixed now, so remove the error and the bad option
                $container.removeClass("has-error");
                $container.find(".help-block").remove();
                _.each($element.find("option"), function (option) {
                    if (option.value && !_.contains(allowedValues, option.value)) {
                        $(option).remove();
                    }
                });
                $element.trigger('change.select2');
            }
        },
    };
}());

ko.bindingHandlers.casePropertyAutocomplete = {
    /*
     * Strip "attachment:" prefix and show icon for attachment properties.
     * Replace any spaces in free text with underscores.
     */
    init: function (element, valueAccessor) {
        $(element).on('textchange', function () {
            var $el = $(this);
            if ($el.val().match(/\s/)) {
                var pos = $el.caret('pos');
                $el.val($el.val().replace(/\s/g, '_'));
                $el.caret('pos', pos);
            }
        });
        ko.bindingHandlers.autocompleteAtwho.init(element, valueAccessor);
    },
    update: function (element, valueAccessor, allBindingsAccessor) {
        function wrappedValueAccessor() {
            return _.map(ko.unwrap(valueAccessor()), function (value) {
                if (value.indexOf("attachment:") === 0) {
                    var text = value.substring(11),
                        html = ' ' + text;
                    return {name: text, content: html};
                }
                return {name: value, content: value};
            });
        }
        ko.bindingHandlers.autocompleteAtwho.update(element, wrappedValueAccessor, allBindingsAccessor);
    },
};

// Originally from http://stackoverflow.com/a/17998880
ko.extenders.withPrevious = function (target) {
    // Define new properties for previous value and whether it's changed
    target.previous = ko.observable();

    // Subscribe to observable to update previous, before change.
    target.subscribe(function (v) {
        target.previous(v);
    }, null, 'beforeChange');

    // Return modified observable
    return target;
};

ko.bindingHandlers.numericValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var underlyingObservable = valueAccessor();
        var interceptor = ko.dependentObservable({
            read: underlyingObservable,
            write: function (value) {
                if ($.isNumeric(value)) {
                    underlyingObservable(parseFloat(value));
                } else if (value === '') {
                    underlyingObservable(null);
                }
            },
        });
        ko.bindingHandlers.value.init(element, function () { return interceptor; }, allBindingsAccessor);
    },
    update: ko.bindingHandlers.value.update,
};
<|endoftext|>