livingsocial/rearview-engine

View on GitHub
public/rearview-src/js/view/expandedmonitor.js

Summary

Maintainability
F
5 days
Test Coverage
define([
    'view/base',
    'view/deletemonitor',
    'model/monitor',
    'util/cron',
    'parsley'
], function(
    BaseView,
    DeleteMonitorView,
    JobModel,
    CronUtil
){
    var ExpandedMonitorView = BaseView.extend({

        cronScheduleFormValid         : true,
        cronScheduleWeekdaysSelected  : false,
        cronScheduleDaysDisabled      : false,

        events : {
            'click .name-save'            : 'updateMonitorName',
            'click .schedule-tab'         : '_setExpandedViewHeight',
            'click #cronScheduleFormEdit .day-month-picker .day-picker button' : 'cronWeekdayButtonClicked',
            'change #selectPreviousError' : '_setErrorDropDown'
        },

        subscriptions : {
            'view:dashboard:init'       : 'exit',
            'view:deletemonitor:delete' : 'deleteMonitor'
        },

        initialize : function(options) {
            _.bindAll(this);

            var self = this;
            this.user    = options.user;
            this.router  = options.router;
            this.templar = options.templar;

            // init delete monitor view
            this.deleteMonitorView = new DeleteMonitorView({
                'el'      : $('.delete-monitor-wrap'),
                'templar' : this.templar
            });

            // use debounce to throttle resize events and set the height when
            // the viewport changes.
            var resize = _.debounce(this._setExpandedViewHeight, 500);

            // Add the event listener
            $(window).resize(resize);
        },

        render : function(id, categories, categoryId, dashboardId) {
            this.monitorId   = id;
            this.categories  = categories;
            this.categoryId  = categoryId;
            this.dashboardId = dashboardId;
            this.initMonitor();
        },
        /**
         * ExpandedMonitorView#initMonitor(model)
         * - model (Object): job (monitor) backbone model object
         *
         *
         **/
        initMonitor : function() {
            var self = this;

            // retrieve monitor model & graph data
            self.getMonitor(self.monitorId, function(result) {
                // make sure update has a reference to the instance and model
                // at the time of editing/updating
                self.model = result;
                _.bind(self.updateMonitor, self, self.model);

                _.each(self.categories, function(category) {
                    if (category.id === self.model.get('dashboardId')) {
                        category.selected = true;
                    } else {
                        category.selected = false;
                    }
                });

                self.getGraphData(self.monitorId, function(result) {

                    self._initErrorsList(function(errors) {
                        self.templar.render({
                            path : 'expandedmonitor',
                            el   : self.$el,
                            data : {
                                'monitor'    : self.model.toJSON(),
                                'errors'     : errors,
                                'alerts'     : errors,
                                'output'     : self.model.get('output'),
                                'categories' : self.categories
                            }
                        });

                        self.$monitor = self.$el.find('.expanded-monitor');
                        //edit-monitor-wrap container element
                        self.$el.html( self.$monitor );
                        // add events to the view buttons in the alert window
                        self._setAlertsHistory();
                        // setup the graph area
                        self.chart = self.initGraph( self.$monitor.find('.graph')[0]);
                        // set error state
                        self._setErrorState();

                        self.updateGraph(self.model, function(output) {
                            self._initializeCodeMirror();
                            self._initializeDatePicker('.input-from-date');
                            self._setErrorDropDown();
                            self._setScheduleData();
                            self._setEditNameField();
                            self.$el.find('.save-changes').click({'model' : self.model}, self.updateMonitor);
                            self.$el.find('.cancel-changes').click(function() {
                                self.exit();
                            });
                            self.$el.find('.test-in-graph button').click({
                                'model'  : new JobModel(),
                                'output' : output
                            }, self.testMonitor, self);

                            // finally open up the edit monitor widget
                            self.open();
                        }, self.formattedGraphData);

                        // inline help
                        self.setHelp();
                        self.setCronScheduleValidation(); 

                        // dynamically set the heights to maximum screen utilization
                        self._setExpandedViewHeight();
                    }, self.model);

                });
            });


        },

        setCronScheduleWeekdaysSelected : function() {
          this.cronScheduleWeekdaysSelected = $('#cronScheduleFormEdit .day-month-picker .day-picker button.active').size() > 0
        },

        setCronScheduleDaysDisabled : function(disabled) {
          if(disabled) {
            if(!this.cronScheduleDaysDisabled) {
              $('#inputDays').parsley('ParsleyField').reset();
              $('#inputDays').val('?');
              $('#inputDays').attr('disabled',"");
              this.cronScheduleDaysDisabled = true;
            }
          }
          else {
            if(this.cronScheduleDaysDisabled) {
              $('#inputDays').val('*');
              $('#inputDays').removeAttr('disabled');
              this.cronScheduleDaysDisabled = false;
            } 
          }
        },

        cronWeekdayButtonClicked : function(event) {
            $(event.target).button('toggle');
            this.setCronScheduleWeekdaysSelected();
            this.setCronScheduleDaysDisabled(this.cronScheduleWeekdaysSelected);
        },
        
        setCronScheduleValidation : function() {
            this.cronScheduleForm = $('#cronScheduleFormEdit');
            var validator = CronUtil.parsleyValidator();
            _.extend(validator,{
                errors: {
                  container: function (parsleyElement, parsleyTemplate, isRadioOrCheckbox) {
                    var container = $('#cronScheduleFormEditErrors');
                    container.append(parsleyTemplate);
                    return container;
                  }
                },
                listeners: {
                    onFormSubmit : function ( isFormValid, event, ParsleyForm ) {
                      this.cronScheduleFormValid = isFormValid;
                    }.bind(this)
                }
            });
            this.cronScheduleForm.parsley(validator);
        },

        getGraphData : function(monitorId, cb) {
            var self = this;
            $.ajax({
                url   : '/jobs/' + monitorId + '/data',
                success : function(result) {
                    if ( result.graph_data ) {
                        self.formattedGraphData = self.formatGraphData( result.graph_data );
                    }
                    // always call the callback function
                    // it loads the view
                    if ( !_.isUndefined(cb) ) cb(result);
                },
                error : function(result) {
                    if ( !_.isUndefined(cb) ) {
                        cb(result);
                    }
                }
            });
        },

        getMonitor : function(monitorId, cb) {
            var self = this;

            self.model = new JobModel({
                id : monitorId
            });

            self.model.fetch({
                success : function(result) {
                    if ( !_.isUndefined(cb) ) {
                        cb(result);
                    }
                }
            });
        },

        getErrorStatus : function(status) {
            var self  = this,
                error = false;

            switch ( status ) {
                case 'error' :
                    error = true;
                    break;
                case 'failed' :
                    error = true;
                    break;
                case 'graphite_error' :
                    error = true;
                    break;
                case 'graphite_metric_error' :
                    error = true;
                    break;
                case 'security_error' :
                    error = true;
                    break;
            }

            return error;
        },
        setHelp : function() {
            var self     = this,
            $quickContent = '';
            $alertContent = '';
            $cronHelpContent = '';

            $.ajax({
                url     : rearview.path + '/help/cron.html',
                async   : false,
                success : function( response ) {
                    $cronHelpContent = response;
                }
            });

            $.ajax({
                url     : rearview.path + '/help/quick.html',
                async   : false,
                success : function( response ) {
                    $quickContent = response;
                }
            });

            $.ajax({
                url     : rearview.path + '/help/alert.html',
                async   : false,
                success : function( response ) {
                    $alertContent = response;
                }
            });

            self.$el.find('#viewSchedule .help.label').tooltip({
                trigger   : 'click',
                html      : true,
                placement : 'left',
                delay     : { show : 100, hide : 200 },
                title     : $cronHelpContent
            });

            self.$el.find('.help:nth-child(2)').tooltip({
                trigger   : 'click',
                html      : true,
                placement : 'right',
                delay     : { show : 100, hide : 200 },
                title     : $quickContent
            });

            self.$el.find('#viewSettings .monitor-meta .pager-duty .help.label').tooltip({
                trigger   : 'click',
                html      : true,
                placement : 'left',
                delay     : { show : 100, hide : 200 },
                title     : $alertContent
            });
        },
        /**
         * ExpandedMonitorView#open()
         *
         *
         **/
        open : function() {
            this.$el.addClass('active');
            //publish for other views can update
            Backbone.Mediator.pub('view:expandedmonitor:open', {
                'nav' : {
                    'back' : true
                }
            });
            // set route for hotlinking purposes
            this.router.navigate('dash/' + this.dashboardId + '/expand/' + this.model.get('id'));
        },
        /**
         * ExpandedMonitorView#exit()
         *
         *
         **/
        exit : function(monitor) {
            var self    = this,
                monitor = ( monitor ) ? monitor : null;

            self.$el.removeClass('active');

            // set route for hotlinking purposes
            if(self.model) {
                self.router.navigate('dash/' + self.model.get('dashboardId'));
            }

            // fire event that expandedmonitor is exiting
            Backbone.Mediator.pub('view:expandedmonitor:exit', {
                status : 'delete'
            });
        },
        /**
         * ExpandedMonitorView#updateGraph(model, cb)
         * - model (Object): job (monitor) backbone model object
         * - cb (Function): callback function
         *
         **/
        updateGraph : function(model, cb, graphData) {
            var self = this;

            if(graphData) {
                self.renderGraphData(self.chart, graphData);

                if(typeof cb === 'function') {
                    cb();
                }
            } else {
                $.ajax({
                    url     : '/monitor.json',
                    type    : 'POST',
                    data    : model.toJSON(),
                    success : function(result) {
                        if ( result.graph_data ) {
                            var formattedGraphData = self.formatGraphData( result.graph_data );
                            self.renderGraphData(self.chart, formattedGraphData);
                            self.$el.find('textarea.output-view').val(result.output);
                        }

                        if(result.status === 'error') {
                            Backbone.Mediator.pub('view:expandedmonitor:test', {
                                'model'     : this.model,
                                'errors'    : result.errors,
                                'raw'       : result.output,
                                'attention' : 'Monitor Test Error!',
                                'status'    : 'error'
                            });
                        }

                        if(typeof cb === 'function') {
                            cb(result.output);
                        }
                    },
                    failure : function(result) {
                    }
                });
            }
        },
        /**
         * ExpandedMonitorView#testMonitor(e)
         *
         *
         **/
        testMonitor : function(e) {
            var self        = this,
                monitor     = e.data.model,
                output      = e.data.output,
                $testBtn    = $(e.target);

            // set button state
            $testBtn.button('loading');

            monitor = self._setMetrics(monitor);
            monitor = self._setSchedule(monitor);

            // update to the output view for testing
            self.$el.find('textarea.output-view').val(output);
            self.$el.find('.output-tab').tab('show');

            self.updateGraph(monitor, function() {
                $testBtn.button('reset');
            });
        },
        /**
         * ExpandedMonitorView#viewErrorInGraph(e)
         *
         *
         **/
        viewErrorInGraph : function(e) {
            var self        = this,
                monitor     = e.data.model,
                output      = e.data.output,
                toDate      = e.data.toDate;

            monitor.set('toDate', toDate);

            // viewError in graph gets the from
            // toDate field not from the html field
            monitor = self._setMetrics(monitor, true);
            monitor = self._setSchedule(monitor);

            // otherwise update to the output view for testing
            self.$el.find('textarea.output-view').val(output);
            self.$el.find('.output-tab').tab('show');

            self.updateGraph(monitor);
        },
        /**
         * ExpandedMonitorView#updateMonitor(e)
         *
         *
         **/
        updateMonitor : function(e) {
            var self        = this,
                monitor     = e.data.model,
                test        = e.data.test,
                toDate      = e.data.toDate,
                output      = e.data.output;

            this.cronScheduleForm.parsley('validate');
            if(this.cronScheduleFormValid) {
            
              monitor = self._setMetrics(monitor);
              monitor = self._setSchedule(monitor);
              monitor = self._setSettings(monitor);

              monitor.save(null, {
                  success : function(model, response, options) {
                      Backbone.Mediator.pub('view:expandedmonitor:save', {
                          'model'     : self.model,
                          'message'   : "The monitor '" + model.get('name') + "' was saved.",
                          'attention' : 'Monitor Saved!',
                          'status'    : 'success'
                      });

                      // quit out of the edit monitor view
                      self.exit(monitor);
                      self.updateGraph(monitor);
                  },
                  error : function(model, xhr, options) {
                      Backbone.Mediator.pub('view:expandedmonitor:save', {
                          'model'     : self.model,
                          'tryJSON'   : xhr.responseText,
                          'attention' : 'Monitor Saved Error!',
                          'status'    : 'error'
                      });
                  }
              });

            }
        },
        /**
         * ExpandedMonitorView#deleteMonitor(e)
         *
         *
         **/
        deleteMonitor : function() {
            var self    = this;

            self.model.destroy({
                success : function(model, response) {
                    Backbone.Mediator.pub('view:expandedmonitor:delete', {
                        'model'     : self.model,
                        'message'   : "The monitor '" + model.get('name') + "' was removed from the current dashboard.",
                        'attention' : 'Monitor Deleted!'
                    });
                },
                error : function(model, response) {
                    Backbone.Mediator.pub('view:expandedmonitor:delete', {
                        'model'     : self.model,
                        'message'   : "The monitor '" + model.get('name') + "' produced an error during the process of deletion.",
                        'attention' : 'Monitor Delete Error!',
                        'status'    : 'error'
                    });
                }
            });

            self.exit();
        },
        /**
         * ExpandedMonitorView#updateMonitorName()
         */
        updateMonitorName : function() {
            var self = this;

            var $nameField   = self.$el.find('.name-field'),
                previousName = $nameField.prev().html();

            self.model.set({
                'name' : $nameField.find('input').val()
            });

            $nameField.prev().html( self.model.get('name') );
            $nameField.hide();
            $nameField.prev().show();
        },
        /**
         * ExpandedMonitorView#resize()
         */
        resize : function() {
            var self = this;
            return _.debounce(self._setExpandedViewHeight, 500);
        },
        /**
         * ExpandedMonitorView#destructor()
         */
        destructor : function() {
            var self          = this,
                $prevSibling  = self.$el.prev();

            // clean up nested views
            self.deleteMonitorView.destructor();

            // unsubscribe from mediator channels
            self.destroySubscriptions();

            self.remove();
            self.unbind();
            self.off();

            $prevSibling.after("<div class='edit-monitor-wrap clearfix'>");
        },
        /**
         *
         */
        _setExpandedViewHeight : function() {
            var windowOffSet = 580,
                alertsOffset = 6,
                outputOffset = 4,
                tabHeight    = ( this.$el.find('#viewMetrics') )
                             ? this.$el.find('#viewMetrics').height()
                             : null;

            // tab height fixing
            if ( tabHeight ) {
                this.$el.find('#viewSchedule, #viewSettings').css({
                    'height' : tabHeight + 'px'
                });
            }

            this.$el.find('.output-view').css({
                'height' : ( $(window).height() - windowOffSet - outputOffset ) + 'px'
            });

            this.$el.find('.alerts-history ul').css({
                'height' : ( $(window).height() - windowOffSet - alertsOffset ) + 'px'
            });

            this.$el.find('.graph').css({
                'height' : ( $(window).height() - windowOffSet ) + 'px'
            });

            // set chart height dynamically
            var chartHeight = ( ($(window).height() - windowOffSet) > 150 ) ? $(window).height() - windowOffSet : 150;
            if (this.chart && chartHeight > 150) {
                this.chart.setSize(null, chartHeight);
            }
        },
        /**
         *
         */
        _setErrorState : function() {
            var errorStatus = this.getErrorStatus(this.model.get('status'));

            if( errorStatus ) {
                this.$monitor.addClass('red');
            } else {
                this.$monitor.removeClass('red');
            }
        },
        /** internal
         * ExpandedMonitorView#_initErrorsList(cb)
         *
         *
         **/
        _initErrorsList : function(cb) {
            var self      = this,
                monitorId = self.model.get('id');

            $.get('/jobs/' + monitorId + '/errors', function(data) {
                data = _.map(data, function(error) {
                    return {
                        label   : self.formatServerDateTime(error.date, true).toString("MM/dd/yyyy HH:mm"),
                        value   : error.date,
                        message : error.message,
                        id      : error.id
                    }
                });

                cb(data);
            });
        },
        /** internal
         * ExpandedMonitorView#_setErrorDropDown()
         **/
        _setErrorDropDown : function() {
            var self     = this,
                dropDown = this.$el.find('#selectPreviousError');

            if ( dropDown.find('option:selected').val() !== '' ) {
                var selectedErrorDateTime = dropDown.find('option:selected').html();
                self.fromDatePicker.datetimepicker('setDate', selectedErrorDateTime);
            }
        },
        /** internal
         * ExpandedMonitorView#_setEditNameField()
         *
         *
         **/
        _setEditNameField : function() {
            var self       = this,
                $nameField = self.$el.find('.name-field');

            $nameField.prev().click(function() {
                $nameField.prev().hide();
                $nameField.show();
                $nameField.find('input').focus();
            });
        },
        /** internal
         * ExpandedMonitorView#_initializeCodeMirror()
         *
         *
         **/
        _initializeCodeMirror : function() {
            var self          = this,
                $expressions  = self.$el.find('#inputExpressions')[0],
                $metrics      = self.$el.find('#inputMetrics')[0];

            self.expressionsMirror = CodeMirror.fromTextArea( $expressions, {
                value        : '',
                lineNumbers  : true,
                lineWrapping : true,
                height       : '100',
                mode         : 'ruby',
                theme        : 'ambiance',
                onKeyEvent   : function(i, e) {
                    if (( e.keyCode == 70 && e.ctrlKey ) && e.type == 'keydown') {
                        e.stop();
                        return self._toggleFullscreen('.expanded-monitor .expressions .CodeMirror', self.expressionsMirror);
                    }
                }
            });

            self.metricsMirror = CodeMirror.fromTextArea( $metrics, {
                value        : '',
                lineNumbers  : true,
                lineWrapping : true,
                mode         : 'ruby',
                theme        : 'ambiance',
                onKeyEvent   : function(i, e) {
                    if (( e.keyCode == 70 && e.ctrlKey ) && e.type == 'keydown') {
                        e.stop();
                        return self._toggleFullscreen('.expanded-monitor .metrics .CodeMirror', self.metricsMirror);
                    }
                }
            });
        },
        /** internal
         * ExpandedMonitorView#_initializeDatePicker()
         *
         *
         **/
        _initializeDatePicker : function(selector) {
            var self = this;
            self.fromDatePicker = self.$el.find(selector).datetimepicker();
        },
        /** internal
         * ExpandedMonitorView#_setScheduleData(model)
         *
         *
         **/
        _setScheduleData : function() {
            var self     = this,
                cronExpr = self.model.get('cronExpr').split(' '); // space delimited cron job expression

            self.$el.find('#inputSeconds').val( cronExpr[0] );
            self.$el.find('#inputMinutes').val( cronExpr[1] );
            self.$el.find('#inputHours').val( cronExpr[2] );
            self.$el.find('#inputDays').val( cronExpr[3] );

            var month   = cronExpr[4].split(','),
                weekday = cronExpr[5].split(',');

            self._setButtonGroup( self.$el.find('.day-picker button'), weekday );
            self._setButtonGroup( self.$el.find('.month-picker button'), month );
        },
        /** internal
         * ExpandedMonitorView#_setButtonGroup(collection, dataList)
         *
         *
         **/
        _setButtonGroup : function(collection, dataList) {
            collection.each(function(index, day) {
                day = $(day);

                var dataValue = _.indexOf(dataList, day.attr('data-value'))

                if ( dataValue > -1 ) {
                    day.addClass('active');
                }
            });
        },
        /** internal
         * ExpandedMonitorView#_setMetrics(model)
         *
         * Set metrics data to the job model.
         **/
        _setMetrics : function( model, errorView ) {
            var self = this;

            model.set({
                'userId'      : self.user.get('id'),
                'monitorExpr' : self.expressionsMirror.getValue(),
                'metrics'     : self.metricsMirror.getValue().split('\n'),
                'minutes'     : parseInt(self.$el.find('.input-minutes-back').val()),
                'toDate'      : ( errorView ) ? model.get('toDate') : self.$el.find('.input-from-date').val()
            });

            return model;
        },
        /** internal
         * ExpandedMonitorView#_setSchedule(model)
         *
         * Set scheduling data to the job model.
         **/
        _setSchedule : function( model ) {
            var monitorName = this.$el.find('.monitor-name')[0]
                            ? this.$el.find('.monitor-name').val()
                            : model.get('name');
            // grab form data
            model.set({
                'userId'      : this.user.get('id'),
                'name'        : monitorName,
                'description' : this.$el.find('#description').val(),
                'alertKeys'   : this.parseAlertKeys( this.$el.find('.pager-duty textarea').val() ),
                'cronExpr'    : this._createCronExpr()
            });

            return model;
        },

        _setSettings : function( model ) {
            model.set({
                'dashboardId' : parseInt(this.$el.find('#selectCategory').val(), 10)
            });

            return model;
        },

        _setAlertsHistory : function() {
            var self = this;
            self.$monitor.find('.alerts-history ul').delegate( "li button", "click", function(e) {
                self.viewErrorInGraph({
                    'data' : {
                        'model' : new JobModel(),
                        'test'  : true,
                        'toDate': $(e.target).attr('data-date')
                    }
                });

                // copy datetime to from field
                self.fromDatePicker.datetimepicker('setDate', $(e.target).attr('data-date'));
            });
        }
    });

    return ExpandedMonitorView;
});