Commit e28b527b authored by Leigh B Stoller's avatar Leigh B Stoller

More work on issue #334:

Switch the user version of the cluster status page to a new page that
provides a table of immediately available nodes as well as the time
series graphs. Note that the graphs are slightly different on this page
since we are shoiwing "available for allocation" instead of "available
to reserve", so we have to add the 'held' number to the free count
returned by Forecast().

The bulk of the commit is really shuffling things around a bit to make
it easier to reuse the graphs code in the new page.
parent 6b462763
......@@ -44,7 +44,11 @@ $(function ()
Delete();
});
}
LoadReservations();
// Give this a slight delay so that the spinners appear.
// Not really sure why they do not.
setTimeout(function () {
LoadReservations();
}, 100);
}
//
......@@ -64,9 +68,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}));
// Graph list(s).
html = "";
_.each(amlist, function(details, urn) {
html += graphTemplate({"details" : details,
"urn" : urn,
"showhelp" : true,
"showfullscreen" : true});
});
$('#reservation-lists .reservation-div').html(html);
// Handler for the Help button
$('#reservation-help-button').click(function (event) {
......@@ -268,24 +278,26 @@ $(function ()
// Set the cluster after clicking on a graph.
function SetCluster(nickname, urn)
{
var id = "resgraph-" + nickname;
$('#reserve-request-form [name=cluster] option[value="' + urn + '"]')
.prop("selected", "selected");
if ($('#reservation-lists :first-child').attr("id") != nickname) {
$('#' + nickname).fadeOut("fast", function () {
if ($('#reservation-lists :first-child').attr("id") != id) {
$('#' + id).fadeOut("fast", function () {
if ($(window).scrollTop()) {
$('html, body').animate({scrollTop: '0px'},
500, "swing",
function () {
$('#reservation-lists')
.prepend($('#' + nickname));
$('#' + nickname)
.prepend($('#' + id));
$('#' + id)
.fadeIn("fast");
});
}
else {
$('#reservation-lists').prepend($('#' + nickname));
$('#' + nickname).fadeIn("fast");
$('#reservation-lists').prepend($('#' + id));
$('#' + id).fadeIn("fast");
}
});
}
......@@ -299,39 +311,32 @@ $(function ()
*/
function LoadReservations()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(details, urn) {
var callback = function(json) {
console.log("LoadReservations", json);
var id = "resgraph-" + details.nickname;
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
$('#' + id + ' .resgraph-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");
// When clicking on a graph, make it the current cluster.
if (!editing) {
$('#' + details.nickname + ' .panel-body')
$('#' + id + ' .panel-body')
.click(function (event) {
SetCluster(details.nickname, urn);
});
}
ShowResGraph({"forecast" : json.value.forecast,
"selector" : details.nickname +
" .timeseries-graph-panel",
"selector" : id,
"click_callback" : SetDates});
$('#' + details.nickname + ' .resgraph-fullscreen')
$('#' + id + ' .resgraph-fullscreen')
.click(function (event) {
event.preventDefault();
// Panel title in the modal.
......@@ -559,6 +564,7 @@ $(function ()
var options = "";
var typelist = amlist[selected_cluster].typeinfo;
var nickname = amlist[selected_cluster].nickname;
var id = "resgraph-" + nickname;
_.each(typelist, function(details, type) {
var count = details.count;
......@@ -570,10 +576,10 @@ $(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");
if ($('#reservation-lists :first-child').attr("id") != id) {
$('#' + id).fadeOut("fast", function () {
$('#reservation-lists').prepend($('#' + id));
$('#' + id).fadeIn("fast");
});
}
}
......
......@@ -6,9 +6,16 @@ window.ShowResGraph = (function ()
{
'use strict';
function ProcessData(forecast) {
var index = 0;
var datums = [];
function ProcessData(args) {
var forecast = args.forecast;
// For the availablity page instead of reserve page.
var foralloc = args.foralloc;
var index = 0;
var datums = [];
if (foralloc === undefined) {
foralloc = false;
}
/*
* For the interactive tooltip to work, every has data set has to
......@@ -24,9 +31,14 @@ window.ShowResGraph = (function ()
var array = forecast[type];
if (array.length == 1) {
if (parseInt(array[0].free) == 0) {
var free = parseInt(array[0].free);
if (foralloc) {
free += parseInt(array[0].held);
}
if (free == 0) {
continue;
}
// Need two points to make a line.
array.push($.extend({}, array[0]));
array[1].t = parseInt(array[1].t) + (30 * 3600 * 24);
}
......@@ -58,6 +70,9 @@ window.ShowResGraph = (function ()
var data = array[i];
var stamp = data.t;
var free = parseInt(data.free);
if (foralloc) {
free += parseInt(data.held);
}
if (! _.has(stamps, stamp)) {
stamps[stamp] = {};
......@@ -72,7 +87,9 @@ window.ShowResGraph = (function ()
if (i > 0) {
var lastfree = parseInt(array[i - 1].free);
var prevstamp = stamp - 1;
if (foralloc) {
lastfree += parseInt(array[i - 1].held);
}
if (! _.has(stamps, prevstamp)) {
stamps[prevstamp] = {};
}
......@@ -115,6 +132,7 @@ window.ShowResGraph = (function ()
}
}
}
// The first array element now has all the types we want to graph.
var types = Object.keys(array[0].counts);
......@@ -136,6 +154,12 @@ window.ShowResGraph = (function ()
}
}
}
// For graph clarity, add an extra point at the end to give
// little space on the right hand side.
array.push($.extend({}, array[array.length - 1]));
array[array.length - 1].stamp =
parseInt(array[array.length - 1].stamp) + (3600 * 24);
//console.info(array);
/*
......@@ -167,7 +191,6 @@ window.ShowResGraph = (function ()
function CreateGraph(datums, selector, click_callback) {
var id = '#' + selector;
$(id).removeClass("hidden");
$(id + ' svg').html("");
window.nv.addGraph(function() {
......@@ -216,7 +239,7 @@ window.ShowResGraph = (function ()
return function(args) {
console.info("ShowResGraph", args);
var datums = ProcessData(args.forecast);
var datums = ProcessData(args);
if (datums == null) {
return;
}
......
......@@ -2,13 +2,14 @@ $(function ()
{
'use strict';
var template_list = ["resinfo", "reservation-graph",
var template_list = ["resinfo", "resinfo-totals", "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 totalsTemplate = _.template(templates["resinfo-totals"]);
var amlist = null;
var isadmin = false;
......@@ -25,7 +26,11 @@ $(function ()
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadReservations();
// Give this a slight delay so that the spinners appear.
// Not really sure why they do not.
setTimeout(function () {
LoadReservations();
}, 100);
}
//
......@@ -37,9 +42,23 @@ $(function ()
isadmin: isadmin,
});
$('#main-body').html(html);
// Graph list.
$('#reservation-lists')
.html(graphTemplate({"amlist": amlist, "showcontrols" : false}));
// Per clusters rows filled in with templates.
_.each(amlist, function(details, urn) {
$('#' + details.nickname + " .counts-panel")
.html(totalsTemplate({"details" : details,
"urn" : urn}));
$('#' + details.nickname + " .resgraph-panel")
.html(graphTemplate({"details" : details,
"urn" : urn,
"showhelp" : true,
"showfullscreen" : false}));
});
// 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({
......@@ -53,34 +72,55 @@ $(function ()
}
/*
* Load anonymized reservations from each am in the list and
* generate tables.
* Load reservation info from each am in the list and generate
* graphs and tables.
*/
function LoadReservations()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(details, urn) {
var callback = function(json) {
console.log("LoadReservations", json);
var graphid = 'resgraph-' + details.nickname;
var countid = details.nickname + " .counts-panel";
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
// Kill the spinners
$('#' + details.nickname + ' .resgraph-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",
ShowResGraph({"forecast" : json.value.forecast,
"selector" : graphid,
"foralloc" : true,
"click_callback" : null});
/*
* Fill in the counts panel. The first tuple in the forecast
* for each type is the immediately available node count.
*/
var forecast = json.value.forecast;
var html = "";
// Each node type
for (var type in forecast) {
// This is an array of objects.
var array = forecast[type];
var data = array[0];
var free = parseInt(data.free) + parseInt(data.held);
if (free) {
html +=
"<tr>" +
" <td>" + type + "</td>" +
" <td>" + free + "</td>" +
"</tr>";
}
}
$('#' + countid + ' tbody').html(html);
$('#' + countid + ' table').removeClass("hidden");
};
var xmlthing = sup.CallServerMethod(null, "reserve",
"ReservationInfo",
......
......@@ -376,7 +376,7 @@ echo "
<li><a href='instantiate.php'>Start Experiment</a></li>
<li><a href='manage_profile.php'>Create Experiment Profile</a></li>
<li><a href='reserve.php'>Reserve Nodes</a></li>
<li><a href='cluster-status.php'>Cluster Status</a></li>
<li><a href='resinfo.php'>Cluster Status</a></li>
";
echo " <li class='divider'></li>
<li><a href='user-dashboard.php#experiments'>
......
......@@ -54,6 +54,8 @@ if (isset($cluster)) {
SPITHEADER(1);
echo "<link rel='stylesheet'
href='css/tablesorter.css'>\n";
echo "<link rel='stylesheet'
href='css/nv.d3.css'>\n";
......@@ -96,10 +98,14 @@ REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
AddLibrary("js/resgraphs.js");
AddTemplateList(array("resinfo", "reservation-graph",
AddTemplateList(array("resinfo", "resinfo-totals", "reservation-graph",
"oops-modal", "waitwait-modal"));
SPITREQUIRE("js/resinfo.js",
"<script src='js/lib/d3.v3.js'></script>\n".
"<script src='js/lib/nv.d3.js'></script>\n");
"<script src='js/lib/nv.d3.js'></script>\n".
"<script src='js/lib/jquery.tablesorter.min.js'></script>".
"<script src='js/lib/jquery.tablesorter.widgets.min.js'></script>".
"<script src='js/lib/sugar.min.js'></script>".
"<script src='js/lib/jquery.tablesorter.parser-date.js'></script>");
SPITFOOTER();
?>
......@@ -427,7 +427,7 @@
<% } %>
</div>
<div id="cluster_status_link"><center>
<a target="_blank" href="cluster-graphs.php">
<a target="_blank" href="resinfo.php">
Check Cluster Status</a></center>
</div>
</div>
......
<style>
.panel-heading-list {
.panel-body {
padding: 2px;
}
.panel-body-dashboard {
padding: 2px;
}
.resgraph {
margin: 0px;
padding: 0px;
......@@ -16,14 +13,13 @@ svg {
}
</style>
<div>
<% _.each(amlist, function(details, urn) { %>
<div class='row hidden' id='<%- details.nickname %>'>
<div id='resgraph-<%- details.nickname %>'>
<div class='col-xs-12 col-xs-offset-0 reservation-details'>
<div class='panel panel-default hidden timeseries-graph-panel'>
<div class="panel-heading panel-heading-list">
<div class='panel panel-default'>
<div class="panel-heading">
<center>
<h5>
<% if (showcontrols) { %>
<h3 class="panel-title">
<% if (showfullscreen) { %>
<a href="#" class="resgraph-fullscreen">
<span style="margin-right: 10px;"
class='glyphicon
......@@ -31,24 +27,28 @@ svg {
</a>
<% } %>
<%- details.nickname %> Availability
<% if (showcontrols) { %>
<% if (showhelp) { %>
<a href='#' class='btn btn-xs resgraph-help-button'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
style='padding-left: 0px;'
title='Click for more info'>
<span class='glyphicon glyphicon-question-sign text-primary'
style='margin-bottom: 4px;'></span>
</a>
style='margin-bottom: 4px;'></span></a>
<% } %>
</h5>
</h3>
</center>
</div>
<div class='panel-body panel-body-dashboard'>
<div class='panel-body'>
<!-- The col setting gives us a "relative" position div -->
<div class='col-xs-12 col-xs-offset-0'
style="padding:0px;">
<div class='resgraph-size timeseries-graph resgraph'>
<div class='resgraph-size resgraph'>
<div class='resgraph-spinner'>
<center>
<img src='images/spinner.gif' /></center>
</div>
<svg class="resgraph"></svg>
</div>
</div>
......@@ -56,5 +56,4 @@ svg {
</div>
</div>
</div>
<% }) %>
</div>
......@@ -41,7 +41,7 @@
class='glyphicon glyphicon-question-sign'></span></a>
</h3>
</div>
<div class='panel-body' style="padding-top: 0px;">
<div class='panel-body' style="padding-ttop: 0px;">
<center id="unapproved-warning" class="hidden">
<span class="text-danger">
This reservation has been submitted but is unapproved!
......@@ -314,9 +314,6 @@
</div>
<div class='col-lg-5
col-md-5' id="reservation-lists">
<div id='spinner'>
<center id='spinner'><img src='images/spinner.gif' /></center><br>
</div>
<div class="reservation-div"></div>
</div>
</div>
......@@ -353,6 +350,7 @@
<div class='modal-body'>
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<br>
<p>
Use this form to request that resources be set aside for
your experiments at a future time. A reservation request
......@@ -427,6 +425,7 @@
<div class='modal-body'>
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<br>
<p>
The reservation graphs tell you how many of each node type
are available to reserve at a specific time. Use the graphs to
......@@ -463,7 +462,7 @@
<div class='modal-content'>
<div class='modal-body'>
<div class='panel panel-default'>
<div class="panel-heading panel-heading-list">
<div class="panel-heading">
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<center>
......@@ -472,7 +471,7 @@
</h5>
</center>
</div>
<div class='panel-body panel-body-dashboard'>
<div class='panel-body'>
<svg style="height: 80%; padding: 0px;"></svg>
</div>
</div>
......
<div>
<div class='panel panel-default'>
<div class="panel-heading">
<h3 class="panel-title">
<center><%- details.nickname %></center></h3>
</div>
<div class='panel-body counts-size'>
<div class='resgraph-spinner'>
<center>
<img src='images/spinner.gif' /></center>
</div>
<table class='tablesorter-green hidden'>
<thead>
<tr>
<th>Type</th>
<th>Free
<a href='#' class='btn btn-xs'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='Number of nodes that are immediately available for
use in an experiment.'
style="margin-bottom: 3px;
padding-left: 0px;
padding-right: 0px;">
<span class='glyphicon glyphicon-question-sign'></span></a></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<style>
.panel-body {
padding: 2px;
}
.table-dashboard {
margin: 1px;
}
.resgraph-size {
max-height:550px;
height:550px;
max-height:500px;
height:500px;
}
.counts-size {
max-height:475px;
overflow-y:scroll;
}
svg {
display: block;
}
</style>
<div>
<div class='row'>
<div class='col-sm-10 col-sm-offset-1'>
<div id='spinner'>
<center id='spinner'><img src='images/spinner.gif' /></center><br>
<% _.each(amlist, function(details, urn) { %>
<div class='row' id='<%- details.nickname %>'>
<div class='col-sm-3 counts-panel'>
<!-- Template goes here -->
</div>
<div class='col-sm-9 resgraph-panel'>
<!-- Template goes here -->
</div>
<div id="reservation-lists"></div>
</div>
</div>
<% }) %>
<div id='waitwait_div'></div>
<div id='oops_div'></div>
</div>
<!-- Graph Help -->
<div id='resgraph-help-modal' class='modal fade'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class='modal-body'>
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<br>
<p>
The graph tells you how many of each node type are available
for use in an experiment, at a specific time in the
future. Note that nodes available immediately might not be
available later, although in general as you move further out
in time, more nodes are available. Here are some helpful
features of the graphs:<ul>
<li> Click on a node type label to turn off that type's
line. This will rescale the other lines, sometimes
making it easier to see those other lines.</li>