Преглед на файлове

AMBARI-14612. Add custom time range selection for graphs (alexantonenko)

Alex Antonenko преди 9 години
родител
ревизия
18eb4e2cbd

+ 1 - 1
ambari-web/app/assets/test/tests.js

@@ -200,8 +200,8 @@ var files = [
   'test/views/common/rolling_restart_view_test',
   'test/views/common/modal_popup_test',
   'test/views/common/sort_view_test',
-  'test/views/common/custom_date_popup_test',
   'test/views/common/progress_bar_view_test',
+  'test/views/common/select_custom_date_view_test',
   'test/views/common/widget/graph_widget_view_test',
   'test/views/common/widget/number_widget_view_test',
   'test/views/common/widget/gauge_widget_view_test',

+ 12 - 1
ambari-web/app/messages.js

@@ -2772,12 +2772,23 @@ Em.I18n.translations = {
   'jobs.error.400': 'Unable to load data.',
   'jobs.table.custom.date.am':'AM',
   'jobs.table.custom.date.pm':'PM',
-  'jobs.table.custom.date.header':'Select Custom Dates',
+  'jobs.table.custom.date.header':'Select Time Range',
   'jobs.table.job.fail':'Job failed to run',
   'jobs.customDateFilter.error.required':'This field is required',
+  'jobs.customDateFilter.error.incorrect':'Date is incorrect',
   'jobs.customDateFilter.error.date.order':'End Date must be after Start Date',
   'jobs.customDateFilter.startTime':'Start Time',
   'jobs.customDateFilter.endTime':'End Time',
+  'jobs.customDateFilter.duration.15min':'15 minutes',
+  'jobs.customDateFilter.duration.30min':'30 minutes',
+  'jobs.customDateFilter.duration.1hr':'1 hour',
+  'jobs.customDateFilter.duration.2hr':'2 hours',
+  'jobs.customDateFilter.duration.4hr':'4 hours',
+  'jobs.customDateFilter.duration.12hr':'12 hours',
+  'jobs.customDateFilter.duration.24hr':'24 hours',
+  'jobs.customDateFilter.duration.1w':'1 week',
+  'jobs.customDateFilter.duration.1m':'1 month',
+  'jobs.customDateFilter.duration.1yr':'1 year',
 
   'views.main.yourViews': 'Your Views',
   'views.main.noViews': 'No views',

+ 15 - 2
ambari-web/app/mixins/common/chart/storm_linear_time.js

@@ -24,11 +24,24 @@ App.StormLinearTimeChartMixin = Em.Mixin.create({
   metricsTemplate: 'metrics/storm/nimbus/{0}[{1},{2},{3}]',
 
   getDataForAjaxRequest: function() {
-    var currentTime = Math.round(App.dateTime() / 1000);
+    var fromSeconds,
+      toSeconds,
+      index = this.get('isPopup') ? this.get('currentTimeIndex') : this.get('parentView.currentTimeRangeIndex'),
+      customStartTime = this.get('isPopup') ? this.get('customStartTime') : this.get('parentView.customStartTime'),
+      customEndTime = this.get('isPopup') ? this.get('customEndTime') : this.get('parentView.customEndTime');
+    if (index === 8 && !Em.isNone(customStartTime) && !Em.isNone(customEndTime)) {
+      // Custom start and end time is specified by user
+      fromSeconds = customStartTime / 1000;
+      toSeconds = customEndTime / 1000;
+    } else {
+      // Preset time range is specified by user
+      toSeconds = Math.round(App.dateTime() / 1000);
+      fromSeconds = toSeconds - this.get('timeUnitSeconds')
+    }
     var metricTemplate = [];
     this.get('stormChartDefinition').forEach(function(chartInfo) {
       metricTemplate.push(
-        this.get('metricsTemplate').format(chartInfo.field, currentTime - this.get('timeUnitSeconds'), currentTime, 15)
+        this.get('metricsTemplate').format(chartInfo.field, fromSeconds, toSeconds, 15)
       );
     }, this);
     return {

+ 42 - 3
ambari-web/app/mixins/common/widgets/time_range_mixin.js

@@ -19,6 +19,7 @@
 var App = require('app');
 
 require('views/common/time_range_list');
+var timeRangePopup = require('views/common/custom_date_popup');
 
 App.TimeRangeMixin = Em.Mixin.create({
 
@@ -36,7 +37,8 @@ App.TimeRangeMixin = Em.Mixin.create({
     {index: 4, name: Em.I18n.t('graphs.timeRange.day'), value: '24'},
     {index: 5, name: Em.I18n.t('graphs.timeRange.week'), value: '168'},
     {index: 6, name: Em.I18n.t('graphs.timeRange.month'), value: '720'},
-    {index: 7, name: Em.I18n.t('graphs.timeRange.year'), value: '8760'}
+    {index: 7, name: Em.I18n.t('graphs.timeRange.year'), value: '8760'},
+    {index: 8, name: Em.I18n.t('common.custom'), value: '0'}
   ],
 
   currentTimeRangeIndex: 0,
@@ -45,12 +47,49 @@ App.TimeRangeMixin = Em.Mixin.create({
     return this.get('timeRangeOptions').objectAt(this.get('currentTimeRangeIndex'));
   }.property('currentTimeRangeIndex'),
 
+  customStartTime: null,
+
+  customEndTime: null,
+
   /**
    * onclick handler for a time range option
    * @param {object} event
    */
-  setTimeRange: function (event) {
-    this.set('currentTimeRangeIndex', event.context.index);
+  setTimeRange: function (event, callback, context) {
+    var prevIndex = this.get('currentTimeRangeIndex'),
+      prevCustomTimeRange = {
+        start: this.get('customStartTime'),
+        end: this.get('customEndTime')
+      },
+      index = event.context.index,
+      primary = function () {
+        if (callback) {
+          callback();
+        }
+      },
+      secondary = function () {
+        this.setProperties({
+          currentTimeRangeIndex: prevIndex,
+          customStartTime: prevCustomTimeRange.start,
+          customEndTime: prevCustomTimeRange.end
+        });
+      };
+
+    // Preset time range is active
+    if (prevIndex !== 8) {
+      this.setProperties({
+        customStartTime: null,
+        customEndTime: null
+      });
+    }
+
+    // Custom start and end time is specified by user
+    if (index === 8) {
+      context = context || this;
+      timeRangePopup.showCustomDatePopup(context, primary.bind(this), secondary.bind(this));
+    }
+
+    this.set('currentTimeRangeIndex', index);
   },
 
   timeRangeListView: App.TimeRangeListView.extend()

+ 11 - 0
ambari-web/app/styles/application.less

@@ -2189,6 +2189,8 @@ a:focus {
     min-height: 420px !important;
     overflow: hidden;
     .corner-icon {
+      position: absolute;
+      right: 15px;
       text-decoration: none;
       .icon-save {
         color: #555;
@@ -2219,6 +2221,15 @@ a:focus {
 
 /*start chart/style graphs*/
 .chart-wrapper {
+  .actions-container {
+    text-align: center;
+    .graph-details-time-range {
+      display: inline-block;
+      .dropdown-menu {
+        text-align: left;
+      }
+    }
+  }
   .timezone {
     font-size: @smaller-font-size;
   }

+ 3 - 3
ambari-web/app/templates/common/chart/linear_time.hbs

@@ -17,9 +17,9 @@
 }}
 <div class="chart-wrapper">
   <div {{bindAttr class="view.isReady:hide:show :screensaver :no-borders :chart-container"}}></div>
-  <div {{bindAttr class="view.isReady::hidden :time-label"}}>
-    {{view.parentView.currentTimeState.name}}
-    <a {{bindAttr class="view.isExportButtonHidden:hidden :corner-icon :pull-right"}}
+  <div {{bindAttr class="view.isReady::hidden :actions-container"}}>
+    {{view view.timeRangeListView}}
+    <a {{bindAttr class="view.isExportButtonHidden:hidden :corner-icon"}}
         href="#" {{action toggleFormatsList target="view"}}>
       {{t common.export}} <i class="icon-save"></i>
     </a>

+ 14 - 13
ambari-web/app/templates/common/custom_date_popup.hbs

@@ -17,23 +17,24 @@
 }}
 
 <div class="jobs-custom-dates">
-  <div {{bindAttr class=":control-group view.isValid.isStartDateError:error"}}>
+  <div {{bindAttr class=":control-group view.errors.isStartDateError:error"}}>
     <label>{{t jobs.customDateFilter.startTime}}</label>
-    {{view Ember.TextField valueBinding="controller.customDateFormFields.startDate" class="input-small datepicker"}}
-    {{view Ember.Select contentBinding="view.hourOptions" selectionBinding="controller.customDateFormFields.hoursForStart" class="input-mini"}}
-    {{view Ember.Select contentBinding="view.minuteOptions" selectionBinding="controller.customDateFormFields.minutesForStart" class="input-mini"}}
-    {{view Ember.Select contentBinding="view.middayPeriodOptions" selectionBinding="controller.customDateFormFields.middayPeriodForStart" class="input-mini"}}
-    <span class="help-inline">{{view.validationErrors.startDate}}</span>
+    {{view Ember.TextField valueBinding="view.customDateFormFields.startDate" class="input-small datepicker no-autofocus"}}
+    {{view Ember.Select contentBinding="view.hourOptions" selectionBinding="view.customDateFormFields.hoursForStart" class="input-mini"}}
+    {{view Ember.Select contentBinding="view.minuteOptions" selectionBinding="view.customDateFormFields.minutesForStart" class="input-mini"}}
+    {{view Ember.Select contentBinding="view.middayPeriodOptions" selectionBinding="view.customDateFormFields.middayPeriodForStart" class="input-mini"}}
+    <span class="help-inline">{{view.errorMessages.startDate}}</span>
   </div>
   <div>
-
+    <label>{{t common.duration}}</label>
+    {{view Ember.Select contentBinding="view.durationOptions" optionValuePath="content.value" optionLabelPath="content.label" selectionBinding="view.customDateFormFields.duration" class="input-medium"}}
   </div>
-  <div {{bindAttr class=":control-group view.isValid.isEndDateError:error"}}>
+  <div {{bindAttr class=":control-group view.errors.isEndDateError:error view.isCustomEndDate::hidden"}}>
     <label>{{t jobs.customDateFilter.endTime}}</label>
-    {{view Ember.TextField valueBinding="controller.customDateFormFields.endDate" class="input-small datepicker"}}
-    {{view Ember.Select contentBinding="view.hourOptions" selectionBinding="controller.customDateFormFields.hoursForEnd" class="input-mini"}}
-    {{view Ember.Select contentBinding="view.minuteOptions" selectionBinding="controller.customDateFormFields.minutesForEnd" class="input-mini"}}
-    {{view Ember.Select contentBinding="view.middayPeriodOptions" selectionBinding="controller.customDateFormFields.middayPeriodForEnd" class="input-mini"}}
-    <span class="help-inline">{{view.validationErrors.endDate}}</span>
+    {{view Ember.TextField valueBinding="view.customDateFormFields.endDate" class="input-small datepicker no-autofocus"}}
+    {{view Ember.Select contentBinding="view.hourOptions" selectionBinding="view.customDateFormFields.hoursForEnd" class="input-mini"}}
+    {{view Ember.Select contentBinding="view.minuteOptions" selectionBinding="view.customDateFormFields.minutesForEnd" class="input-mini"}}
+    {{view Ember.Select contentBinding="view.middayPeriodOptions" selectionBinding="view.customDateFormFields.middayPeriodForEnd" class="input-mini"}}
+    <span class="help-inline">{{view.errorMessages.endDate}}</span>
   </div>
 </div>

+ 11 - 0
ambari-web/app/utils/helper.js

@@ -770,6 +770,17 @@ App.dateTimeWithTimeZone = function (x) {
   return x || new Date().getTime();
 };
 
+App.getTimeStampFromLocalTime = function (time) {
+  var timezone = App.router.get('userSettingsController.userSettings.timezone'),
+    offsetString = '',
+    date = moment(time).format('YYYY-MM-DD HH:mm:ss');
+  if (timezone) {
+    var offset = timezone.utcOffset;
+    offsetString = moment().utcOffset(offset).format('Z');
+  }
+  return moment(date + offsetString).toDate().getTime();
+};
+
 /**
  * Helper function for bound property helper registration
  * @memberof App

+ 86 - 25
ambari-web/app/views/common/chart/linear_time.js

@@ -341,20 +341,29 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
   },
 
   getDataForAjaxRequest: function () {
-    var toSeconds = Math.round(App.dateTime() / 1000);
-    var hostName = (this.get('content')) ? this.get('content.hostName') : "";
-
-    var HDFSService = App.HDFSService.find().objectAt(0);
-    var nameNodeName = "";
-    var YARNService = App.YARNService.find().objectAt(0);
-    var resourceManager = YARNService ? YARNService.get('resourceManager.hostName') : "";
-    var timeUnit = this.get('timeUnitSeconds');
+    var fromSeconds,
+      toSeconds,
+      hostName = (this.get('content')) ? this.get('content.hostName') : "",
+      HDFSService = App.HDFSService.find().objectAt(0),
+      nameNodeName = "",
+      YARNService = App.YARNService.find().objectAt(0),
+      resourceManager = YARNService ? YARNService.get('resourceManager.hostName') : "";
     if (HDFSService) {
       nameNodeName = (HDFSService.get('activeNameNode')) ? HDFSService.get('activeNameNode.hostName') : HDFSService.get('nameNode.hostName');
     }
+    if (this.get('currentTimeIndex') === 8 && !Em.isNone(this.get('customStartTime')) && !Em.isNone(this.get('customEndTime'))) {
+      // Custom start and end time is specified by user
+      toSeconds = this.get('customEndTime') / 1000;
+      fromSeconds = this.get('customStartTime') / 1000;
+    } else {
+      // Preset time range is specified by user
+      var timeUnit = this.get('timeUnitSeconds');
+      toSeconds = Math.round(App.dateTime() / 1000);
+      fromSeconds = toSeconds - timeUnit;
+    }
     return {
       toSeconds: toSeconds,
-      fromSeconds: toSeconds - timeUnit,
+      fromSeconds: fromSeconds,
       stepSeconds: 15,
       hostName: hostName,
       nameNodeName: nameNodeName,
@@ -901,7 +910,7 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
     var self = this;
 
     App.ModalPopup.show({
-      bodyClass: Em.View.extend(App.ExportMetricsMixin, {
+      bodyClass: Em.View.extend(App.ExportMetricsMixin, App.TimeRangeMixin, {
 
         containerId: null,
         containerClass: null,
@@ -916,9 +925,16 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
         titleId: null,
         titleClass: null,
 
+        timeRangeClassName: 'graph-details-time-range',
+
         isReady: Em.computed.alias('parentView.graph.isPopupReady'),
 
         didInsertElement: function () {
+          this.setTimeRange({
+            context: {
+              index: 0
+            }
+          });
           var popupBody = this;
           App.tooltip(this.$('.corner-icon > .icon-save'), {
             title: Em.I18n.t('common.export')
@@ -956,11 +972,14 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
         }.property(),
 
         rightArrowVisible: function () {
-          return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 0));
+          // Time range is neither custom nor the least possible preset
+          return (this.get('isReady') && this.get('parentView.currentTimeIndex') != 0 &&
+            this.get('parentView.currentTimeIndex') != 8);
         }.property('isReady', 'parentView.currentTimeIndex'),
 
         leftArrowVisible: function () {
-          return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 7));
+          // Time range is neither custom nor the largest possible preset
+          return (this.get('isReady') && this.get('parentView.currentTimeIndex') < 7);
         }.property('isReady', 'parentView.currentTimeIndex'),
 
         exportGraphData: function (event) {
@@ -970,6 +989,17 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
           targetView.exportGraphData({
             context: event.context
           });
+        },
+
+        setTimeRange: function (event) {
+          var index = event.context.index,
+            callback = this.get('parentView').reloadGraphByTime.bind(this.get('parentView'), index);
+          this._super(event, callback, self);
+
+          // Preset time range is specified by user
+          if (index !== 8) {
+            callback();
+          }
         }
       }),
       header: this.get('title'),
@@ -997,7 +1027,7 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
        */
       switchTimeBack: function (event) {
         var index = this.get('currentTimeIndex');
-        // 7 - number of last time state
+        // 7 - number of last preset time state
         if (index < 7) {
           this.reloadGraphByTime(++index);
         }
@@ -1017,6 +1047,7 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
        * @param index
        */
       reloadGraphByTime: function (index) {
+        this.set('childViews.firstObject.currentTimeRangeIndex', index);
         this.set('currentTimeIndex', index);
         self.set('currentTimeIndex', index);
       },
@@ -1033,8 +1064,10 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
     });
   },
   reloadGraphByTime: function () {
-    this.loadData();
-  }.observes('timeUnitSeconds'),
+    Em.run.once(this, function () {
+      this.loadData();
+    });
+  }.observes('timeUnitSeconds', 'customStartTime', 'customStartTime'),
 
   timeStates: [
     {name: Em.I18n.t('graphs.timeRange.hour'), seconds: 3600},
@@ -1044,17 +1077,37 @@ App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
     {name: Em.I18n.t('graphs.timeRange.day'), seconds: 86400},
     {name: Em.I18n.t('graphs.timeRange.week'), seconds: 604800},
     {name: Em.I18n.t('graphs.timeRange.month'), seconds: 2592000},
-    {name: Em.I18n.t('graphs.timeRange.year'), seconds: 31104000}
+    {name: Em.I18n.t('graphs.timeRange.year'), seconds: 31104000},
+    {name: Em.I18n.t('common.custom'), seconds: 0}
   ],
   // should be set by time range control dropdown list when create current graph
   currentTimeIndex: 0,
+  customStartTime: null,
+  customEndTime: null,
   setCurrentTimeIndexFromParent: function () {
-    var index = !Em.isNone(this.get('parentView.currentTimeRangeIndex')) ? this.get('parentView.currentTimeRangeIndex') : this.get('parentView.parentView.currentTimeRangeIndex');
-    this.set('currentTimeIndex', index);
-  }.observes('parentView.parentView.currentTimeRangeIndex', 'parentView.currentTimeRangeIndex'),
-  timeUnitSeconds: function () {
-    return this.get('timeStates').objectAt(this.get('currentTimeIndex')).seconds;
-  }.property('currentTimeIndex')
+    // 8 index corresponds to custom start and end time selection
+    var targetView = !Em.isNone(this.get('parentView.currentTimeRangeIndex')) ? this.get('parentView') : this.get('parentView.parentView'),
+      index = targetView.get('currentTimeRangeIndex'),
+      customStartTime = (index === 8) ? targetView.get('customStartTime') : null,
+      customEndTime = (index === 8) ? targetView.get('customEndTime'): null;
+    this.setProperties({
+      currentTimeIndex: index,
+      customStartTime: customStartTime,
+      customEndTime: customEndTime
+    });
+  }.observes('parentView.parentView.currentTimeRangeIndex', 'parentView.currentTimeRangeIndex', 'parentView.parentView.customStartTime', 'parentView.customStartTime', 'parentView.parentView.customEndTime', 'parentView.customEndTime'),
+  timeUnitSeconds: 3600,
+  timeUnitSecondsSetter: function () {
+      var index = this.get('currentTimeIndex');
+    if (index !== 8) {
+      // Preset time range is specified by user
+      var seconds = this.get('timeStates').objectAt(this.get('currentTimeIndex')).seconds;
+      this.set('timeUnitSeconds', seconds);
+    } else {
+      // Custom start and end time is specified by user
+      this.propertyDidChange('timeUnitSeconds');
+    }
+  }.observes('currentTimeIndex')
 
 });
 
@@ -1397,10 +1450,18 @@ App.ChartLinearTimeView.LoadAggregator = Em.Object.create({
    * @returns {number[]}
    */
   formatRequestData: function (request) {
-    var toSeconds = Math.round(App.dateTime() / 1000);
-    var timeUnit = request.context.get('timeUnitSeconds');
+    var fromSeconds, toSeconds;
+    if (request.context.get('currentTimeIndex') === 8 && !Em.isNone(request.context.get('customStartTime')) && !Em.isNone(request.context.get('customEndTime'))) {
+      // Custom start and end time is specified by user
+      toSeconds = request.context.get('customEndTime') / 1000;
+      fromSeconds = request.context.get('customStartTime') / 1000;
+    } else {
+      var timeUnit = request.context.get('timeUnitSeconds');
+      toSeconds = Math.round(App.dateTime() / 1000);
+      fromSeconds = toSeconds - timeUnit;
+    }
     var fields = request.fields.uniq().map(function (field) {
-      return field + "[" + (toSeconds - timeUnit) + "," + toSeconds + "," + 15 + "]";
+      return field + "[" + (fromSeconds) + "," + toSeconds + "," + 15 + "]";
     });
 
     return fields.join(",");

+ 24 - 92
ambari-web/app/views/common/custom_date_popup.js

@@ -20,111 +20,43 @@ var App = require('app');
 
 module.exports = Em.Object.create({
 
-  // Fields values from Select Custom Dates form
-  customDateFormFields: Ember.Object.create({
-    startDate: null,
-    hoursForStart: null,
-    minutesForStart: null,
-    middayPeriodForStart: null,
-    endDate: null,
-    hoursForEnd: null,
-    minutesForEnd: null,
-    middayPeriodForEnd: null
-  }),
+  startTime: null,
 
-  errors: Ember.Object.create({
-    isStartDateError: false,
-    isEndDateError: false
-  }),
+  endTime: null,
 
-  errorMessages: Ember.Object.create({
-    startDate: '',
-    endDate: ''
-  }),
-
-  showCustomDatePopup: function (context) {
+  showCustomDatePopup: function (context, primary, secondary) {
     var self = this;
 
     return App.ModalPopup.show({
       header: Em.I18n.t('jobs.table.custom.date.header'),
       onPrimary: function () {
-        self.validate();
-        if(self.get('errors.isStartDateError') || self.get('errors.isEndDateError')) {
-          return false;
-        }
-
-        var windowStart = self.createCustomStartDate();
-        var windowEnd = self.createCustomEndDate();
-        context.set('actualValues', {
-          endTime: windowEnd.getTime(),
-          startTime: windowStart.getTime()
+        context.setProperties({
+          customEndTime: self.endTime,
+          customStartTime: self.startTime
         });
-        this.hide();
+        if (primary) {
+          primary();
+        }
+        this._super();
       },
       onSecondary: function () {
-        context.cancel();
-        this.hide();
+        //context.cancel(); to check!
+        if (secondary) {
+          secondary();
+        }
+        this._super();
+      },
+      onClose: function () {
+        if (secondary) {
+          secondary();
+        }
+        this._super();
       },
+      disablePrimary: false,
       bodyClass: App.JobsCustomDatesSelectView.extend({
-        controller: self,
-        validationErrors: self.get('errorMessages'),
-        isValid: self.get('errors')
+        controller: self
       })
     });
-  },
-
-  createCustomStartDate : function () {
-    var startDate = this.get('customDateFormFields.startDate');
-    var hoursForStart = this.get('customDateFormFields.hoursForStart');
-    var minutesForStart = this.get('customDateFormFields.minutesForStart');
-    var middayPeriodForStart = this.get('customDateFormFields.middayPeriodForStart');
-    if (startDate && hoursForStart && minutesForStart && middayPeriodForStart) {
-      return new Date(startDate + ' ' + hoursForStart + ':' + minutesForStart + ' ' + middayPeriodForStart);
-    }
-    return null;
-  },
-
-  createCustomEndDate : function () {
-    var endDate = this.get('customDateFormFields.endDate');
-    var hoursForEnd = this.get('customDateFormFields.hoursForEnd');
-    var minutesForEnd = this.get('customDateFormFields.minutesForEnd');
-    var middayPeriodForEnd = this.get('customDateFormFields.middayPeriodForEnd');
-    if (endDate && hoursForEnd && minutesForEnd && middayPeriodForEnd) {
-      return new Date(endDate + ' ' + hoursForEnd + ':' + minutesForEnd + ' ' + middayPeriodForEnd);
-    }
-    return null;
-  },
-
-  clearErrors: function () {
-    var errorMessages = this.get('errorMessages');
-    Em.keys(errorMessages).forEach(function (key) {
-      errorMessages.set(key, '');
-    }, this);
-    var errors = this.get('errors');
-    Em.keys(errors).forEach(function (key) {
-      errors.set(key, false);
-    }, this);
-  },
-
-  // Validation for every field in customDateFormFields
-  validate: function () {
-    var formFields = this.get('customDateFormFields');
-    var errors = this.get('errors');
-    var errorMessages = this.get('errorMessages');
-    this.clearErrors();
-    // Check if feild is empty
-    Em.keys(errorMessages).forEach(function (key) {
-      if (!formFields.get(key)) {
-        errors.set('is' + key.capitalize() + 'Error', true);
-        errorMessages.set(key, Em.I18n.t('jobs.customDateFilter.error.required'));
-      }
-    }, this);
-    // Check that endDate is after startDate
-    var startDate = this.createCustomStartDate();
-    var endDate = this.createCustomEndDate();
-    if (startDate && endDate && (startDate > endDate)) {
-      errors.set('isEndDateError', true);
-      errorMessages.set('endDate', Em.I18n.t('jobs.customDateFilter.error.date.order'));
-    }
   }
+
 });

+ 1 - 1
ambari-web/app/views/common/modal_popup.js

@@ -72,7 +72,7 @@ App.ModalPopup = Ember.View.extend({
       }
     }
     this.fitZIndex();
-    var firstInputElement = this.$('#modal').find(':input').not(':disabled').first();
+    var firstInputElement = this.$('#modal').find(':input').not(':disabled, .no-autofocus').first();
     this.focusElement(firstInputElement);
   },
 

+ 157 - 1
ambari-web/app/views/common/select_custom_date_view.js

@@ -30,11 +30,167 @@ App.JobsCustomDatesSelectView = Em.View.extend({
 
   minuteOptions: ['00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'],
 
+  durationOptions: [
+    {
+      value: 900000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.15min')
+    },
+    {
+      value: 1800000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.30min')
+    },
+    {
+      value: 3600000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.1hr')
+    },
+    {
+      value: 7200000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.2hr')
+    },
+    {
+      value: 14400000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.4hr')
+    },
+    {
+      value: 43200000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.12hr')
+    },
+    {
+      value: 86400000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.24hr')
+    },
+    {
+      value: 604800000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.1w')
+    },
+    {
+      value: 2592000000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.1m')
+    },
+    {
+      value: 31536000000,
+      label: Em.I18n.t('jobs.customDateFilter.duration.1yr')
+    },
+    {
+      value: 0,
+      label: Em.I18n.t('common.custom')
+    }
+  ],
+
+  customDateFormFields: Ember.Object.create({
+    startDate: null,
+    hoursForStart: null,
+    minutesForStart: null,
+    middayPeriodForStart: null,
+    duration: null,
+    endDate: null,
+    hoursForEnd: null,
+    minutesForEnd: null,
+    middayPeriodForEnd: null
+  }),
+
+  errors: Ember.Object.create({
+    isStartDateError: false,
+    isEndDateError: false
+  }),
+
+  errorMessages: Ember.Object.create({
+    startDate: '',
+    endDate: ''
+  }),
+
+  isCustomEndDate: Em.computed.equal('customDateFormFields.duration.value', 0),
+
   didInsertElement: function () {
+    this.validate();
     $('.datepicker').datepicker({
       format: 'mm/dd/yyyy'
     }).on('changeDate', function() {
       $(this).datepicker('hide');
     });
-  }
+  },
+
+  createCustomStartDate : function () {
+    var startDate = this.get('customDateFormFields.startDate');
+    var hoursForStart = this.get('customDateFormFields.hoursForStart');
+    var minutesForStart = this.get('customDateFormFields.minutesForStart');
+    var middayPeriodForStart = this.get('customDateFormFields.middayPeriodForStart');
+    if (startDate && hoursForStart && minutesForStart && middayPeriodForStart) {
+      return new Date(startDate + ' ' + hoursForStart + ':' + minutesForStart + ' ' + middayPeriodForStart);
+    }
+    return null;
+  },
+
+  createCustomEndDate : function (startDate) {
+    var duration = this.get('customDateFormFields.duration.value'),
+      date;
+    if (duration === 0) {
+      var endDate = this.get('customDateFormFields.endDate');
+      var hoursForEnd = this.get('customDateFormFields.hoursForEnd');
+      var minutesForEnd = this.get('customDateFormFields.minutesForEnd');
+      var middayPeriodForEnd = this.get('customDateFormFields.middayPeriodForEnd');
+      if (endDate && hoursForEnd && minutesForEnd && middayPeriodForEnd) {
+        date = endDate + ' ' + hoursForEnd + ':' + minutesForEnd + ' ' + middayPeriodForEnd;
+      }
+    } else if (!Em.isNone(startDate)) {
+      date = startDate.getTime() + duration;
+    }
+    if (!Em.isNone(date)) {
+      return new Date(date);
+    }
+    return null;
+  },
+
+  setErrorMessage: function (key, message) {
+    var errors = this.get('errors'),
+      errorMessages = this.get('errorMessages'),
+      isError = !Em.isNone(message);
+    message = isError ? message: '';
+    errors.set('is' + key.capitalize() + 'Error', isError);
+    errorMessages.set(key, message);
+  },
+
+  validate: function () {
+    var hasErrors = false,
+      formFields = this.get('customDateFormFields'),
+      errors = this.get('errors'),
+      errorMessages = this.get('errorMessages');
+
+    // Check if fields are empty or invalid
+    Em.keys(errorMessages).forEach(function (key) {
+      var value = formFields.get(key);
+      if (key !== 'endDate' || this.get('isCustomEndDate')) {
+        if (!formFields.get(key)) {
+          hasErrors = true;
+          this.setErrorMessage(key);
+        } else if (isNaN(new Date(value).valueOf())) {
+          this.setErrorMessage(key, Em.I18n.t('jobs.customDateFilter.error.incorrect'));
+          hasErrors = true;
+        } else {
+          this.setErrorMessage(key);
+        }
+      }
+    }, this);
+
+    // Check that endDate is after startDate
+    if (!hasErrors) {
+      var startDate = this.createCustomStartDate(),
+        endDate = this.createCustomEndDate(startDate);
+      if (startDate && endDate && (startDate > endDate)) {
+        hasErrors = true;
+        this.setErrorMessage('endDate', Em.I18n.t('jobs.customDateFilter.error.date.order'));
+      }
+    }
+
+    this.set('parentView.disablePrimary', hasErrors);
+
+    // Get customized time range if there are no errors
+    if (!hasErrors) {
+      this.get('controller').setProperties({
+        startTime: App.getTimeStampFromLocalTime(startDate),
+        endTime: App.getTimeStampFromLocalTime(endDate)
+      });
+    }
+
+  }.observes('customDateFormFields.startDate', 'customDateFormFields.hoursForStart', 'customDateFormFields.minutesForStart', 'customDateFormFields.middayPeriodForStart', 'customDateFormFields.endDate', 'customDateFormFields.hoursForEnd', 'customDateFormFields.minutesForEnd', 'customDateFormFields.middayPeriodForEnd', 'customDateFormFields.duration.value')
 });

+ 30 - 5
ambari-web/app/views/common/widget/graph_widget_view.js

@@ -57,6 +57,12 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
       //1h - default time range
       timeRange = 1;
     }
+
+    // Custom start and end time is specified by user
+    if (this.get('exportTargetView.currentTimeIndex') === 8) {
+      return 0;
+    }
+
     return this.get('customTimeRange') || timeRange * this.get('TIME_FACTOR');
   }.property('content.properties.time_range', 'customTimeRange'),
 
@@ -200,10 +206,23 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
    * @returns {Array} result
    */
   addTimeProperties: function (metricPaths) {
-    var toSeconds = Math.round(App.dateTime() / 1000);
-    var fromSeconds = toSeconds - this.get('timeRange');
-    var step = this.get('timeStep');
-    var result = [];
+    var toSeconds,
+      fromSeconds,
+      step = this.get('timeStep'),
+      timeRange = this.get('timeRange'),
+      result = [],
+      targetView = this.get('exportTargetView.isPopup') ? this.get('exportTargetView') : this.get('parentView'),
+      customStartTime = targetView.get('customStartTime'),
+      customEndTime = targetView.get('customEndTime');
+    if (timeRange === 0 && !Em.isNone(customStartTime) && !Em.isNone(customEndTime)) {
+      // Custom start and end time is specified by user
+      toSeconds = customEndTime / 1000;
+      fromSeconds = customStartTime / 1000;
+    } else {
+      // Preset time range is specified by user
+      toSeconds = Math.round(App.dateTime() / 1000);
+      fromSeconds = toSeconds - timeRange;
+    }
 
     metricPaths.forEach(function (metricPath) {
       result.push(metricPath + '[' + fromSeconds + ',' + toSeconds + ',' + step + ']');
@@ -237,7 +256,13 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
      */
     setTimeRange: function () {
       if (this.get('isPopup')) {
-        this.set('parentView.customTimeRange', this.get('timeUnitSeconds'));
+        if (this.get('currentTimeIndex') === 8) {
+          // Custom start and end time is specified by user
+          this.get('parentView').propertyDidChange('customTimeRange');
+        } else {
+          // Preset time range is specified by user
+          this.set('parentView.customTimeRange', this.get('timeUnitSeconds'));
+        }
       } else {
         this.set('parentView.customTimeRange', null);
       }

+ 11 - 4
ambari-web/app/views/main/service/info/summary.js

@@ -487,11 +487,18 @@ App.MainServiceInfoSummaryView = Em.View.extend(App.UserPref, App.TimeRangeMixin
    * @param {object} event
    */
   setTimeRange: function (event) {
-    this._super(event);
+    var graphs = this.get('controller.widgets').filterProperty('widgetType', 'GRAPH'),
+      callback = function () {
+        graphs.forEach(function (widget) {
+          widget.set('properties.time_range', event.context.value);
+        });
+      };
+    this._super(event, callback);
 
-    this.get('controller.widgets').filterProperty('widgetType', 'GRAPH').forEach(function (widget) {
-      widget.set('properties.time_range', event.context.value);
-    }, this);
+    // Preset time range is specified by user
+    if (event.context.value !== '0') {
+      callback();
+    }
   },
 
   loadServiceSummary: function () {

+ 89 - 5
ambari-web/test/mixins/common/widgets/time_range_mixin_test.js

@@ -20,6 +20,8 @@ var App = require('app');
 
 require('mixins/common/widgets/time_range_mixin');
 
+var timeRangePopup = require('views/common/custom_date_popup');
+
 describe('App.TimeRangeMixin', function () {
 
   var obj;
@@ -44,13 +46,95 @@ describe('App.TimeRangeMixin', function () {
 
   describe('#setTimeRange', function () {
 
-    it('should set time range', function () {
-      obj.setTimeRange({
-        context: {
-          index: 1
+    var indexCases = [
+        {
+          index: 1,
+          showCustomDatePopupCallCount: 0,
+          title: 'preset time range',
+          popupTestTitle: 'popup should not be displayed'
+        },
+        {
+          index: 8,
+          showCustomDatePopupCallCount: 1,
+          title: 'custom time range',
+          popupTestTitle: 'popup should be displayed'
+        }
+      ],
+      rangeCases = [
+        {
+          currentTimeRangeIndex: 1,
+          customStartTime: null,
+          customEndTime: null,
+          title: 'previous time range is preset',
+          testTitle: 'should reset time range boundaries'
+        },
+        {
+          currentTimeRangeIndex: 8,
+          customStartTime: 1,
+          customEndTime: 1,
+          title: 'previous time range is custom',
+          testTitle: 'should not reset time range boundaries'
         }
+      ];
+
+    beforeEach(function () {
+      sinon.stub(timeRangePopup, 'showCustomDatePopup', Em.K);
+    });
+
+    afterEach(function () {
+      timeRangePopup.showCustomDatePopup.restore();
+    });
+
+    indexCases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          obj.setTimeRange({
+            context: {
+              index: item.index
+            }
+          });
+        });
+
+        it('should set time range', function () {
+          expect(obj.get('currentTimeRangeIndex')).to.equal(item.index);
+        });
+
+        it(item.popupTestTitle, function () {
+          expect(timeRangePopup.showCustomDatePopup.callCount).to.equal(item.showCustomDatePopupCallCount);
+        });
+
+      });
+
+    });
+
+    rangeCases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          obj.setProperties({
+            currentTimeRangeIndex: item.currentTimeRangeIndex,
+            customStartTime: 1,
+            customEndTime: 1
+          });
+          obj.setTimeRange({
+            context: {
+              index: 0
+            }
+          });
+        });
+
+        it(item.testTitle, function () {
+          expect(obj.getProperties(['customStartTime', 'customEndTime'])).to.eql({
+            customStartTime: item.customStartTime,
+            customEndTime: item.customEndTime
+          });
+        });
+
       });
-      expect(obj.get('currentTimeRangeIndex')).to.equal(1);
+
     });
 
   });

+ 82 - 14
ambari-web/test/views/common/chart/linear_time_test.js

@@ -153,6 +153,32 @@ describe('App.ChartLinearTimeView', function () {
       yarnService: [],
       hdfsService: []
     };
+    var rangeCases = [
+      {
+        currentTimeIndex: 0,
+        customStartTime: 100000,
+        customEndTime: 200000,
+        fromSeconds: -3599,
+        toSeconds: 1,
+        title: 'preset time range'
+      },
+      {
+        currentTimeIndex: 8,
+        customStartTime: 100000,
+        customEndTime: 200000,
+        fromSeconds: 100,
+        toSeconds: 200,
+        title: 'custom time range'
+      },
+      {
+        currentTimeIndex: 8,
+        customStartTime: null,
+        customEndTime: null,
+        fromSeconds: -3599,
+        toSeconds: 1,
+        title: 'custom time range, no boundaries set'
+      }
+    ];
     beforeEach(function(){
       sinon.stub(App.HDFSService, 'find', function(){return services.hdfsService});
       sinon.stub(App.YARNService, 'find', function(){return services.yarnService});
@@ -226,6 +252,20 @@ describe('App.ChartLinearTimeView', function () {
       });
       services.yarnService = [];
     });
+    rangeCases.forEach(function (item) {
+      it(item.title, function () {
+        chartLinearTimeView.setProperties({
+          currentTimeIndex: item.currentTimeIndex,
+          customStartTime: item.customStartTime,
+          customEndTime: item.customEndTime
+        });
+        var requestData = Em.Object.create(chartLinearTimeView.getDataForAjaxRequest());
+        expect(requestData.getProperties(['fromSeconds', 'toSeconds'])).to.eql({
+          fromSeconds: item.fromSeconds,
+          toSeconds: item.toSeconds
+        });
+      });
+    });
   });
 
   describe('#setCurrentTimeIndexFromParent', function () {
@@ -255,12 +295,12 @@ describe('App.ChartLinearTimeView', function () {
     beforeEach(function () {
       view = App.ChartLinearTimeView.create({
         controller: {},
-        parentView: {
+        parentView: Em.Object.create({
           currentTimeRangeIndex: 1,
-          parentView: {
+          parentView: Em.Object.create({
             currentTimeRangeIndex: 2
-          }
-        }
+          })
+        })
       });
     });
 
@@ -458,22 +498,50 @@ describe('App.ChartLinearTimeView.LoadAggregator', function () {
   });
 
   describe("#formatRequestData()", function () {
+    var cases = [
+      {
+        currentTimeIndex: 0,
+        customStartTime: 100000,
+        customEndTime: 200000,
+        result: 'f3[400,4000,15],f4[400,4000,15]',
+        title: 'preset time range'
+      },
+      {
+        currentTimeIndex: 8,
+        customStartTime: 100000,
+        customEndTime: 200000,
+        result: 'f3[100,200,15],f4[100,200,15]',
+        title: 'custom time range'
+      },
+      {
+        currentTimeIndex: 8,
+        customStartTime: null,
+        customEndTime: null,
+        result: 'f3[400,4000,15],f4[400,4000,15]',
+        title: 'custom time range, no boundaries set'
+      }
+    ];
     beforeEach(function () {
       sinon.stub(App, 'dateTime').returns(4000000);
-
     });
     afterEach(function () {
       App.dateTime.restore();
-
     });
-    it("data is formed", function () {
-      var context = Em.Object.create({timeUnitSeconds: 3600});
-      var request = {
-        name: 'r1',
-        context: context,
-        fields: ['f3', 'f4']
-      };
-      expect(aggregator.formatRequestData(request)).to.equal('f3[400,4000,15],f4[400,4000,15]');
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        var context = Em.Object.create({
+          timeUnitSeconds: 3600,
+          currentTimeIndex: item.currentTimeIndex,
+          customStartTime: item.customStartTime,
+          customEndTime: item.customEndTime
+        });
+        var request = {
+          name: 'r1',
+          context: context,
+          fields: ['f3', 'f4']
+        };
+        expect(aggregator.formatRequestData(request)).to.equal(item.result);
+      });
     });
   });
 

+ 0 - 92
ambari-web/test/views/common/custom_date_popup_test.js

@@ -1,92 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-var App = require('app');
-
-describe('CustomDatePopup', function() {
-  var customDatePopup = require('views/common/custom_date_popup');
-
-  describe('#showCustomDatePopup', function() {
-    var context = Em.Object.create({
-      cancel: sinon.spy(),
-      actualValues: Em.Object.create()
-    });
-    var popup = customDatePopup.showCustomDatePopup(context);
-    
-    it('`onSecondary` should call `cancel` method in passed context', function() {
-      popup.onSecondary();
-      expect(context.cancel.calledOnce).to.ok;
-    });
-
-    describe('empty values passed for end and start dates, validation should fail with appropriate message', function () {
-      it('onPrimary is false', function () {
-        expect(popup.onPrimary()).to.false;
-      });
-      it('isStartDateError is true', function () {
-        expect(customDatePopup.get('errors.isStartDateError')).to.ok;
-      });
-      it('isEndDateError is true', function () {
-        expect(customDatePopup.get('errors.isEndDateError')).to.ok;
-      });
-      it('startDate invalid', function () {
-        expect(customDatePopup.get('errorMessages.startDate')).to.equal(Em.I18n.t('jobs.customDateFilter.error.required'));
-      });
-      it('endDate invalid', function () {
-        expect(customDatePopup.get('errorMessages.endDate')).to.equal(Em.I18n.t('jobs.customDateFilter.error.required'));
-      });
-    });
-
-    describe('passed start date is greater then end data, validation should fail with apporpriate message', function () {
-      beforeEach(function () {
-        customDatePopup.set('customDateFormFields.startDate', '11/11/11');
-        customDatePopup.set('customDateFormFields.endDate', '11/10/11');
-      });
-
-      it('onPrimary is false', function () {
-        expect(popup.onPrimary()).to.false;
-      });
-      it('isStartDateError is false', function () {
-        expect(customDatePopup.get('errors.isStartDateError')).to.false;
-      });
-      it('isEndDateError is true', function () {
-        expect(customDatePopup.get('errors.isEndDateError')).to.ok;
-      });
-      it('endDate invalid', function () {
-        expect(customDatePopup.get('errorMessages.endDate')).to.equal(Em.I18n.t('jobs.customDateFilter.error.date.order'));
-      });
-    });
-
-    describe('valid values passed, `valueObject` should contain `endTime` and `startTime`', function () {
-
-      beforeEach(function () {
-        customDatePopup.set('customDateFormFields.startDate', '11/11/11');
-        customDatePopup.set('customDateFormFields.endDate', '11/12/11');
-        popup.onPrimary();
-      });
-
-      it('startTime is valid', function() {
-        expect(context.get('actualValues.startTime')).to.equal(new Date('11/11/11 01:00 AM').getTime());
-      });
-      it('endTime is valid', function() {
-        expect(context.get('actualValues.endTime')).to.equal(new Date('11/12/11 01:00 AM').getTime());
-      });
-
-    });
-
-  });
-});

+ 260 - 0
ambari-web/test/views/common/select_custom_date_view_test.js

@@ -0,0 +1,260 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+require('views/common/select_custom_date_view');
+
+describe('App.JobsCustomDatesSelectView', function () {
+
+  var view;
+
+  beforeEach(function () {
+    view = App.JobsCustomDatesSelectView.create();
+  });
+
+  describe('#isCustomEndDate', function () {
+
+    var cases = [
+      {
+        duration: null,
+        isCustomEndDate: false,
+        title: 'duration not set'
+      },
+      {
+        duration: 1000,
+        isCustomEndDate: false,
+        title: 'preset duration'
+      },
+      {
+        duration: 0,
+        isCustomEndDate: true,
+        title: 'custom duration'
+      }
+    ];
+
+    beforeEach(function () {
+      view.reopen({
+        validate: Em.K
+      });
+    });
+
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        view.set('customDateFormFields.duration', {
+          value: item.duration
+        });
+        expect(view.get('isCustomEndDate')).to.equal(item.isCustomEndDate);
+      });
+    });
+
+  });
+
+  describe('#createCustomStartDate', function () {
+
+    var cases = [
+      {
+        startDate: '01/01/2016',
+        hoursForStart: '01',
+        minutesForStart: '00',
+        middayPeriodForStart: 'AM',
+        isInvalidDate: false,
+        title: 'valid date and time'
+      },
+      {
+        startDate: '',
+        hoursForStart: '01',
+        minutesForStart: '00',
+        middayPeriodForStart: 'AM',
+        isInvalidDate: true,
+        title: 'no date specified'
+      },
+      {
+        startDate: '01/01/2016',
+        hoursForStart: '',
+        minutesForStart: '00',
+        middayPeriodForStart: 'AM',
+        isInvalidDate: true,
+        title: 'no hours specified'
+      },
+      {
+        startDate: '01/01/2016',
+        hoursForStart: '01',
+        minutesForStart: '',
+        middayPeriodForStart: 'AM',
+        isInvalidDate: true,
+        title: 'no minutes specified'
+      },
+
+      {
+        startDate: '01/01/2016',
+        hoursForStart: '01',
+        minutesForStart: '00',
+        middayPeriodForStart: '',
+        isInvalidDate: true,
+        title: 'no midday period specified'
+      }
+    ];
+
+    beforeEach(function () {
+      view.reopen({
+        validate: Em.K
+      });
+    });
+
+    cases.forEach(function (item) {
+      it(item.title, function () {
+        view.get('customDateFormFields').setProperties({
+          startDate: item.startDate,
+          hoursForStart: item.hoursForStart,
+          minutesForStart: item.minutesForStart,
+          middayPeriodForStart: item.middayPeriodForStart
+        });
+        expect(Em.isNone(view.createCustomStartDate())).to.equal(item.isInvalidDate);
+      });
+    });
+
+  });
+
+  describe('#createCustomEndDate', function () {
+
+    var customEndCases = [
+      {
+        endDate: '01/01/2016',
+        hoursForEnd: '01',
+        minutesForEnd: '00',
+        middayPeriodForEnd: 'AM',
+        isInvalidDate: false,
+        title: 'valid date and time'
+      },
+      {
+        endDate: '',
+        hoursForEnd: '01',
+        minutesForEnd: '00',
+        middayPeriodForEnd: 'AM',
+        isInvalidDate: true,
+        title: 'no date specified'
+      },
+      {
+        endDate: '01/01/2016',
+        hoursForEnd: '',
+        minutesForEnd: '00',
+        middayPeriodForEnd: 'AM',
+        isInvalidDate: true,
+        title: 'no hours specified'
+      },
+      {
+        endDate: '01/01/2016',
+        hoursForEnd: '01',
+        minutesForEnd: '',
+        middayPeriodForEnd: 'AM',
+        isInvalidDate: true,
+        title: 'no minutes specified'
+      },
+
+      {
+        endDate: '01/01/2016',
+        hoursForEnd: '01',
+        minutesForEnd: '00',
+        middayPeriodForEnd: '',
+        isInvalidDate: true,
+        title: 'no midday period specified'
+      }
+    ];
+
+    beforeEach(function () {
+      view.reopen({
+        validate: Em.K
+      });
+    });
+
+    customEndCases.forEach(function (item) {
+      it(item.title, function () {
+        view.get('customDateFormFields').setProperties({
+          endDate: item.endDate,
+          hoursForEnd: item.hoursForEnd,
+          minutesForEnd: item.minutesForEnd,
+          middayPeriodForEnd: item.middayPeriodForEnd,
+          duration: {
+            value: 0
+          }
+        });
+        expect(Em.isNone(view.createCustomEndDate(new Date()))).to.equal(item.isInvalidDate);
+      });
+    });
+
+    it('preset duration', function () {
+      view.set('customDateFormFields.duration', {
+        value: 900000
+      });
+      expect(view.createCustomEndDate(new Date(1000)).getTime()).to.equal(901000);
+    });
+
+  });
+
+  describe('#setErrorMessage', function () {
+
+    var cases = [
+      {
+        key: 'startDate',
+        property: 'isStartDateError',
+        value: true,
+        message: 'error',
+        errorMessage: 'error',
+        title: 'error'
+      },
+      {
+        key: 'endDate',
+        property: 'isEndDateError',
+        value: false,
+        message: null,
+        errorMessage: '',
+        title: 'no error'
+      }
+    ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          view.get('errors').setProperties({
+            isStartDateError: false,
+            isEndDateError: true
+          });
+          view.get('errorMessages').setProperties({
+            startDate: '',
+            endDate: 'error'
+          });
+          view.setErrorMessage(item.key, item.message);
+        });
+
+        it('should set error flag', function () {
+          expect(view.get('errors').get(item.property)).to.equal(item.value);
+        });
+
+        it('should set error message', function () {
+          expect(view.get('errorMessages').get(item.key)).to.equal(item.errorMessage);
+        });
+
+      });
+
+    });
+
+  });
+
+});