Commit 3ead4566 authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

Implement time series graphs on the reserve page, as per issue #334.

parent 195b5d18
......@@ -500,14 +500,14 @@ sub DoList()
}
$tmp->{$key} = $details;
}
$list = $tmp;
$response->value()->{'reservations'} = $tmp;
}
if (defined($webtask)) {
$webtask->value($list);
$webtask->value($response->value());
$webtask->Exited(0);
}
else {
print Dumper($list);
print Dumper($response->value());
}
exit(0);
}
......
......@@ -189,7 +189,8 @@ class Aggregate
$query_result =
DBQueryFatal("select urn from apt_aggregates ".
"where disabled=0 and reservations=1 and ".
" FIND_IN_SET('$PORTAL_GENESIS', portals)");
" FIND_IN_SET('$PORTAL_GENESIS', portals)".
"order by isfederate,name");
while ($row = mysql_fetch_array($query_result)) {
$urn = $row["urn"];
......
......@@ -33,7 +33,7 @@ $(function ()
_.each(amlist, function(urn, name) {
var callback = function(json) {
console.log(json);
console.log("LoadData", json);
// Kill the spinner.
amcount--;
......@@ -45,7 +45,7 @@ $(function ()
name + ": " + json.value);
return;
}
var reservations = json.value;
var reservations = json.value.reservations;
rescount += reservations.length;
if (reservations.length == 0) {
......
......@@ -2,17 +2,17 @@ $(function ()
{
'use strict';
var template_list = ["reserve-request", "reserve-faq", "reservation-list",
"oops-modal", "waitwait-modal"];
var template_list = ["reserve-request", "reserve-faq",
"reservation-graph", "oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var mainString = templates["reserve-request"];
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var mainTemplate = _.template(mainString);
var listTemplate = _.template(templates["reservation-list"]);
var mainTemplate = _.template(templates["reserve-request"]);
var graphTemplate = _.template(templates["reservation-graph"]);
var fields = null;
var projlist = null;
var amlist = null;
var amorder = [];
var isadmin = false;
var editing = false;
var buttonstate = "check";
......@@ -44,6 +44,7 @@ $(function ()
Delete();
});
}
LoadReservations();
}
//
......@@ -63,6 +64,15 @@ $(function ()
html = aptforms.FormatFormFieldsHorizontal(html);
$('#main-body').html(html);
$('.faq-contents').html(templates["reserve-faq"]);
// Graph list.
$('#reservation-lists .reservation-div')
.html(graphTemplate({"amlist": amlist, "showcontrols" : true}));
// Handler for the Help button
$('#reservation-help-button').click(function (event) {
event.preventDefault();
sup.ShowModal('#reservation-help-modal');
});
// Handler for the FAQ link.
$('#reservation-faq-button').click(function (event) {
......@@ -75,11 +85,22 @@ $(function ()
// Set the manual link since the FAQ is not a template.
$('#reservation-manual').attr("href", window.MANUAL);
// Handler for the Reservation Graph Help button
$('.resgraph-help-button').click(function (event) {
event.preventDefault();
sup.ShowModal('#resgraph-help-modal');
});
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
placement: 'auto'
});
// Handler for cluster change to show the type list.
$('#reserve-request-form #cluster').change(function (event) {
$("#reserve-request-form #cluster option:selected").
......@@ -132,7 +153,6 @@ $(function ()
aptforms.EnableUnsavedWarning('#reserve-request-form',
modified_callback);
LoadReservations();
}
/*
......@@ -230,8 +250,51 @@ $(function ()
"Validate", checkonly_callback);
}
// Call back from the graphs to change the dates on a blank form
function SetDates(when)
{
//console.info("dates", when);
// Bump to next hour. Will be confusing at midnight.
when.setHours(when.getHours() + 1);
if (! editing) {
$("#reserve-request-form #start_day").datepicker("setDate", when);
//$("#reserve-request-form #end_day").datepicker("setDate", when);
$("#reserve-request-form [name=start_hour]").val(when.getHours());
//$("#reserve-request-form [name=end_hour]").val(when.getHours());
aptforms.MarkFormUnsaved();
}
}
// Set the cluster after clicking on a graph.
function SetCluster(nickname, urn)
{
$('#reserve-request-form [name=cluster] option[value="' + urn + '"]')
.prop("selected", "selected");
if ($('#reservation-lists :first-child').attr("id") != nickname) {
$('#' + nickname).fadeOut("fast", function () {
if ($(window).scrollTop()) {
$('html, body').animate({scrollTop: '0px'},
500, "swing",
function () {
$('#reservation-lists')
.prepend($('#' + nickname));
$('#' + nickname)
.fadeIn("fast");
});
}
else {
$('#reservation-lists').prepend($('#' + nickname));
$('#' + nickname).fadeIn("fast");
}
});
}
aptforms.MarkFormUnsaved();
}
/*
* Load anonymized reservations from each am in the list and generate tables.
* Load anonymized reservations from each am in the list and
* generate tables.
*/
function LoadReservations()
{
......@@ -239,7 +302,7 @@ $(function ()
_.each(amlist, function(details, urn) {
var callback = function(json) {
//console.log(json);
console.log("LoadReservations", json);
// Kill the spinner.
count--;
......@@ -251,48 +314,40 @@ $(function ()
details.name + ": " + json.value);
return;
}
var reservations = json.value;
if (reservations.length == 0)
return;
$('#reservation-lists #' + details.nickname)
.removeClass("hidden");
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showidx" : false,
"showproject" : false,
"showuser" : false,
"showusing" : false,
"anonymous" : true,
"name" : details.name,
// When clicking on a graph, make it the current cluster.
if (!editing) {
$('#' + details.nickname + ' .panel-body')
.click(function (event) {
SetCluster(details.nickname, urn);
});
html =
"<div class='row' id='" + details.nickname + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
}
$('#reservation-lists').prepend(html);
ShowResGraph({"forecast" : json.value.forecast,
"selector" : details.nickname +
" .timeseries-graph-panel",
"click_callback" : SetDates});
// Format dates with moment before display.
$('#' + details.nickname + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
$('#' + details.nickname + ' .resgraph-fullscreen')
.click(function (event) {
event.preventDefault();
// Panel title in the modal.
$('#resgraph-modal .cluster-name')
.html(details.nickname);
$('#resgraph-modal').on('shown.bs.modal', function() {
ShowResGraph({"forecast" : json.value.forecast,
"selector" : "resgraph-modal",
"click_callback" : SetDates});
});
$('#' + details.nickname + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
sup.ShowModal('#resgraph-modal', function () {
$('#resgraph-modal').off('shown.bs.modal');
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
placement: 'auto',
});
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
"ReservationInfo",
{"cluster" : details.nickname,
"anonymous" : 1});
xmlthing.done(callback);
......@@ -385,7 +440,7 @@ $(function ()
function PopulateReservation()
{
var callback = function(json) {
console.log(json);
console.log("PopulateReservation", json);
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", json.value);
......@@ -502,6 +557,7 @@ $(function ()
*/
var options = "";
var typelist = amlist[selected_cluster].typeinfo;
var nickname = amlist[selected_cluster].nickname;
_.each(typelist, function(details, type) {
var count = details.count;
......@@ -512,6 +568,13 @@ $(function ()
});
$("#reserve-request-form #type")
.html("<option value=''>Please Select</option>" + options);
if ($('#reservation-lists :first-child').attr("id") != nickname) {
$('#' + nickname).fadeOut("fast", function () {
$('#reservation-lists').prepend($('#' + nickname));
$('#' + nickname).fadeIn("fast");
});
}
}
// Toggle the button between check and submit.
......
//
// Reservation timeline graph.
//
$(function () {
window.ShowResGraph = (function ()
{
'use strict';
function ProcessData(forecast) {
var index = 0;
var datums = [];
/*
* For the interactive tooltip to work, every has data set has to
* have same set of x axis values (timestamps). So we are first
* going to create a hash of hashes; the keys are the time stamps
* and the value is a hash of type => free for that timestamp.
*/
var stamps = {};
// Each node type
for (var type in forecast) {
// This is an array of objects.
var array = forecast[type];
if (array.length == 1) {
if (parseInt(array[0].free) == 0) {
continue;
}
array.push($.extend({}, array[0]));
array[1].t = parseInt(array[1].t) + (30 * 3600 * 24);
}
/*
* Hmm, Gary says there can be duplicate entries for the same
* time stamp, and we want the last one. So have to splice those
* out before we process. Yuck.
*/
if (array.length > 1) {
var temp = [];
for (var i = 0; i < array.length - 1; i++) {
var data = array[i];
var nextdata = array[i + 1];
if (data.t == nextdata.t) {
continue;
}
temp.push(data);
}
// Tack on last one.
if (temp[temp.length - 1].t != array[array.length - 1].t) {
temp.push(array[array.length - 1]);
}
array = temp;
}
for (var i = 0; i < array.length; i++) {
var data = array[i];
var stamp = data.t;
var free = parseInt(data.free);
if (! _.has(stamps, stamp)) {
stamps[stamp] = {};
}
stamps[stamp][type] = free;
/*
* We want the changes to look like step functions not
* slopes, so each time we change add another entry for
* the previous second with the old free count.
*/
if (i > 0) {
var lastfree = parseInt(array[i - 1].free);
var prevstamp = stamp - 1;
if (! _.has(stamps, prevstamp)) {
stamps[prevstamp] = {};
}
stamps[prevstamp][type] = lastfree;
}
}
}
/*
* Well, this can happen; no datapoints cause no reservations
* and no experiments.
*/
if (Object.keys(stamps).length == 0) {
return null;
}
/*
* Create a sorted (by timestamp) array of the per-stamp hashes.
*/
var array = Object.keys(stamps).map(function (key) {
return {stamp : key,
counts : stamps[key]};
});
array = array.sort(function(obj1, obj2) {
// Ascending: first stamp less than the previous
return obj1.stamp - obj2.stamp;
});
/*
* Nuts, the first timestamp does not always include all the
* types. It should ... so fill those in with the first count
* we find in the ordered array.
*/
for (var i = 1; i < array.length; i++) {
var counts = array[i].counts;
// Each node type
for (var type in counts) {
if (!_.has(array[0].counts, type)) {
array[0].counts[type] = counts[type];
}
}
}
// The first array element now has all the types we want to graph.
var types = Object.keys(array[0].counts);
/*
* Okay, since each time stamp has to have data points for every
* type, go through each stamp and fill in the missing values from
* the immediately preceeding stamp. All of this to make the
* fancy tooltip work right! Sheesh.
*/
for (var i = 1; i < array.length; i++) {
var counts = array[i].counts;
// Each node type
for (var t = 0; t < types.length; t++) {
var type = types[t];
if (!_.has(counts, type)) {
counts[type] = array[i - 1].counts[type];
}
}
}
//console.info(array);
/*
* Finally, create the series data for NVD3.
*/
for (var t = 0; t < types.length; t++) {
var type = types[t];
var values = [];
datums[index++] = {
"key" : type,
"area" : 0,
"values" : values,
};
for (var i = 0; i < array.length; i++) {
var stamp = array[i].stamp;
var counts = array[i].counts;
values[i] = {
// convert seconds to milliseconds.
"x" : stamp * 1000,
"y" : counts[type],
};
}
}
return datums;
}
function CreateGraph(datums, selector, click_callback) {
var id = '#' + selector;
$(id).removeClass("hidden");
$(id + ' svg').html("");
window.nv.addGraph(function() {
var chart = window.nv.models.lineChart();
var ylabel = "Free Nodes";
chart.margin({"left":25,"right":15,"top":20,"bottom":20});
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%m/%d')(new Date(d))
});
//chart.yAxis.axisLabel(ylabel);
var intformater = d3.format(',.0f');
var formatter = function (d) {
return intformater(d);
};
chart.yAxis.tickFormat(formatter);
chart.useInteractiveGuideline(true);
d3.select(id + ' svg')
.datum(datums)
.call(chart);
// set up the tooltip to display full dates
var tsFormat = d3.time.format('%b %-d, %I:%M%p');
var tooltip = chart.interactiveLayer.tooltip;
tooltip.headerFormatter(function (d) {
return tsFormat(new Date(d));
});
/*
* When user clicks in the graph, send the timestamp back
* to the caller for changing the form.
*/
if (click_callback) {
chart.lines.dispatch.on("elementClick", function(e) {
console.info(e);
click_callback(new Date(e[0].point.x));
});
}
window.nv.utils.windowResize(chart.update);
});
}
// Pass in forecast info for a single aggregate.
return function(args) {
console.info("ShowResGraph", args);
var datums = ProcessData(args.forecast);
if (datums == null) {
return;
}
console.info("datums", datums);
CreateGraph(datums, args.selector, args.click_callback);
};
}
)();
});
$(function ()
{
'use strict';
var template_list = ["resinfo", "reservation-graph",
"oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var mainTemplate = _.template(templates["resinfo"]);
var graphTemplate = _.template(templates["reservation-graph"]);
var amlist = null;
var isadmin = false;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
isadmin = window.ISADMIN;
amlist = JSON.parse(_.unescape($('#amlist-json')[0].textContent));
GeneratePageBody();
// Now we can do this.
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadReservations();
}
//
function GeneratePageBody()
{
// Generate the template.
var html = mainTemplate({
amlist: amlist,
isadmin: isadmin,
});
$('#main-body').html(html);
// Graph list.
$('#reservation-lists')
.html(graphTemplate({"amlist": amlist, "showcontrols" : false}));
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
placement: 'auto'
});
}
/*
* Load anonymized reservations from each am in the list and
* generate tables.
*/
function LoadReservations()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(details, urn) {
var callback = function(json) {
console.log("LoadReservations", json);
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.log("Could not get reservation data for " +
details.name + ": " + json.value);
return;
}
$('#reservation-lists #' + details.nickname)
.removeClass("hidden");
ShowResGraph({"forecast" : json.value.forecast,
"selector" : details.nickname +
" .timeseries-graph-panel",
"click_callback" : null});
};
var xmlthing = sup.CallServerMethod(null, "reserve",
"ReservationInfo",
{"cluster" : details.nickname,
"anonymous" : 1});
xmlthing.done(callback);