public/rearview-src/js/view/addmonitor.js
define([
'view/base',
'model/monitor',
'codemirror',
'util/cron',
'codemirror-ruby',
'jquery-validate',
'parsley'
], function(
BaseView,
MonitorModel,
CodeMirror,
CronUtil
){
var AddMonitorView = BaseView.extend({
scheduleViewInitialized : false,
metricsViewInitialized : false,
scheduleView : true,
cronScheduleFormValid : false,
cronScheduleWeekdaysSelected : false,
cronScheduleDaysDisabled : false,
namePagerFormValid : false,
el : '.add-monitor-wrap',
subscriptions : {
'view:dashboard:category' : 'updateDashboardId',
'view:settings:save' : 'render'
},
events : {
'click .setMetrics' : 'advanceToMetrics',
'click .testGraph' : 'testMetrics',
'click .nameSchedule' : 'backToSchedule',
'click .saveFinish' : 'saveFinish',
'click .back' : 'exitFullScreen',
'click #cronScheduleForm .day-month-picker .day-picker button' : 'cronWeekdayButtonClicked',
'hide #addMonitor' : 'modalClose',
'show #addMonitor' : 'modalShow',
'shown #addMonitor' : 'focusFirst'
},
initialize : function(options) {
_.bindAll(this);
this.user = options.user;
this.dashboardId = options.dashboardId;
this.templar = options.templar;
// use debounce to throttle resize events and set the height when
// the viewport changes.
var resize = _.debounce(this.adjustModalLayout, 500);
// Add the event listener
$(window).resize(resize);
this.render();
},
render : function() {
this.setElement( $('.add-monitor-wrap') );
this.templar.render({
path : 'addmonitor',
el : this.$el,
data : {}
});
this.templar.render({
path : 'schedulemonitor',
el : this.$el.find('.content-wrap'),
data : {
'user' : this.user.toJSON()
}
});
this.scheduleViewInitialized = true;
this.setNamePagerValidation();
this.setCronScheduleValidation();
this.setNameScheduleHelp();
// store reference to modal
this.$modal = this.$el.find('.add-monitor');
this.resizeModal($('#addMonitor'), 'large');
},
updateDashboardId : function(id) {
this.dashboardId = id;
this.model.set({
'dashboardId' : this.dashboardId
});
},
/** Non-Backbone methods **/
/**
* AddMonitorView#adjustModalLayout()
*
* Dynamic adjustment of elements within the modal interface
**/
adjustModalLayout : function() {
var $modal = $('#addMonitor'),
sizes = this.resizeModal($modal, 'large'),
heroUnitMin = 325,
heroAdjust = 80, // hero unit height adjust
testFieldsetAdjust = 150,
graphOutputAdjust = 80;
// only adjust if not the scheduling view step
if ( !this.scheduleView ) {
var heroMinHeightCheck = ( sizes.body.height - heroAdjust ) > heroUnitMin
? sizes.body.height - heroAdjust
: heroUnitMin;
// make all the needed height, width calculation and DOM adjustments
$modal.find('.hero-unit').css({
'height' : heroMinHeightCheck
});
var MirrorHeightCalculation = Math.floor((heroMinHeightCheck - testFieldsetAdjust ) / 2);
this.expressionsMirror.setSize(null, MirrorHeightCalculation);
this.metricsMirror.setSize(null, MirrorHeightCalculation);
var graphOutputHeightCalculation = Math.floor(( heroMinHeightCheck - graphOutputAdjust ) / 2);
$modal.find('.graph').css({
'height' : graphOutputHeightCalculation
});
// highcharts graph needs to be sized via highcharts api
this.chart.setSize(null, graphOutputHeightCalculation);
$modal.find('#outputView').css({
'height' : graphOutputHeightCalculation
});
}
},
/**
* AddMonitorView#advanceToMetrics()
*
* Triggers form validation which on success advances to the metrics
* view.
**/
advanceToMetrics : function() {
this.namePagerForm.parsley('validate');
this.cronScheduleForm.parsley('validate');
if(this.namePagerFormValid && this.cronScheduleFormValid) {
this._setSchedule();
this._setupMetricsView();
}
},
/**
* AddMonitorView#backToSchedule()
*
* Sets the schedule view when going backwards through the add monitor
* workflow.
**/
backToSchedule : function() {
this._setupScheduleView();
},
/**
* AddMonitorView#exitFullScreen(e)
*
* Method for changing styles on CodeMirror in question so the user
* can work in a fullscreen setting.
**/
exitFullScreen : function(e) {
var $closeButton = this.$el.find('button.close'),
$backButton = this.$el.find('button.back'),
$metricsEditor = $('.add-monitor .metrics .CodeMirror'),
$expressionsEditor = $('.add-monitor .expressions .CodeMirror');
$closeButton.show();
$backButton.hide();
$metricsEditor.removeClass('fullscreen');
if ( $metricsEditor.data('beforeFullscreen') ) {
$metricsEditor.height($metricsEditor.data('beforeFullscreen').height);
$metricsEditor.width($metricsEditor.data('beforeFullscreen').width);
}
this.metricsMirror.refresh();
$expressionsEditor.removeClass('fullscreen');
if ( $expressionsEditor.data('beforeFullscreen') ) {
$expressionsEditor.height($expressionsEditor.data('beforeFullscreen').height);
$expressionsEditor.width($expressionsEditor.data('beforeFullscreen').width);
}
this.expressionsMirror.refresh();
},
setCronScheduleWeekdaysSelected : function() {
this.cronScheduleWeekdaysSelected = $('#cronScheduleForm .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);
},
setNameScheduleHelp : function() {
$cronHelpContent = '';
$alertHelpContent = '';
$.ajax({
url : rearview.path + '/help/cron.html',
async : false,
success : function( response ) {
$cronHelpContent = response;
}
});
$.ajax({
url : rearview.path + '/help/alert.html',
async : false,
success : function( response ) {
$alertHelpContent = response;
}
});
this.$el.find('#cronScheduleForm .set-schedule .help.label').tooltip({
trigger : 'click',
html : true,
placement : 'left',
delay : { show : 100, hide : 200 },
title : $cronHelpContent
});
this.$el.find('#namePagerForm .pager-duty .help.label').tooltip({
trigger : 'click',
html : true,
placement : 'left',
delay : { show : 100, hide : 200 },
title : $alertHelpContent
});
},
/**
* AddMonitorView#setNamePagerValidation()
*
* Sets up the front end form validation for the name field which is required.
* If name is present, save the sceduling data to the monitor model and setup the
* next view in the add monitor workflow to set up the metrics data.
**/
setNamePagerValidation : function() {
this.namePagerForm = $('#namePagerForm');
var validator = this.namePagerForm.parsley({
listeners: {
onFormSubmit : function ( isFormValid, event, ParsleyForm ) {
this.namePagerFormValid = isFormValid;
}.bind(this)
}
});
},
setCronScheduleValidation : function() {
this.cronScheduleForm = $('#cronScheduleForm');
var validator = CronUtil.parsleyValidator();
_.extend(validator,{
errors: {
container: function (parsleyElement, parsleyTemplate, isRadioOrCheckbox) {
var container = $('#cronScheduleFormErrors');
container.append(parsleyTemplate);
return container;
}
},
listeners: {
onFormSubmit : function ( isFormValid, event, ParsleyForm ) {
this.cronScheduleFormValid = isFormValid;
}.bind(this)
}
});
this.cronScheduleForm.parsley(validator);
},
setMetricsValidation : function() {
$.validator.addMethod('code', function(value, element) {
var mirror = $(element).data('CodeMirror'),
wrapper = $( mirror.getWrapperElement() );
return this._validateMirror(mirror);
}.bind(this), 'This field is required.');
$.validator.addMethod('metric-ruby', function(value, element) {
var valid = false;
$.ajax({
url : '/monitor.json',
type : 'post',
data : this.model.toJSON(),
async : false,
success : function( response ) {
if ( response.status == 'success' ) {
valid = true;
}
}
});
return valid;
}, 'Your metrics code does not validate.');
$.validator.addMethod('expression-ruby', function(value, element) {
var valid = false;
$.ajax({
url : '/monitor.json',
type : 'post',
data : this.model.toJSON(),
async : false,
success : function( response ) {
if ( response.status == 'success' || value == '') {
valid = true;
}
}
});
return valid;
}.bind(this), 'Your expression code does not validate.');
this.metricsForm = $('#metricsExpressionsForm');
// set up form validation
this.metricsForm.validate({
rules : {
'inputMetrics' : {
'code' : true,
'expression-ruby' : true
},
'inputExpressions' : {
'expression-ruby' : true
}
},
errorPlacement: function(error, element) {
var mirror = $(element).data('CodeMirror');
if(mirror) {
var wrapper = $( mirror.getWrapperElement() );
this._validateMirror(mirror);
error.insertAfter(wrapper);
} else {
error.insertAfter(element);
}
}.bind(this),
highlight : function(label) {
$(label).closest('.control-group').addClass('error');
$(label).closest('fieldset').addClass('error');
},
success : function(label) {
$(label).closest('.control-group').removeClass('error');
$(label).closest('fieldset').removeClass('error');
$(label).remove();
},
submitHandler : function(form) {
this._saveMonitor(function() {
this._closeModal();
}.bind(this));
}.bind(this)
});
},
/**
* AddMonitorView#testMetrics()
*
* Set scheduling data to the monitor model and post the data to
* the /job route which will return the proper graphite data.
* Finally, format the returned data for HighCharts to consume and
* render.
**/
testMetrics : function() {
this._setMetrics();
$.post('/monitor.json', this.model.toJSON(), function(result) {
if (result.graph_data) {
var formattedGraphData = this.formatGraphData( result.graph_data );
this.renderGraphData(this.chart, formattedGraphData);
// set the output field from the std out response
$('#outputView').val(result.output);
}
if(result.status === 'error') {
Backbone.Mediator.pub('view:addmonitor:test', {
'model' : this.model,
'errors' : result.errors,
'raw' : result.output,
'attention' : 'Monitor Test Error!',
'status' : 'error'
});
}
}.bind(this))
.error(function(result) {
Backbone.Mediator.pub('view:addmonitor:test', {
'model' : this.model,
'errors' : result.errors,
'raw' : result.output,
'attention' : 'Monitor Test Error!',
'status' : 'error'
});
}.bind(this));
},
/**
* AddMonitorView#modalClose(e)
*
**/
modalClose : function(e) {
// NOTE : hack, figure out backbone events on hidden.
// ie. there are 2 nested bootstrap ui elements and I'm
// trying to bind to one hidden event and to another,
// but ALL hidden events are firing all bound methods.
if ( $(e.target).hasClass('add-monitor') ) {
e.stopPropagation();
// reset addMonitorView for when modal closes
if (this.metricsViewInitialized) {
this.backToSchedule();
this.metricsViewInitialized = false;
}
Backbone.Mediator.pub('view:addmonitor:close');
}
},
/**
* AddMonitorView#modalShow()
*
**/
modalShow : function() {
Backbone.Mediator.pub('view:addmonitor:show');
},
/**
* AddMonitorView#resize()
*/
resize : function() {
return _.debounce(this.adjustModalLayout, 500);
},
/**
* AddMonitorView#saveFinish()
*
* Save the current model and close the modal dialogue.
**/
saveFinish : function() {
this._setMetrics();
$('#inputMetrics').css({
'margin-left' : '-10000px',
'left' : '-10000px',
'display' : 'block',
'position' : 'absolute'
});
$('#inputExpressions').css({
'margin-left' : '-10000px',
'left' : '-10000px',
'display' : 'block',
'position' : 'absolute'
});
this.metricsForm.submit();
},
setMetricsHelp : function() {
var $content = '';
$.ajax({
url : rearview.path + '/help/quick.html',
async : false,
success : function( response ) {
$content = response;
}
});
var $help = this.$el.find('.help');
$help.tooltip({
container : '.expressions-metrics',
trigger : 'manual',
html : true,
placement : 'right',
delay : { show : 100, hide : 200 },
title : $content
}).click(function(e) {
e.stopPropagation();
$(this).tooltip('toggle');
});
},
/**
* AddMonitorView#destructor()
*
* Try and keep memory leaks from happening by cleaning up DOM,
* nulling out references, and unbinding events. Also since this
* view sticks around, we need to reset things such as a new model
* for saving next time.
**/
destructor : function() {
this.metricsViewInitialized = false;
this.scheduleViewInitialized = false;
this.metricsMonitorFooter = null;
this.scheduleMonitorBody = null;
this.scheduleMonitorFooter = null;
var prevSiblingEl = this.$el.prev();
// cleanup events tied to template feature if init'd
if ( this.$template ) {
this.$template.off();
}
this.remove();
this.off();
// containing element in server side template is removed for garbage collection,
// so we are currently putting a new one in it's place after this process
$("<section class='add-monitor-wrap'></section>").insertAfter(prevSiblingEl);
},
/*
* PSEUDO-PRIVATE METHODS (internal)
*/
/** internal
* AddMonitorView#_closeModal()
*
* Call hide on the modal initialized to a saved DOM element.
**/
_closeModal : function() {
this.$modal.modal('hide');
},
/** internal
* AddMonitorView#_getTemplateMetaData(cb)
* - cb (Function): method to be called after response received
*
* Grab meta data for exisitng expression templates
**/
_getTemplateMetaData : function(cb) {
$.ajax({
url : rearview.path + '/monitors/index.json',
success : function( response ) {
if ( typeof cb === 'function' ) {
cb(response);
}
}
});
},
/** internal
* AddMonitorView#_initCodeMirror()
*
* Setup code entry areas on the metrics view.
**/
_initCodeMirror : function() {
var $expressions = this.$el.find('#inputExpressions')[0],
expressionsCodeSelector = '.add-monitor .expressions .CodeMirror',
$metrics = this.$el.find('#inputMetrics')[0],
metricsCodeSelector = '.add-monitor .metrics .CodeMirror',
$closeButton = this.$el.find('button.close'),
$backButton = this.$el.find('button.back');
this.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 this._toggleFullscreen(expressionsCodeSelector, this.expressionsMirror, $closeButton, $backButton);
}
}.bind(this)
});
$($expressions).data('CodeMirror', this.expressionsMirror);
this.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 this._toggleFullscreen(metricsCodeSelector, this.metricsMirror, $closeButton, $backButton);
}
}.bind(this)
});
$($metrics).data('CodeMirror', this.metricsMirror);
},
/** internal
* AddMonitorView#_initDatePicker()
*
* Set up date picker widget.
**/
_initDatePicker : function() {
this.fromDatePicker = $('#fromDatePicker').datetimepicker();
},
/** internal
* AddMonitorView#_setTemplateSelect(data)
* - data (Object): data containing metrics/expressions route
*
* Bind selection event to populate expressions field
**/
_setTemplateSelect : function(data) {
this.$template = this.$el.find('#selectTemplate');
this.$template.data('template', data);
this.$template.on('change', function() {
var data = this.$template.data('template');
// first option is nothing, just lets you know template is optional
if ( this.selectedIndex > 0 ) {
var index = this.selectedIndex - 1,
templateMeta = data[index];
if (templateMeta.path) {
$.ajax({
url : templateMeta.path,
success : function( response ) {
if (templateMeta.metrics && templateMeta.metrics.length > 0) {
this.metricsMirror.setValue(templateMeta.metrics.join('\n'));
}
this.expressionsMirror.setValue(response);
}.bind(this)
});
}
}
}.bind(this));
},
/** internal
* AddMonitorView#_setSchedule()
*
* Save scheduling data to the monitor model.
**/
_setSchedule : function() {
// grab form data & update model
this.model.set({
'userId' : this.user.get('id'),
'name' : this.$el.find('#monitorName').val(),
'description' : this.$el.find('#description').val(),
'alertKeys' : this.parseAlertKeys( this.$el.find('#pagerDuty').val() ),
'cronExpr' : this._createCronExpr()
});
},
/** internal
* AddMonitorView#_setMetrics()
*
* Save metrics data to the monitor model.
**/
_setMetrics : function() {
// grab form data & update model
this.model.set({
'userId' : this.user.get('id'),
'monitorExpr' : this.expressionsMirror.getValue(),
'metrics' : this.metricsMirror.getValue().split('\n'),
'minutes' : parseInt(this.$el.find('#minutesBack').val()),
'toDate' : this.$el.find('#fromDatePicker').val()
});
},
/** internal
* AddMonitorView#_setupScheduleView()
*
* Store reference to previous page and substitute in the scheduling form.
**/
_setupScheduleView : function() {
// store metrics body & footer
this.metricsMonitorBody = $('.add-monitor .modal-body').detach();
this.metricsMonitorFooter = $('.add-monitor .modal-footer').detach();
$('.add-monitor').append( this.scheduleMonitorBody );
$('.add-monitor').append( this.scheduleMonitorFooter );
this.scheduleView = true;
this.adjustModalLayout();
},
/** internal
* AddMonitorView#_setupMetricsView()
*
* Handles transition between scheduling and metrics views
* by checking to see if we already have initialized the view,
* otherwise initializing code entry, date picker, and graph areas.
**/
_setupMetricsView : function() {
var modalContainerEl = $('.add-monitor');
if ( !this.metricsViewInitialized ) {
this.scheduleMonitorBody = $('.add-monitor .modal-body').detach();
this.scheduleMonitorFooter = $('.add-monitor .modal-footer').detach();
// get template meta data
this._getTemplateMetaData(function(data) {
this.templar.render({
path : 'setmetrics',
el : modalContainerEl,
append : true,
data : {
monitor : {
templates : data
}
}
});
this._setTemplateSelect(data);
this._initCodeMirror();
this._initDatePicker();
this.initGraph( modalContainerEl.find('.graph')[0] );
this.setMetricsHelp();
this.setMetricsValidation();
// set that metrics view has been initialized to
// prevent initialization again
this.metricsViewInitialized = true;
this.scheduleView = false;
this.adjustModalLayout();
}.bind(this));
} else {
this.scheduleMonitorBody = $('.add-monitor .modal-body').detach();
this.scheduleMonitorFooter = $('.add-monitor .modal-footer').detach();
$('.add-monitor').append( this.metricsMonitorBody );
$('.add-monitor').append( this.metricsMonitorFooter );
}
},
/** internal
* AddMonitorView#_saveMonitor(cb)
* - cb (Function): method to be called after monitor saved.
*
* Post new model to the /jobs service route.
**/
_saveMonitor : function(cb) {
this._setMetrics();
this.model.save({
'id' : null,
'userId' : this.user.get('id')
},
{
success : function(model, response, options) {
if ( typeof cb === 'function' ) {
cb();
}
Backbone.Mediator.pub('view:addmonitor:save', {
'model' : model,
'message' : "The monitor '" + model.get('name') + "' was added.",
'attention' : 'Monitor Saved!',
'status' : 'success'
});
this.model = new MonitorModel();
}.bind(this),
error : function(model, xhr, options) {
Backbone.Mediator.pub('view:addmonitor:save', {
'model' : model,
'tryJSON' : xhr.responseText,
'attention' : 'Monitor Save Error!',
'status' : 'error'
});
}
});
}
});
return AddMonitorView;
});