define([ 'jquery', 'underscore', 'backbone', 'handlebars', 'highcharts', 'underscore-string', 'highcharts-theme', 'xdate', 'backbone-mediator' ], function( $, _, Backbone, Handlebars, HighChart ){ var BaseView = Backbone.View.extend({ assetRoot : 'public/', // just a reference for asset url config intervalDiviser : 4, // used for dynamically reducing x-axis label markers infiniteTimes : [], // save infinite data in our graphData object infiniteLabels : [], infiniteTimesInt : [], /** * BaseView#addHelpers() **/ addHelpers : function() { Handlebars.registerHelper('nl2br', function(text) { var nl2br = (text + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '
' + '$2'); return new Handlebars.SafeString(nl2br); }); }, /** * BaseView#destroySubscriptions() **/ destroySubscriptions : function () { for (sub in this.subscriptions) { Backbone.Mediator.unsubscribe(sub, this[this.subscriptions[sub]], this); } }, /** * BaseView#drawAsInfinite(key) -> Boolean|Null **/ drawAsInfinite : function (key) { var match = key.match(/^drawAsInfinite\((.*)\)$/); if (match) { return _.last(match); } else { return null; } }, /** * BaseView#formatGraphData(graphData) -> Object * - graphData (Object): payload containing graph data, output, status * from the /monitors route * * This method splits out coordinate data into 2 linear arrays keyed to * the specific metrics. This makes it easier for high charts to consume * the data and graph it. The old data key/value pairs are stored in the * graphite key under the metrics queue created by the metrics field in * the setmetrics.hbs template. **/ formatGraphData : function( graphData ) { var formattedGraphData = _.reduce(_.keys(graphData), (function(accum, key) { var graphiteDataRaw = graphData[key]; graphData[key] = {}; graphData[key].graphite = graphiteDataRaw; graphData[key].xInt = []; graphData[key].x = []; graphData[key].y = []; _.each(graphData[key].graphite, function(dataPair) { // NOTE : non standardized date data coming from database, sometimes 13 digits sometimes 10 // in this case 10 graphData[key].x.push( new XDate(dataPair[0] * 1000).toUTCString("HH:mm") ); graphData[key].y.push( dataPair[1] ); graphData[key].xInt.push( dataPair[0] ); }); return graphData; }), []); return formattedGraphData; }, focusFirst : function() { this.$el.find('input[type=text]:first').focus(); }, /** * BaseView#initGraph(el) -> Object * - el (Element): container element for graph * * Just sets up a high chart for any given element container. **/ initGraph : function(el) { var self = this; self.chart = new Highcharts.Chart({ exporting : { buttons : { exportButton : { enabled : false }, printButton : { enabled : false } } }, credits: { enabled : false }, chart: { zoomType : 'x', spacingRight : 24, animation : true, renderTo : el, type : 'line', borderRadius : 3, backgroundColor : { stops: [ [0, 'rgb(16, 16, 16)'], [1, 'rgb(16, 16, 16)'] ] }, }, title : { text : null }, xAxis : { minTickInterval : this, title : { text : null } }, yAxis : { title : { text : null } }, plotOptions: { column: { animation: true }, series: { animation : false, marker: { enabled: false } } }, tooltip : { animation : true } }); return self.chart; }, /** * BaseView#keyOrder(hash) -> Array * - hash : a hash of values * * Just getting a list of alphabetically sorted hash keys. **/ keyOrder : function(hash) { var self = this, keys = []; keys = _.keys(hash); keys = _.sortBy(keys, function (key) { return key.toLowerCase(); }); return keys; }, /** * BaseView#_renderGraphData(chart, graphData) * - chart (Object): HighChart object * - graphData (Object): formatted graph data object * * Adds the right data sets to the given chart. **/ renderGraphData : function(chart, graphData) { var self = this, renderData = graphData, times = [], colors = [ // creating an array to a series color scheme to stay consistant between updates from the server '#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92' ]; // clear out previous data series while (chart.series.length > 0) { chart.series[0].remove(true); } // remove all existing plot lines chart.xAxis[0].removePlotLine('plotLine'); self.infiniteTimes = []; self.infiniteTimesInt = []; self.infiniteLabels = []; _.each(graphData, function(dataSet, key) { if ( self.drawAsInfinite(key) ) { var drawAsInfiniteTimes = [], drawAsInfiniteIntTimes = []; // check for time plots where defineAsInfinite is enabled for (var i = 0; i < dataSet.xInt.length; i++) { if (dataSet.y[i]) { drawAsInfiniteTimes.push(dataSet.x[i]); drawAsInfiniteIntTimes.push(dataSet.xInt[i]); } } self.infiniteTimes.push(drawAsInfiniteTimes); self.infiniteTimesInt.push(drawAsInfiniteIntTimes); self.infiniteLabels.push(self.drawAsInfinite(key)); delete graphData[key]; } }); // go by alpha sorted key order for graph rendering color/label consistancy var sortedGraphDataKeys = self.keyOrder(graphData); // index of array for position placement for drawAsInfinite method for (var i = 0; i < sortedGraphDataKeys.length; i++) { var dataSet = graphData[sortedGraphDataKeys[i]]; chart.xAxis[0].setCategories( dataSet.x ); chart.xAxis[0].options.tickInterval = parseInt((dataSet.x.length / self.intervalDiviser), 10); chart.xAxis[0].setExtremes(null, null); chart.addSeries({ name : sortedGraphDataKeys[i], data : dataSet.y, animation : false, color : colors[i] }); // keep track of x time intervals for each group combined // in order to have a complete list of x data points times = _.union(times, dataSet.xInt); } // check to see that there are any infinite lines to plot if ( self.infiniteTimes.length ) { var infinitesLength = self.infiniteTimes.length - 1; for (var i = infinitesLength; i >= 0; i--) { var infiniteTimesPlots = self.infiniteTimesInt[i]; if ( infiniteTimesPlots.length ) { for (var j = 0; j < infiniteTimesPlots.length; j++) { // check the position in the time array to place the plot var timeIndex = _.sortedIndex(times, infiniteTimesPlots[j]); if (!_.isNull(timeIndex)) { chart.xAxis[0].addPlotLine({ id : 'plotLine', value : timeIndex, color : 'green', dashStyle : 'shortdash', width : 2, label : { text : self.infiniteLabels[i], style : { color : '#ccc' } } }); } } } } } }, /** * BaseView#formatServerDateTime(value) -> Date Obj * - value (String|Int): Can't rely always rely on date format being given to the front end * - utc (Boolean): UTC time format flag * * This is a centralized method simply for detecting the date format from the service architecture, * where some systems may use a 13 digit timestamp and others a 10, etc. This method should make * things easier to change wholesale in the future if needed. **/ formatServerDateTime : function(value, utc) { var serverDateLength = value.toString().length; return ( serverDateLength == 10 ) ? new XDate( parseInt(value, 10) * 1000, utc ) : new XDate( parseInt(value, 10), utc ); }, /** * BaseView#parseAlertKeys(value) -> Array * - value (String): INput string to parse * * This method is used to parse a string delimited by spaces, commas, or newlines **/ parseAlertKeys : function(value) { var parsed = []; var fragments = value.split('\n'); _.each(fragments, function(frag) { parsed.push(_.str.words(frag, /([ \,])/)); }); // assure a one dimensional array parsed = _.flatten(parsed); // filter out non valid alert keys parsed = _.filter(parsed, function(str) { // no blank values and no commas return !_.str.isBlank(str) && ( str !== ',' ); }); return parsed; }, resizeModal : function($modal, size, ignoreHeight) { var width = $(window).width(), height = $(window).height(), widthMultiplier = 0.6, heightMultiplier = 0.8; switch(size) { case 'small' : widthMultipler = 0.3; break; case 'medium' : widthMultipler = 0.6; break; case 'large' : widthMultipler = 0.9; break; default : widthMultipler = 0.6; } $modal.css({ width : width * widthMultipler, marginLeft : -(width * (widthMultipler / 2)) }); if (!ignoreHeight) { $modal.css({ height : height * heightMultiplier }); $modal.find('.modal-body').css({ maxHeight : (height * heightMultiplier) - 140, height : (height * heightMultiplier) - 149 }); } $modal.each(function () { $(this).css({ marginTop: -($(this).height() / 2), }); }); // add a class to add other styling to $modal.addClass('modal-' + size); return { height : $modal.height(), width : $modal.width(), body : { height : $modal.find('.modal-body').height(), width : $modal.find('.modal-body').width() } }; }, /** internal * BaseView#_processRadioControl(collection[, defaults]) -> String * - collection (Object): Element collection * - defaults (String): if empty collection, return defaulted string * * Simply used to process bootstrap button groups and find what is * currently selected. Buttons require data-value attributes to set * selected value. **/ _processRadioControl : function(collection, defaults) { var activeValues = _.map(collection, function(el) { return $(el).attr('data-value'); }); if ( activeValues.length > 0 ) { return activeValues.join(','); } else if ( !defaults ) { return ''; } else { return defaults; } }, /** internal * BaseView#_createCronExpr() -> String * * Used to set up cron expression from the schedulemonitor.hbs view **/ _createCronExpr : function() { var self = this; // cron syntax eg. 0 * * * * ? return [ 0, // self.$el.find('#inputSeconds').val() self.$el.find('#inputMinutes').val(), self.$el.find('#inputHours').val(), self.$el.find('#inputDays').val(), self._processRadioControl(self.$el.find('.month-picker button.active'), '*'), self._processRadioControl(self.$el.find('.day-picker button.active'), '?') ].join(' '); }, /** * BaseView#showOverlay(el, text, class) -> Element * * **/ showOverlay : function(el, text, className) { var self = this; el = $(el); if ( !self.overlay ) { var className = ( className ) ? className : ''; self.overlay = $("

"); el.append(self.overlay); } self.overlay.find('h1').html(text); self.overlay.show(); }, /** * BaseView#hideOverlay() -> Element * * **/ hideOverlay : function() { var self = this; self.overlay.hide(); }, /** internal * AddMonitorView#_toggleFullscreen(selector, mirrorRef) * - selector (String|DOM Object): Code mirror selector * - mirrorRef (Object): Code mirror object reference * * Set code mirror styles to expand to their container. **/ _toggleFullscreen : function(selector, mirrorRef, closeButton, backButton) { var self = this, editorEl = $(selector); if (!editorEl.hasClass('fullscreen')) { if ( closeButton && backButton ) { closeButton.hide(); backButton.show(); } editorEl.data('beforeFullscreen', { height : editorEl.height(), width : editorEl.width() }); editorEl.addClass('fullscreen'); editorEl.width('100%'); editorEl.height('100%'); mirrorRef.setSize('100%', '100%'); mirrorRef.refresh(); } else { if ( closeButton && backButton ) { closeButton.show(); backButton.hide(); } editorEl.removeClass('fullscreen'); editorEl.height(editorEl.data('beforeFullscreen').height); editorEl.width(editorEl.data('beforeFullscreen').width); mirrorRef.refresh(); } }, _validateMirror : function(mirror) { var self = this, wrapper = $( mirror.getWrapperElement() ); if(self._isNonblank(mirror.getValue())) { wrapper.removeClass('error'); return true; } else { wrapper.addClass('error'); return false; } }, _isNonblank : function(string) { var isNonblankRegex = /\S/; return String (string).search(isNonblankRegex) != -1; }, getPositions : function(el) { var self = this, pos = $(el).offset(), width = $(el).width(), height = $(el).height(); return [ [ pos.left, pos.left + width ], [ pos.top, pos.top + height ] ]; }, comparePositions : function( p1, p2 ) { var self = this, r1 = p1[0] < p2[0] ? p1 : p2, r2 = p1[0] < p2[0] ? p2 : p1; return ( r1[1] > r2[0] || r1[0] === r2[0] ); }, overlaps : function( el1, el2 ) { var self = this, pos1 = self.getPositions( el1 ), pos2 = self.getPositions( el2 ); return self.comparePositions(pos1[0], pos2[0]) && self.comparePositions(pos1[1], pos2[1]); } }); BaseView.prototype.destructor = function() { this.destroySubscriptions(); if (this.onDestruct) { this.onDestruct(); } this.off(); this.remove(); this.$el.empty(); } return BaseView; });