summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Wall <richard@largo>2011-06-12 16:42:44 +0100
committerRichard Wall <richard@largo>2011-06-12 16:42:44 +0100
commit153f51bcc0f537fab5ab059b8fb76cd34389354d (patch)
tree299a0c681f63653f0fb5a8de582f57847192530a
parent579605cb677a0345688c5421b0075b26111c4393 (diff)
parent2e0922788f4f3d34e8fe56fcdf140e59299c730c (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.css41
-rw-r--r--docs/examples/index.html175
-rw-r--r--docs/examples/jarmon_example_recipes.js138
-rw-r--r--jarmon/jarmon.js761
-rw-r--r--jarmon/jarmon.test.js178
-rw-r--r--jarmonbuild/commands.py42
-rw-r--r--test.html5
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
diff --git a/test.html b/test.html
index e8508f6..5d0baf4 100644
--- a/test.html
+++ b/test.html
@@ -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;