diff options
author | Richard Wall <richard@largo> | 2011-06-12 16:42:44 +0100 |
---|---|---|
committer | Richard Wall <richard@largo> | 2011-06-12 16:42:44 +0100 |
commit | 153f51bcc0f537fab5ab059b8fb76cd34389354d (patch) | |
tree | 299a0c681f63653f0fb5a8de582f57847192530a | |
parent | 579605cb677a0345688c5421b0075b26111c4393 (diff) | |
parent | 2e0922788f4f3d34e8fe56fcdf140e59299c730c (diff) |
Various changes and re-factoring towards implementation of a customisable interface. Old and unfinished but needs merging before starting work on Jquery 1.5 compatibility work.
-rw-r--r-- | docs/examples/assets/css/style.css | 41 | ||||
-rw-r--r-- | docs/examples/index.html | 175 | ||||
-rw-r--r-- | docs/examples/jarmon_example_recipes.js | 138 | ||||
-rw-r--r-- | jarmon/jarmon.js | 761 | ||||
-rw-r--r-- | jarmon/jarmon.test.js | 178 | ||||
-rw-r--r-- | jarmonbuild/commands.py | 42 | ||||
-rw-r--r-- | test.html | 5 |
7 files changed, 1015 insertions, 325 deletions
diff --git a/docs/examples/assets/css/style.css b/docs/examples/assets/css/style.css index 561b11e..e82ec30 100644 --- a/docs/examples/assets/css/style.css +++ b/docs/examples/assets/css/style.css @@ -9,11 +9,7 @@ form div { } h2 { - padding: 0 0 0 55px; - margin: 20px auto 5px auto; font-size: 14px; - text-align: left; - clear: both; } p, li, dt, dd, td, th, div { @@ -36,6 +32,7 @@ p, li, dt, dd, td, th, div { height:200px; width: 850px; margin: 0 auto 0 auto; + clear: both; } .tickLabel { @@ -75,6 +72,11 @@ input[type=checkbox] { border: none; } +input[type=text] { + padding: 3px; + border: 1px solid #EEE; +} + .notice { border: 1px solid Green; background: #FFDDFF; @@ -85,3 +87,34 @@ input[type=checkbox] { #calroot { z-index: 2; } + +.chart-header { + width: 790px; + padding: 5px 0 5px 0; + margin: 20px auto 0 auto; + position: relative; + left: 25px; +} + +.chart-header:AFTER { + content: '' +} + +.chart-container h2{ + float: left; + margin: 0; +} + +.chart-container .chart-controls{ + float: right; + margin: 0; +} + +.tab-controls { + width: 790px; + padding: 5px 0 5px 0; + margin: 20px auto 0 auto; + text-align: right; + position: relative; + left: 25px; +} diff --git a/docs/examples/index.html b/docs/examples/index.html index 3debd18..2bec8aa 100644 --- a/docs/examples/index.html +++ b/docs/examples/index.html @@ -16,151 +16,15 @@ <script type="text/javascript" src="../../jarmon/jarmon.js"></script> <script type="text/javascript" src="jarmon_example_recipes.js"></script> <script type="text/javascript"> - // Recipes for the charts on this page - - var application_recipes = [ - { - title: 'Jarmon Webserver TCP Stats', - data: [ - ['data/tcpconns-8080-local/tcp_connections-CLOSE_WAIT.rrd', 0, 'CLOSE_WAIT', ''], - ['data/tcpconns-8080-local/tcp_connections-SYN_RECV.rrd', 0, 'SYN_RECV', ''], - ['data/tcpconns-8080-local/tcp_connections-TIME_WAIT.rrd', 0, 'TIME_WAIT', ''], - ['data/tcpconns-8080-local/tcp_connections-CLOSED.rrd', 0, 'CLOSED', ''], - ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT2.rrd', 0, 'FIN_WAIT2', ''], - ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT1.rrd', 0, 'FIN_WAIT1', ''], - ['data/tcpconns-8080-local/tcp_connections-ESTABLISHED.rrd', 0, 'ESTABLISHED', ''], - ['data/tcpconns-8080-local/tcp_connections-LAST_ACK.rrd', 0, 'LAST_ACK', ''], - ['data/tcpconns-8080-local/tcp_connections-LISTEN.rrd', 0, 'LISTEN', ''], - ['data/tcpconns-8080-local/tcp_connections-SYN_SENT.rrd', 0, 'SYN_SENT', ''], - ['data/tcpconns-8080-local/tcp_connections-CLOSING.rrd', 0, 'CLOSING', ''] - ], - options: jQuery.extend(true, {yaxis: {tickDecimals: 0}}, jarmon.Chart.BASE_OPTIONS, jarmon.Chart.STACKED_OPTIONS) - } - ]; - - - function initialiseCharts() { - /** - * Setup chart date range controls and all charts - **/ - - var p = new jarmon.Parallimiter(1); - function serialDownloader(url) { - return p.addCallable(jarmon.downloadBinary, [url]); - } - - // Extract the chart template from the page - var chartTemplate = $('.chart-container').remove(); - - function templateFactory(parentEl) { - return function() { - // The chart template must be appended to the page early, so - // that flot can calculate chart dimensions etc. - return chartTemplate.clone().appendTo(parentEl); - } - } - - var cc = new jarmon.ChartCoordinator($('.chartRangeControl')); - var t; - // Initialise tabs and update charts when tab is clicked - $(".css-tabs:first").bind('click', function(i) { - // XXX: Hack to give the tab just enough time to become visible - // so that flot can calculate chart dimensions. - window.clearTimeout(t); - t = window.setTimeout(function() { cc.update(); }, 100); - }); - - cc.charts = [].concat( - jarmon.Chart.fromRecipe( - [].concat( - jarmon.COLLECTD_RECIPES.cpu, - jarmon.COLLECTD_RECIPES.memory, - jarmon.COLLECTD_RECIPES.load), - templateFactory('.system-charts'), serialDownloader), - jarmon.Chart.fromRecipe( - jarmon.COLLECTD_RECIPES.interface, - templateFactory('.network-charts'), serialDownloader), - jarmon.Chart.fromRecipe( - jarmon.COLLECTD_RECIPES.dns, - templateFactory('.dns-charts'), serialDownloader), - jarmon.Chart.fromRecipe( - application_recipes, - templateFactory('.application-charts'), serialDownloader) - ); - - // Initialise all the charts - cc.init(); - } $(function() { - // Add dhtml calendars to the date input fields - $(".timerange_control img") - .dateinput({ - 'format': 'dd mmm yyyy 00:00:00', - 'max': +1, - 'css': {'input': 'jquerytools_date'}}) - .bind('onBeforeShow', function(e) { - var classes = $(this).attr('class').split(' '); - var currentDate, input_selector; - for(var i=0; i<=classes.length; i++) { - input_selector = '[name="' + classes[i] + '"]'; - // Look for a neighboring input element whose name matches the - // class name of this calendar - // Parse the value as a date if the returned date.getTime - // returns NaN we know it's an invalid date - // XXX: is there a better way to check for valid date? - currentDate = new Date($(this).siblings(input_selector).val()); - if(currentDate.getTime() != NaN) { - $(this).data('dateinput')._input_selector = input_selector; - $(this).data('dateinput')._initial_val = currentDate.getTime(); - $(this).data('dateinput').setValue(currentDate); - break; - } - } - }) - .bind('onHide', function(e) { - // Called after a calendar date has been chosen by the user. - - // Use the sibling selector that we generated above before opening - // the calendar - var input_selector = $(this).data('dateinput')._input_selector; - var oldStamp = $(this).data('dateinput')._initial_val; - var newDate = $(this).data('dateinput').getValue(); - // Only update the form field if the date has changed. - if(oldStamp != newDate.getTime()) { - $(this).siblings(input_selector).val( - newDate.toString().split(' ').slice(1,5).join(' ')); - // Trigger a change event which should automatically update the - // graphs and change the timerange drop down selector to - // "custom" - $(this).siblings(input_selector).trigger('change'); - } - }); - - // Avoid overlaps between the calendars - // XXX: This is a bit of hack, what if there's more than one set of calendar - // controls on a page? - $(".timerange_control img.from_custom").bind('onBeforeShow', - function() { - var otherVal = new Date( - $('.timerange_control [name="to_custom"]').val()); - - $(this).data('dateinput').setMax(otherVal); - } - ); - $(".timerange_control img.to_custom").bind('onBeforeShow', - function() { - var otherVal = new Date( - $('.timerange_control [name="from_custom"]').val()); - - $(this).data('dateinput').setMin(otherVal); - } + jarmon.buildTabbedChartUi( + $('.chart-container').remove(), + jarmon.CHART_RECIPES_COLLECTD, + $('.tabbed-chart-interface'), + jarmon.TAB_RECIPES_STANDARD, + $('.chartRangeControl') ); - - // Setup dhtml tabs - $(".css-tabs").tabs(".css-panes > div", {history: true}); - - initialiseCharts(); }); </script> </head> @@ -194,24 +58,19 @@ <div class="range-preview" title="Time range preview - click and drag to select a custom timerange" ></div> </form> - <ul class="css-tabs"> - <li><a href="#system">System</a></li> - <li><a href="#network">Network</a></li> - <li><a href="#dns">DNS</a></li> - <li><a href="#application">Application</a></li> - </ul> - <div class="css-panes charts"> - <div class="system-charts"></div> - <div class="network-charts"></div> - <div class="dns-charts"></div> - <div class="application-charts"></div> - </div> - <div class="chart-container"> + </div> + <div class="tabbed-chart-interface"></div> + <div class="chart-container"> + <div class="chart-header"> <h2 class="title"></h2> - <div class="error"></div> - <div class="chart"></div> - <div class="graph-legend"></div> + <div class="chart-controls"> + <input type="button" name="chart_edit" value="edit"> + <input type="button" name="chart_delete" value="delete"> + </div> </div> + <div class="error"></div> + <div class="chart"></div> + <div class="graph-legend"></div> </div> </body> </html> diff --git a/docs/examples/jarmon_example_recipes.js b/docs/examples/jarmon_example_recipes.js index 90b5c2e..610ecb2 100644 --- a/docs/examples/jarmon_example_recipes.js +++ b/docs/examples/jarmon_example_recipes.js @@ -9,80 +9,76 @@ if(typeof jarmon == 'undefined') { var jarmon = {}; } -jarmon.COLLECTD_RECIPES = { - 'cpu': [ - { - title: 'CPU Usage', - data: [ - ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'], - ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'], - ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'], - ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'], - ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'], - ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%'] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS, - jarmon.Chart.STACKED_OPTIONS) - } - ], +jarmon.TAB_RECIPES_STANDARD = [ + ['System', ['cpu', 'memory','load']], + ['Network', ['interface']], + ['DNS', ['dns_query_types', 'dns_return_codes']] +]; - 'memory': [ - { - title: 'Memory', - data: [ - ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'], - ['data/memory/memory-used.rrd', 0, 'Used', 'B'], - ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'], - ['data/memory/memory-free.rrd', 0, 'Free', 'B'] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS, - jarmon.Chart.STACKED_OPTIONS) - } - ], +jarmon.CHART_RECIPES_COLLECTD = { + 'cpu': { + title: 'CPU Usage', + data: [ + ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'], + ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'], + ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'], + ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'], + ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'], + ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%'] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS, + jarmon.Chart.STACKED_OPTIONS) + }, - 'dns': [ - { - title: 'DNS Query Types', - data: [ - ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'], - ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'], - ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'], - ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s'] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) - }, + 'memory': { + title: 'Memory', + data: [ + ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'], + ['data/memory/memory-used.rrd', 0, 'Used', 'B'], + ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'], + ['data/memory/memory-free.rrd', 0, 'Free', 'B'] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS, + jarmon.Chart.STACKED_OPTIONS) + }, - { - title: 'DNS Return Codes', - data: [ - ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'], - ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'], - ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s'] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) - } - ], + 'dns_query_types': { + title: 'DNS Query Types', + data: [ + ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'], + ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'], + ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'], + ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s'] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) + }, - 'load': [ - { - title: 'Load Average', - data: [ - ['data/load/load.rrd', 'shortterm', 'Short Term', ''], - ['data/load/load.rrd', 'midterm', 'Medium Term', ''], - ['data/load/load.rrd', 'longterm', 'Long Term', ''] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) - } - ], + 'dns_return_codes': { + title: 'DNS Return Codes', + data: [ + ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'], + ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'], + ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s'] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) + }, - 'interface': [ - { - title: 'Wlan0 Throughput', - data: [ - ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'], - ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s'] - ], - options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) - } - ] + 'load': { + title: 'Load Average', + data: [ + ['data/load/load.rrd', 'shortterm', 'Short Term', ''], + ['data/load/load.rrd', 'midterm', 'Medium Term', ''], + ['data/load/load.rrd', 'longterm', 'Long Term', ''] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) + }, + + 'interface': { + title: 'Wlan0 Throughput', + data: [ + ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'], + ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s'] + ], + options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS) + } }; diff --git a/jarmon/jarmon.js b/jarmon/jarmon.js index 406c4d1..1dda6a8 100644 --- a/jarmon/jarmon.js +++ b/jarmon/jarmon.js @@ -256,6 +256,18 @@ jarmon.RrdQuery.prototype.getData = function(startTimeJs, endTimeJs, dsId, cfNam 'lastUpdated': lastUpdated*1000.0}; }; + +jarmon.RrdQuery.prototype.getDSNames = function() { + /** + * Return a list of RRD Data Source names + * + * @method getDSNames + * @return {Array} An array of DS names. + **/ + return this.rrd.getDSNames(); +}; + + /** * A wrapper around RrdQuery which provides asynchronous access to the data in a * remote RRD file. @@ -275,23 +287,11 @@ jarmon.RrdQueryRemote = function(url, unit, downloader) { this._download = null; }; -jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) { - /** - * Return a Flot compatible data series asynchronously. - * - * @method getData - * @param startTime {Number} The start timestamp - * @param endTime {Number} The end timestamp - * @param dsId {Variant} identifier of the RRD datasource (string or number) - * @return {Object} A Deferred which calls back with a flot data series. - **/ - var endTimestamp = endTime/1000; - // Download the rrd if there has never been a download or if the last - // completed download had a lastUpdated timestamp less than the requested - // end time. - // Don't start another download if one is already in progress. - if(!this._download || (this._download.fired > -1 && this.lastUpdate < endTimestamp )) { +jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) { + // Download the rrd if there has never been a download and don't start + // another download if one is already in progress. + if(!this._download) { this._download = this.downloader(this.url) .addCallback( function(self, binary) { @@ -307,9 +307,10 @@ jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) { // Set up a deferred which will call getData on the local RrdQuery object // returning a flot compatible data object to the caller. var ret = new MochiKit.Async.Deferred().addCallback( - function(self, startTime, endTime, dsId, rrd) { - return new jarmon.RrdQuery(rrd, self.unit).getData(startTime, endTime, dsId); - }, this, startTime, endTime, dsId); + function(self, methodName, args, rrd) { + var rq = new jarmon.RrdQuery(rrd, self.unit); + return rq[methodName].apply(rq, args); + }, this, methodName, args); // Add a pair of callbacks to the current download which will callback the // result which we setup above. @@ -326,6 +327,35 @@ jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) { return ret; }; + +jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId, cfName) { + /** + * Return a Flot compatible data series asynchronously. + * + * @method getData + * @param startTime {Number} The start timestamp + * @param endTime {Number} The end timestamp + * @param dsId {Variant} identifier of the RRD datasource (string or number) + * @return {Object} A Deferred which calls back with a flot data series. + **/ + if(this.lastUpdate < endTime/1000) { + this._download = null; + } + return this._callRemote('getData', [startTime, endTime, dsId, cfName]); +}; + + +jarmon.RrdQueryRemote.prototype.getDSNames = function() { + /** + * Return a list of RRD Data Source names + * + * @method getDSNames + * @return {Object} A Deferred which calls back with an array of DS names. + **/ + return this._callRemote('getDSNames'); +}; + + /** * Wraps RrdQueryRemote to provide access to a different RRD DSs within a * single RrdDataSource. @@ -364,12 +394,17 @@ jarmon.RrdQueryDsProxy.prototype.getData = function(startTime, endTime) { * @param options {Object} Flot options which control how the chart should be * drawn. **/ -jarmon.Chart = function(template, options) { +jarmon.Chart = function(template, recipe, downloader) { this.template = template; - this.options = jQuery.extend(true, {yaxis: {}}, options); + this.recipe = recipe; + this.downloader = downloader; + + this.options = jQuery.extend(true, {yaxis: {}}, recipe.options); this.data = []; + this.setup(); + var self = this; @@ -380,7 +415,6 @@ jarmon.Chart = function(template, options) { self.draw(); }); - this.options['yaxis']['ticks'] = function(axis) { /* * Choose a suitable SI multiplier based on the min and max values from @@ -442,6 +476,29 @@ jarmon.Chart = function(template, options) { }; }; +jarmon.Chart.prototype.setup = function() { + this.template.find('.title').text(this.recipe['title']); + this.data = []; + var recipe = this.recipe; + var dataDict = {}; + for(var j=0; j<recipe['data'].length; j++) { + var rrd = recipe['data'][j][0]; + var ds = recipe['data'][j][1]; + // Test for integer DS index as opposed to DS name + var dsi = parseInt(ds); + if(ds.toString() == dsi.toString()) { + ds = dsi; + } + var label = recipe['data'][j][2]; + var unit = recipe['data'][j][3]; + + if(typeof dataDict[rrd] == 'undefined') { + dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, this.downloader); + } + this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)); + } +}; + jarmon.Chart.prototype.addData = function(label, db, enabled) { /** * Add details of a remote RRD data source whose data will be added to this @@ -568,13 +625,15 @@ jarmon.Chart.prototype.draw = function() { // to accomodate the color box var legend = self.template.find('.graph-legend').show(); legend.empty(); - self.template.find('.legendLabel') - .each(function(i, el) { + self.template.find('.legendLabel').each( + function(i, el) { var orig = $(el); var label = orig.text(); - var newEl = $('<div />') - .attr('class', 'legendItem') - .attr('title', 'Data series switch - click to turn this data series on or off') + var newEl = $('<div />', { + 'class': 'legendItem', + 'title': 'Data series switch - click to turn \ + this data series on or off' + }) .width(orig.width()+20) .text(label) .prepend(orig.prev().find('div div').clone().addClass('legendColorBox')) @@ -585,8 +644,8 @@ jarmon.Chart.prototype.draw = function() { if( $.inArray(label, disabled) > -1 ) { newEl.addClass('disabled'); } - }) - .remove(); + } + ).remove(); legend.append($('<div />').css('clear', 'both')); self.template.find('.legend').remove(); @@ -608,49 +667,513 @@ jarmon.Chart.prototype.draw = function() { }; -jarmon.Chart.fromRecipe = function(recipes, templateFactory, downloader) { - /** - * A static factory method to generate a list of I{Chart} from a list of - * recipes and a list of available rrd files in collectd path format. - * - * @method fromRecipe - * @param recipes {Array} A list of recipe objects. - * @param templateFactory {Function} A callable which generates an html - * template for a chart. - * @param downloader {Function} A download function which returns a Deferred - * @return {Array} A list of Chart objects - **/ +/** + * Generate a form through which to choose a data source from a remote RRD file + * + * @class jarmon.RrdChooser + * @constructor + **/ +jarmon.RrdChooser = function($tpl) { + this.$tpl = $tpl; + this.data = { + rrdUrl: '', + dsName: '', + dsLabel: '', + dsUnit:'' + }; +}; - var charts = []; - var dataDict = {}; +jarmon.RrdChooser.prototype.drawRrdUrlForm = function() { + var self = this; + this.$tpl.empty(); + + $('<form/>').append( + $('<div/>').append( + $('<p/>').text('Enter the URL of an RRD file'), + $('<label/>').append( + 'URL: ', + $('<input/>', { + type: 'text', + name: 'rrd_url', + value: this.data.rrdUrl + }) + ), + $('<input/>', {type: 'submit', value: 'download'}), + $('<div/>', {class: 'next'}) + ) + ).submit( + function(e) { + self.data.rrdUrl = this['rrd_url'].value; + $placeholder = $(this).find('.next').empty(); + new jarmon.RrdQueryRemote(self.data.rrdUrl).getDSNames().addCallback( + function($placeholder, dsNames) { + if(dsNames.length > 1) { + $('<p/>').text( + 'The RRD file contains multiple data sources. \ + Choose one:').appendTo($placeholder); + + $(dsNames).map( + function(i, el) { + return $('<input/>', { + type: 'button', + value: el + } + ).click( + function(e) { + self.data.dsName = this.value; + self.drawDsLabelForm(); + } + ); + }).appendTo($placeholder); + } else { + self.data.dsName = dsNames[0]; + self.drawDsLabelForm(); + } + }, $placeholder + ).addErrback( + function($placeholder, err) { + $('<p/>', {'class': 'error'}).text(err.toString()).appendTo($placeholder); + }, $placeholder + ); + return false; + } + ).appendTo(this.$tpl); +} + +jarmon.RrdChooser.prototype.drawDsLabelForm = function() { + var self = this; + this.$tpl.empty(); + + $('<form/>').append( + $('<p/>').text('Choose a label and unit for this data source.'), + $('<div/>').append( + $('<label/>').append( + 'Label: ', + $('<input/>', { + type: 'text', + name: 'dsLabel', + value: this.data.dslabel || this.data.dsName + }) + ) + ), + $('<div/>').append( + $('<label/>').append( + 'Unit: ', + $('<input/>', { + type: 'text', + name: 'dsUnit', + value: this.data.dsUnit + }) + ) + ), + $('<input/>', {type: 'button', value: 'back'}).click( + function(e) { + self.drawRrdUrlForm(); + } + ), + $('<input/>', {type: 'submit', value: 'save'}), + $('<div/>', {class: 'next'}) + ).submit( + function(e) { + self.data.dsLabel = this['dsLabel'].value; + self.data.dsUnit = this['dsUnit'].value; + self.drawDsSummary(); + return false; + } + ).appendTo(this.$tpl); +}; - var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match; - for(i=0; i<recipes.length; i++) { - recipe = recipes[i]; - chartData = []; +jarmon.RrdChooser.prototype.drawDsSummary = function() { + var self = this; + this.$tpl.empty(); + + jQuery.each(this.data, function(i, el) { + $('<p/>').append( + $('<strong/>').text(i), + [': ', el].join('') + ).appendTo(self.$tpl); + }); - for(j=0; j<recipe['data'].length; j++) { - rrd = recipe['data'][j][0]; - ds = recipe['data'][j][1]; - label = recipe['data'][j][2]; - unit = recipe['data'][j][3]; - if(typeof dataDict[rrd] == 'undefined') { - dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, downloader); + this.$tpl.append( + $('<input/>', {type: 'button', value: 'back'}).click( + function(e) { + self.drawDsLabelForm(); } - chartData.push([label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)]); - } - if(chartData.length > 0) { - template = templateFactory(); - template.find('.title').text(recipe['title']); - c = new jarmon.Chart(template, recipe['options']); - for(j=0; j<chartData.length; j++) { - c.addData.apply(c, chartData[j]); + ), + $('<input/>', {type: 'button', value: 'finish'}) + ); +}; + + +jarmon.ChartEditor = function($tpl, chart) { + this.$tpl = $tpl; + this.chart = chart; + + $('form', this.$tpl[0]).live( + 'submit', + {self: this}, + function(e) { + var self = e.data.self; + self.chart.recipe.title = this['title'].value; + self.chart.recipe.data = $(this).find('.datasources tbody tr').map( + function(i, el) { + return $(el).find('input[type=text]').map( + function(i, el) { + return el.value; + } + ); + } + ); + self.chart.setup(); + self.chart.draw(); + return false; + } + ); + + $('form', this.$tpl[0]).live( + 'reset', + {self: this}, + function(e) { + var self = e.data.self; + self.draw(); + return false; + } + ); + + $('form input[name=datasource_delete]', this.$tpl[0]).live( + 'click', + function(e) { + $(this).closest('tr').remove(); + } + ); + + $('form input[name=datasource_add]', this.$tpl[0]).live( + 'click', + {self: this}, + function(e) { + var self = e.data.self; + self._addDatasourceRow( + self._extractRowValues( + $(this).closest('tr') + ) + ); + $(this).closest('tr').find('input[type=text]').val(''); + } + ); +}; + +jarmon.ChartEditor.prototype.draw = function() { + var self = this; + this.$tpl.empty(); + + $('<form/>').append( + $('<div/>').append( + $('<label/>').append( + 'Title: ', + $('<input/>', { + type: 'text', + name: 'title', + value: this.chart.recipe.title + }) + ) + ), + $('<fieldset/>').append( + $('<legend/>').text('Data Sources'), + $('<table/>', {'class': 'datasources'}).append( + $('<thead/>').append( + $('<tr/>').append( + $('<th/>').text('RRD File'), + $('<th/>').text('DS Name'), + $('<th/>').text('DS Label'), + $('<th/>').text('DS Unit'), + $('<th/>') + ) + ), + $('<tfoot/>').append( + $('<tr/>').append( + $('<td/>').append( + $('<input/>', {type: 'text'}) + ), + $('<td/>').append( + $('<input/>', {type: 'text'}) + ), + $('<td/>').append( + $('<input/>', {type: 'text'}) + ), + $('<td/>').append( + $('<input/>', {type: 'text'}) + ), + $('<td/>').append( + $('<input/>', { + type: 'button', + value: 'add', + name: 'datasource_add' + }) + ) + ) + ), + $('<tbody/>') + ) + ), + $('<input/>', {type: 'submit', value: 'save'}), + $('<input/>', {type: 'reset', value: 'reset'}) + ).appendTo(this.$tpl); + + for(var i=0; i<this.chart.recipe.data.length; i++) { + this._addDatasourceRow(this.chart.recipe.data[i]); + } +}; + + +jarmon.ChartEditor.prototype._extractRowValues = function($row) { + return $row.find('input[type=text]').map( + function(i, el) { + return el.value; + } + ) +}; + + +jarmon.ChartEditor.prototype._addDatasourceRow = function(record) { + $('<tr/>').append( + $('<td/>').append( + $('<input/>', {type: 'text', value: record[0]}) + ), + $('<td/>').append( + $('<input/>', {type: 'text', value: record[1]}) + ), + $('<td/>').append( + $('<input/>', {type: 'text', value: record[2]}) + ), + $('<td/>').append( + $('<input/>', {type: 'text', value: record[3]}) + ), + $('<td/>').append( + $('<input/>', { + type: 'button', + value: 'delete', + name: 'datasource_delete' + }) + ) + ).appendTo(this.$tpl.find('.datasources tbody')); +}; + + +jarmon.TabbedInterface = function($tpl, recipe) { + this.$tpl = $tpl; + this.recipe = recipe; + this.placeholders = []; + + this.$tabBar = $('<ul/>', {'class': 'css-tabs'}).appendTo($tpl); + + // Icon and hidden input box for adding new tabs. See event handlers below. + this.$newTabControls = $('<li/>', { + 'class': 'newTabControls', + 'title': 'Add new tab' + }).append( + $('<img/>', {src: 'assets/icons/next.gif'}), + $('<input/>', {'type': 'text'}).hide() + ).appendTo(this.$tabBar); + + this.$tabPanels = $('<div/>', {'class': 'css-panes charts'}).appendTo($tpl); + var tabName, $tabPanel, placeNames; + for(var i=0; i<recipe.length; i++) { + tabName = recipe[i][0]; + placeNames = recipe[i][1]; + + $tabPanel = this.newTab(tabName); + + for(var j=0; j<placeNames.length; j++) { + this.placeholders.push([ + placeNames[j], $('<div/>').appendTo($tabPanel)]); + } + } + + this.setup(); + + // Show the new tab name input box when the user clicks the new tab icon + $('ul.css-tabs > li.newTabControls > img', $tpl[0]).live( + 'click', + function(e) { + $(this).hide().siblings().show().focus(); + } + ); + + // When the "new" tab input loses focus, use its value to create a new + // tab. + // XXX: Due to event bubbling, this event seems to be triggered twice, but + // only when the input is forcefully blurred by the "keypress" event handler + // below. To prevent two tabs, we blank the input field value. Tried + // preventing event bubbling, but there seems to be some subtle difference + // with the use of jquery live event handlers. + $('ul.css-tabs > li.newTabControls > input', $tpl[0]).live( + 'blur', + {self: this}, + function(e) { + var self = e.data.self; + var value = this.value; + this.value = ''; + $(this).hide().siblings().show(); + if(value) { + self.newTab(value); + self.setup(); + self.$tabBar.data("tabs").click(value); + } + } + ); + + // Unfocus the input element when return key is pressed. Triggers a + // blur event which then replaces the input with a tab + $('ul.css-tabs > li > input', $tpl[0]).live( + 'keypress', + function(e) { + if(e.which == 13) { + $(this).blur(); } - charts.push(c); } + ); + + // Show tab name input box when tab is double clicked. + $('ul.css-tabs > li > a', $tpl[0]).live( + 'dblclick', + {self: this}, + function(e) { + var $originalLink = $(this); + var $input = $('<input/>', { + 'value': $originalLink.text(), + 'name': 'editTabTitle', + 'type': 'text' + }) + $originalLink.replaceWith($input); + $input.focus(); + } + ); + + // Handle the updating of the tab when its name is edited. + $('ul.css-tabs > li > input[name=editTabTitle]', $tpl[0]).live( + 'blur', + {self: this}, + function(e) { + var self = e.data.self; + $(this).replaceWith( + $('<a/>', { + href: ['#', this.value].join('') + }).text(this.value) + ) + self.setup(); + self.$tabBar.data("tabs").click(this.value); + } + ); + + $('input[name=add_new_chart]', $tpl[0]).live( + 'click', + {self: this}, + function(e) { + console.log(e); + } + ); +}; + +jarmon.TabbedInterface.prototype.newTab = function(tabName) { + // Add a tab + $('<li/>').append( + $('<a/>', {href: ['#', tabName].join('')}).text(tabName) + ).appendTo(this.$tabBar); + var $placeholder = $('<div/>'); + // Add tab panel + $('<div/>').append( + $placeholder, + $('<div/>', {'class': 'tab-controls'}).append( + $('<input/>', { + type: 'button', + value: 'Add new chart', + name: 'add_new_chart' + }) + ) + ).appendTo(this.$tabPanels); + + return $placeholder; +}; + +jarmon.TabbedInterface.prototype.setup = function() { + this.$newTabControls.remove(); + // Destroy then re-initialise the jquerytools tabs plugin + var api = this.$tabBar.data("tabs"); + if(api) { + api.destroy(); + } + this.$tabBar.tabs(this.$tabPanels.children('div')); + this.$newTabControls.appendTo(this.$tabBar); +}; + + +jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes, + $tabTemplate, tabRecipes, + $controlPanelTemplate) { + /** + * Setup chart date range controls and all charts + **/ + var p = new jarmon.Parallimiter(1); + function serialDownloader(url) { + return p.addCallable(jarmon.downloadBinary, [url]); } - return charts; + + var ti = new jarmon.TabbedInterface($tabTemplate, tabRecipes); + + var charts = jQuery.map( + ti.placeholders, + function(el, i) { + var chart = new jarmon.Chart( + $chartTemplate.clone().appendTo(el[1]), + chartRecipes[el[0]], + serialDownloader + ); + + $('input[name=chart_edit]', el[1][0]).live( + 'click', + {chart: chart}, + function(e) { + var chart = e.data.chart; + new jarmon.ChartEditor( + chart.template.find('.graph-legend'), chart).draw(); + } + ); + + $('input[name=chart_delete]', el[1][0]).live( + 'click', + {chart: chart}, + function(e) { + var chart = e.data.chart; + chart.template.remove(); + } + ); + + return chart; + } + ); + + var cc = new jarmon.ChartCoordinator($controlPanelTemplate, charts); + // Update charts when tab is clicked + ti.$tpl.find(".css-tabs:first").bind( + 'click', + {'cc': cc}, + function(e) { + var cc = e.data.cc; + // XXX: Hack to give the tab just enough time to become visible + // so that flot can calculate chart dimensions. + window.clearTimeout(cc.t); + cc.t = window.setTimeout( + function() { + cc.update(); + }, 100); + } + ); + + // Initialise all the charts + cc.init(); + + return [charts, ti, cc]; }; @@ -720,10 +1243,10 @@ jarmon.timeRangeShortcuts = [ * @param ui {Object} A one element jQuery containing an input form and * placeholders for the timeline and for the series of charts. **/ -jarmon.ChartCoordinator = function(ui) { +jarmon.ChartCoordinator = function(ui, charts) { var self = this; this.ui = ui; - this.charts = []; + this.charts = charts; // Style and configuration of the range timeline this.rangePreviewOptions = { @@ -817,11 +1340,107 @@ jarmon.ChartCoordinator = function(ui) { // When a selection is made on the range timeline, or any of my charts // redraw all the charts. - this.ui.bind("plotselected", function(event, ranges) { - self.ui.find('[name="from_standard"]').val('custom'); - self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to); - self.update(); - }); + $(document).bind( + 'plotselected', + {self: this}, + function(e, ranges) { + var self = e.data.self; + var eventSourceIsMine = false; + + // plotselected event may be from my range selector chart or + if( self.ui.has(e.target) ) { + eventSourceIsMine = true; + } else { + // ...it may come from one of the charts under my supervision + for(var i=0; i<self.charts.length; i++) { + if(self.charts[i].template.has(e.target).length > 0) { + eventSourceIsMine = true; + break; + } + } + } + + if(eventSourceIsMine) { + // Update the prepared time range select box to value "custom" + self.ui.find('[name="from_standard"]').val('custom'); + + // Update all my charts + self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to); + self.update(); + } + } + ); + + // Add dhtml calendars to the date input fields + this.ui.find(".timerange_control img") + .dateinput({ + 'format': 'dd mmm yyyy 00:00:00', + 'max': +1, + 'css': {'input': 'jquerytools_date'}}) + .bind('onBeforeShow', function(e) { + var classes = $(this).attr('class').split(' '); + var currentDate, input_selector; + for(var i=0; i<=classes.length; i++) { + input_selector = '[name="' + classes[i] + '"]'; + // Look for a neighboring input element whose name matches the + // class name of this calendar + // Parse the value as a date if the returned date.getTime + // returns NaN we know it's an invalid date + // XXX: is there a better way to check for valid date? + currentDate = new Date($(this).siblings(input_selector).val()); + if(currentDate.getTime() != NaN) { + $(this).data('dateinput')._input_selector = input_selector; + $(this).data('dateinput')._initial_val = currentDate.getTime(); + $(this).data('dateinput').setValue(currentDate); + break; + } + } + }) + .bind('onHide', function(e) { + // Called after a calendar date has been chosen by the user. + + // Use the sibling selector that we generated above before opening + // the calendar + var input_selector = $(this).data('dateinput')._input_selector; + var oldStamp = $(this).data('dateinput')._initial_val; + var newDate = $(this).data('dateinput').getValue(); + // Only update the form field if the date has changed. + if(oldStamp != newDate.getTime()) { + $(this).siblings(input_selector).val( + newDate.toString().split(' ').slice(1,5).join(' ')); + // Trigger a change event which should automatically update the + // graphs and change the timerange drop down selector to + // "custom" + $(this).siblings(input_selector).trigger('change'); + } + }); + + // Avoid overlaps between the calendars + // XXX: This is a bit of hack, what if there's more than one set of calendar + // controls on a page? + this.ui.find(".timerange_control img.from_custom").bind( + 'onBeforeShow', + {self: this}, + function(e) { + var self = e.data.self; + var otherVal = new Date( + self.ui.find('.timerange_control [name="to_custom"]').val()); + + $(this).data('dateinput').setMax(otherVal); + } + ); + this.ui.find(".timerange_control img.to_custom").bind( + 'onBeforeShow', + {self: this}, + function(e) { + var self = e.data.self; + var otherVal = new Date( + self.ui.find('.timerange_control [name="from_custom"]').val()); + + $(this).data('dateinput').setMin(otherVal); + } + ); + }; diff --git a/jarmon/jarmon.test.js b/jarmon/jarmon.test.js index c838023..f5b7dae 100644 --- a/jarmon/jarmon.test.js +++ b/jarmon/jarmon.test.js @@ -277,6 +277,105 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { Y.Test.Runner.add(new Y.Test.Case({ + name: "jarmon.RrdQueryRemote", + + setUp: function() { + this.rq = new jarmon.RrdQueryRemote('build/test.rrd', ''); + }, + + test_getDataTimeRangeOverlapError: function () { + /** + * The starttime must be less than the endtime + **/ + this.rq.getData(1, 0).addBoth( + function(self, res) { + self.resume(function() { + Y.Assert.isInstanceOf(RangeError, res); + }); + }, this); + this.wait(); + }, + + + test_getDataUnknownCfError: function () { + /** + * Error is raised if the rrd file doesn't contain an RRA with the + * requested consolidation function (CF) + **/ + this.rq.getData(RRD_STARTTIME, RRD_ENDTIME, 0, 'FOO').addBoth( + function(self, res) { + self.resume(function() { + Y.Assert.isInstanceOf(TypeError, res); + }); + }, this); + this.wait(); + }, + + + test_getData: function () { + /** + * The generated rrd file should have values 0-9 at 300s intervals + * starting at 1980-01-01 00:00:00 + * Result should include a data points with times > starttime and + * <= endTime + **/ + this.rq.getData(RRD_STARTTIME + (RRD_STEP+1) * 1000, + RRD_ENDTIME - (RRD_STEP-1) * 1000).addBoth( + function(self, data) { + self.resume(function() { + // We request data starting 1 STEP +1s after the RRD file + // first val and ending 1 STEP -1s before the RRD last val + // ie one step within the RRD file, but 1s away from the + // step boundary to test the quantisation of the + // requested time range. + + // so we expect two less rows than the total rows in the + // file. + Y.Assert.areEqual(RRD_RRAROWS-2, data.data.length); + + // The value of the first returned row should be the + // second value in the RRD file (starts at value 0) + Y.Assert.areEqual(1, data.data[0][1]); + + // The value of the last returned row should be the + // 10 value in the RRD file (starts at value 0) + Y.Assert.areEqual(10, data.data[data.data.length-1][1]); + + // The timestamp of the first returned row should be + // exactly one step after the start of the RRD file + Y.Assert.areEqual( + RRD_STARTTIME+RRD_STEP*1000, data.data[0][0]); + + // RRD_ENDTIME is on a step boundary and is therfore + // actually the start time of a new row + // So when we ask for endTime = RRD_ENDTIME-STEP-1 we + // actually get data up to the 2nd to last RRD row. + Y.Assert.areEqual( + RRD_ENDTIME-RRD_STEP*1000*2, + data.data[data.data.length-1][0]); + }); + }, this); + this.wait(); + }, + + test_getDataUnknownValues: function () { + /** + * If the requested time range is outside the range of the RRD file + * we should not get any values back + **/ + this.rq.getData(RRD_ENDTIME, RRD_ENDTIME+1000).addBoth( + function(self, data) { + self.resume(function() { + Y.Assert.areEqual(0, data.data.length); + }); + }, this); + this.wait(); + } + + })); + + + Y.Test.Runner.add(new Y.Test.Case({ name: "jarmon.Chart", test_draw: function () { @@ -301,6 +400,85 @@ YUI({ logInclude: { TestRunner: true } }).use('console', 'test', function(Y) { })); + Y.Test.Runner.add(new Y.Test.Case({ + name: "jarmon.RrdChooser", + + setUp: function() { + this.$tpl = $('<div/>').appendTo($('body')) + var c = new jarmon.RrdChooser(this.$tpl); + c.drawRrdUrlForm(); + }, + + test_drawInitialForm: function () { + /** + * Test that the initial config form contains an rrd form field + **/ + Y.Assert.areEqual( + this.$tpl.find('form input[name=rrd_url]').size(), 1); + }, + + test_drawUrlErrorMessage: function () { + /** + * Test that submitting the form with an incorrect url results in + * an error message + **/ + var self = this; + this.$tpl.find('form input[name=rrd_url]').val('Foo/Bar').submit(); + this.wait( + function() { + Y.Assert.areEqual(self.$tpl.find('.error').size(), 1); + }, 1000 + ); + }, + + test_drawUrlListDatasources: function () { + /** + * Test that submitting the form with an correct rrd url results in + * list of further DS label fields + **/ + var self = this; + this.$tpl.find('form input[name=rrd_url]').val('build/test.rrd').submit(); + this.wait( + function() { + Y.Assert.areEqual(self.$tpl.find('input[name=rrd_ds_label]').size(), 1); + }, 1000 + ); + }, + })); + + + Y.Test.Runner.add(new Y.Test.Case({ + name: "jarmon.ChartEditor", + + setUp: function() { + this.$tpl = $('<div/>').appendTo($('body')) + var c = new jarmon.ChartEditor( + this.$tpl, + { + title: 'Foo', + datasources: [ + ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'], + ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'], + ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'], + ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'], + ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'], + ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%'] + ] + } + ); + c.draw(); + }, + + test_drawInitialForm: function () { + /** + * Test that the initial config form contains an rrd form field + **/ + Y.Assert.areEqual( + this.$tpl.find('form input[name=rrd_url]').size(), 1); + } + })); + + //initialize the console var yconsole = new Y.Console({ newestOnTop: false, diff --git a/jarmonbuild/commands.py b/jarmonbuild/commands.py index 9ababd9..0be6cf0 100644 --- a/jarmonbuild/commands.py +++ b/jarmonbuild/commands.py @@ -8,9 +8,7 @@ import logging import os import shutil import sys -import time -from datetime import datetime from optparse import OptionParser from subprocess import check_call, PIPE from tempfile import gettempdir @@ -20,8 +18,8 @@ from zipfile import ZipFile, ZIP_DEFLATED import pkg_resources -JARMON_PROJECT_TITLE='Jarmon' -JARMON_PROJECT_URL='http://www.launchpad.net/jarmon' +JARMON_PROJECT_TITLE = 'Jarmon' +JARMON_PROJECT_URL = 'http://www.launchpad.net/jarmon' YUIDOC_URL = 'http://yuilibrary.com/downloads/yuidoc/yuidoc_1.0.0b1.zip' YUIDOC_MD5 = 'cd5545d2dec8f7afe3d18e793538162c' @@ -91,21 +89,27 @@ class BuildApidocsCommand(BuildCommand): yuizip_path = os.path.join(tmpdir, os.path.basename(YUIDOC_URL)) if os.path.exists(yuizip_path): self.log.debug('Using cached YUI doc') - def producer(): + + def producer_local(): yield open(yuizip_path).read() + + producer = producer_local else: self.log.debug('Downloading YUI Doc') - def producer(): + + def producer_remote(): with open(yuizip_path, 'w') as yuizip: download = urlopen(YUIDOC_URL) while True: - bytes = download.read(1024*10) + bytes = download.read(1024 * 10) if not bytes: break else: yuizip.write(bytes) yield bytes + producer = producer_remote + checksum = hashlib.md5() for bytes in producer(): checksum.update(bytes) @@ -114,7 +118,8 @@ class BuildApidocsCommand(BuildCommand): if actual_md5 != YUIDOC_MD5: raise BuildError( 'YUI Doc checksum error. File: %s, ' - 'Expected: %s, Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5)) + 'Expected: %s, ' + 'Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5)) else: self.log.debug('YUI Doc checksum verified') @@ -145,7 +150,7 @@ class BuildApidocsCommand(BuildCommand): workingbranch_dir, 'jarmonbuild', 'yuidoc_template'),), '--version=%s' % (buildversion,), '--project=%s' % (JARMON_PROJECT_TITLE,), - '--projecturl=%s' % (JARMON_PROJECT_URL,) + '--projecturl=%s' % (JARMON_PROJECT_URL,), ), stdout=PIPE, stderr=PIPE,) shutil.rmtree(yuidoc_dir) @@ -181,21 +186,21 @@ class BuildReleaseCommand(BuildCommand): if status != 0: raise BuildError('bzr export failure. Status: %r' % (status,)) - self.log.debug('Record the branch version') from bzrlib.branch import Branch from bzrlib.version_info_formats import format_python v = format_python.PythonVersionInfoBuilder( Branch.open(workingbranch_dir)) - versionfile_path = os.path.join(build_dir, 'jarmonbuild', '_version.py') + + versionfile_path = os.path.join( + build_dir, 'jarmonbuild', '_version.py') + with open(versionfile_path, 'w') as f: v.generate(f) - self.log.debug('Generate apidocs') BuildApidocsCommand().main([buildversion]) - self.log.debug('Generate archive') archive_root = 'jarmon-%s' % (buildversion,) prefix_len = len(build_dir) + 1 @@ -205,7 +210,7 @@ class BuildReleaseCommand(BuildCommand): for file in files: z.write( os.path.join(root, file), - os.path.join(archive_root, root[prefix_len:], file) + os.path.join(archive_root, root[prefix_len:], file), ) finally: z.close() @@ -233,14 +238,17 @@ class BuildTestDataCommand(BuildCommand): rows = 12 step = 10 - dss.append(DataSource(dsName='speed', dsType='GAUGE', heartbeat=2*step)) + dss.append( + DataSource(dsName='speed', dsType='GAUGE', heartbeat=2 * step)) rras.append(RRA(cf='AVERAGE', xff=0.5, steps=1, rows=rows)) rras.append(RRA(cf='AVERAGE', xff=0.5, steps=12, rows=rows)) my_rrd = RRD(filename, ds=dss, rra=rras, start=start, step=step) my_rrd.create() - for i, t in enumerate(range(start+step, start+step+(rows*step), step)): - self.log.debug('DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t))) + for i, t in enumerate( + range(start + step, start + step + (rows * step), step)): + self.log.debug( + 'DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t))) my_rrd.bufferValue(t, i) # Add further data 1 second later to demonstrate that the rrd @@ -3,10 +3,7 @@ <head> <meta charset="utf-8"> <title>Jarmon Unit Test Runner</title> - <link rel="stylesheet" type="text/css" - href="http://developer.yahoo.com/yui/3/assets/yui.css"/> - <link rel="stylesheet" type="text/css" - href="http://yui.yahooapis.com/3.1.1/build/cssfonts/fonts-min.css"/> + <style type='text/css'> .chart { width: 500px; |