Commit 26f77c59 authored by Leigh B Stoller's avatar Leigh B Stoller

Web UI changes for reservations, for backend/RPC changes in 039f27b1:

1. Show current reservations on the admin extend page (if any) for the
   user who started the experiment.

2. Add a reservation history page, to see historical reservations for a
   user.

3. Changes to the reservation listing page.

4. And then the main content of this commit is that for the pages above,
   show the experiment usage history for the project and the user who
   created the reservation. This takes the form of a time line of
   allocation changes so that we can graph node usage against the
   reservation bounds, to show graphically how well utilized the
   reservation is.
parent 8266ae51
<?php <?php
# #
# Copyright (c) 2000-2017 University of Utah and the Flux Group. # Copyright (c) 2000-2018 University of Utah and the Flux Group.
# #
# {{{EMULAB-LICENSE # {{{EMULAB-LICENSE
# #
...@@ -118,7 +118,7 @@ REQUIRE_UNDERSCORE(); ...@@ -118,7 +118,7 @@ REQUIRE_UNDERSCORE();
REQUIRE_SUP(); REQUIRE_SUP();
REQUIRE_MOMENT(); REQUIRE_MOMENT();
REQUIRE_IDLEGRAPHS(); REQUIRE_IDLEGRAPHS();
AddLibrary("js/resgraphs.js");
SPITREQUIRE("js/adminextend.js", SPITREQUIRE("js/adminextend.js",
"<script src='js/lib/d3.v3.js'></script>". "<script src='js/lib/d3.v3.js'></script>".
"<script src='js/lib/nv.d3.js'></script>". "<script src='js/lib/nv.d3.js'></script>".
...@@ -145,6 +145,6 @@ if (count($extensions)) { ...@@ -145,6 +145,6 @@ if (count($extensions)) {
echo "</script>\n"; echo "</script>\n";
} }
AddTemplateList(array("adminextend", "oops-modal", "waitwait-modal", "admin-history", "admin-firstrow", "admin-secondrow", "admin-utilization", "admin-summary")); AddTemplateList(array("adminextend", "oops-modal", "waitwait-modal", "admin-history", "admin-firstrow", "admin-secondrow", "admin-utilization", "admin-summary", "reservation-list"));
SPITFOOTER(); SPITFOOTER();
?> ?>
...@@ -2,7 +2,7 @@ $(function () ...@@ -2,7 +2,7 @@ $(function ()
{ {
'use strict'; 'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['adminextend', 'waitwait-modal', 'oops-modal', 'admin-history', 'admin-firstrow', 'admin-secondrow', 'admin-utilization', 'admin-summary']); var templates = APT_OPTIONS.fetchTemplateList(['adminextend', 'waitwait-modal', 'oops-modal', 'admin-history', 'admin-firstrow', 'admin-secondrow', 'admin-utilization', 'admin-summary', "reservation-list"]);
var mainString = templates['adminextend']; var mainString = templates['adminextend'];
var waitwaitString = templates['waitwait-modal']; var waitwaitString = templates['waitwait-modal'];
var oopsString = templates['oops-modal']; var oopsString = templates['oops-modal'];
...@@ -16,6 +16,7 @@ $(function () ...@@ -16,6 +16,7 @@ $(function ()
var firstrowTemplate = null; var firstrowTemplate = null;
var secondrowTemplate = null; var secondrowTemplate = null;
var extensionsTemplate = null; var extensionsTemplate = null;
var listTemplate = null;
var maxextension = null; var maxextension = null;
var GENIRESPONSE_REFUSED = 7; var GENIRESPONSE_REFUSED = 7;
...@@ -30,6 +31,7 @@ $(function () ...@@ -30,6 +31,7 @@ $(function ()
firstrowTemplate = _.template(firstrowString); firstrowTemplate = _.template(firstrowString);
secondrowTemplate = _.template(secondrowString); secondrowTemplate = _.template(secondrowString);
extensionsTemplate = _.template(historyString); extensionsTemplate = _.template(historyString);
listTemplate = _.template(templates["reservation-list"]);
LoadFirstRow(); LoadFirstRow();
// Need to serialize this stuff cause of locking in the backend. // Need to serialize this stuff cause of locking in the backend.
...@@ -564,6 +566,7 @@ $(function () ...@@ -564,6 +566,7 @@ $(function ()
$('#max-extension-warning').addClass("hidden"); $('#max-extension-warning').addClass("hidden");
$('#max-extension-warning .max-extension-date').html(""); $('#max-extension-warning .max-extension-date').html("");
}); });
console.info("DoMaxExtension: ", json);
if (json.code) { if (json.code) {
console.info("Failed to get max extension", json); console.info("Failed to get max extension", json);
...@@ -593,6 +596,80 @@ $(function () ...@@ -593,6 +596,80 @@ $(function ()
} }
return; return;
} }
if (Object.keys(json.value.reservations).length) {
// Convert to just a single list of reservations.
var reservations = {};
_.each(json.value.reservations, function(reslist, urn) {
_.each(reslist, function(details, uuid) {
reservations[uuid] = details;
});
});
if (Object.keys(reservations).length) {
console.info("reservations", reservations);
var html = listTemplate({
"reservations" : reservations,
"showcontrols" : false,
"showproject" : false,
"showactivity" : false,
"showuser" : true,
"showusing" : true,
"showstatus" : true,
"name" : "extend",
"isadmin" : true,
"error" : null,
});
$('#reservations-row .panel-body').html(html);
// Show the proper status now, we might change it later.
_.each(reservations, function(value, uuid) {
var id = '#reservations-row ' +
' tr[data-uuid="' + uuid + '"] ';
if (value.cancel) {
$(id + " .status-column .status-canceled")
.removeClass("hidden");
}
else if (value.approved) {
$(id + " .status-column .status-approved")
.removeClass("hidden");
}
else {
$(id + " .status-column .status-pending")
.removeClass("hidden");
}
if (value.approved &&
_.has(value, 'history') && value.history.length) {
$(id + " .resgraph-button").removeClass("invisible");
// Bind usage history graph.
$(id + ' .resgraph-button').click(function() {
DrawHistoryGraph(value);
return false;
});
}
});
$('#reservations-row .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
$('#reservations-row .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment(date)
.format("MMM D, YYYY h:mm A"));
}
});
// This activates the popover subsystem.
$('#reservations-row [data-toggle="popover"]').popover({
placement: 'auto',
container: 'body',
});
$('#reservations-row').removeClass("hidden");
}
}
// Save for checking the extension input field. // Save for checking the extension input field.
maxextension = moment(json.value.maxextension); maxextension = moment(json.value.maxextension);
...@@ -765,6 +842,30 @@ $(function () ...@@ -765,6 +842,30 @@ $(function ()
sup.ShowModal('#metrics-modal'); sup.ShowModal('#metrics-modal');
} }
// Draw the history bar graph.
function DrawHistoryGraph(details)
{
// Setup a handler to draw the large version graph in the modal.
$('#resusage-graph-modal').on('shown.bs.modal', function() {
window.DrawResHistoryGraph({"details" : details,
"graphid" : '#resusage-graph-modal',
"xaxislabel" : true});
});
// Make sure nothing left behind before we show it.
$('#resusage-graph-modal svg').html("");
// Gack, this stuff gets left behind.
d3.selectAll('.nvtooltip').remove();
// Say something informative in the panel header.
$('#resusage-graph-modal .resusage-graph-details')
.html("(" + details.nodes + " " + details.type + " nodes)");
sup.ShowModal('#resusage-graph-modal', function () {
// Need to unbind the hook above.
$('#resusage-graph-modal').off('shown.bs.modal');
});
}
// Helper. // Helper.
function decodejson(id) { function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent)); return JSON.parse(_.unescape($(id)[0].textContent));
......
...@@ -2,11 +2,14 @@ $(function () ...@@ -2,11 +2,14 @@ $(function ()
{ {
'use strict'; 'use strict';
var template_list = ["reservation-list", "resusage-list", var template_list = ["list-reservations", "reservation-list",
"oops-modal", "confirm-modal", "waitwait-modal"]; "confirm-modal", "resusage-list", "resusage-graph",
"oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list); var templates = APT_OPTIONS.fetchTemplateList(template_list);
var mainTemplate = _.template(templates["list-reservations"]);
var listTemplate = _.template(templates["reservation-list"]); var listTemplate = _.template(templates["reservation-list"]);
var usageTemplate = _.template(templates["resusage-list"]); var usageTemplate = _.template(templates["resusage-list"]);
var graphTemplate = _.template(templates["resusage-graph"]);
var confirmString = templates["confirm-modal"]; var confirmString = templates["confirm-modal"];
var oopsString = templates["oops-modal"]; var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"]; var waitwaitString = templates["waitwait-modal"];
...@@ -17,10 +20,11 @@ $(function () ...@@ -17,10 +20,11 @@ $(function ()
window.APT_OPTIONS.initialize(sup); window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json'); amlist = decodejson('#amlist-json');
$('#main-body').html(mainTemplate({"amlist" : amlist}));
$('#oops_div').html(oopsString); $('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString); $('#waitwait_div').html(waitwaitString);
$('#confirm_div').html(confirmString); $('#confirm_div').html(confirmString);
LoadData(); LoadData();
} }
...@@ -47,40 +51,36 @@ $(function () ...@@ -47,40 +51,36 @@ $(function ()
if (json.code) { if (json.code) {
console.log("Could not get reservation data for " + console.log("Could not get reservation data for " +
name + ": " + json.value); name + ": " + json.value);
error = json.value; $('#' + name + " .res-error").html(json.value);
$('#' + name + " .res-error").removeClass("hidden");
$('#' + name).removeClass("hidden");
return;
} }
else { reservations = json.value.reservations;
reservations = json.value.reservations; rescount += reservations.length;
rescount += reservations.length;
if (reservations.length == 0) { if (reservations.length == 0) {
if (amcount == 0 && rescount == 0) { if (amcount == 0 && rescount == 0) {
// No reservations at all, show the message. // No reservations at all, show the message.
$('#noreservations').removeClass("hidden"); $('#noreservations').removeClass("hidden");
}
return;
} }
return;
} }
// Generate the main template. // Generate the main template.
var html = listTemplate({ var html = listTemplate({
"reservations" : reservations, "reservations" : reservations,
"showidx" : true, "showcontrols" : true,
"showproject" : true, "showproject" : true,
"showactivity" : true,
"showuser" : true, "showuser" : true,
"showusing" : true, "showusing" : true,
"anonymous" : false, "showstatus" : true,
"name" : name, "name" : name,
"isadmin" : window.ISADMIN, "isadmin" : window.ISADMIN,
"error" : error, "error" : error,
}); });
html = $('#' + name + " .panel-body").html(html);
"<div class='row' id='" + name + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#main-body').prepend(html);
// On error, no need for the rest of this. // On error, no need for the rest of this.
if (error) if (error)
...@@ -89,21 +89,21 @@ $(function () ...@@ -89,21 +89,21 @@ $(function ()
// Show the proper status now, we might change it later. // Show the proper status now, we might change it later.
_.each(reservations, function(value, uuid) { _.each(reservations, function(value, uuid) {
var id = '#' + name + var id = '#' + name +
' tr[data-uuid="' + uuid + '"] .status-column'; ' tr[data-uuid="' + uuid + '"] ';
if (value.cancel) { if (value.cancel) {
$(id + " .status-canceled").removeClass("hidden"); $(id + " .status-column .status-canceled")
.removeClass("hidden");
} }
else if (value.approved) { else if (value.approved) {
$(id + " .status-approved").removeClass("hidden"); $(id + " .status-column .status-approved")
.removeClass("hidden");
} }
else { else {
$(id + " .status-pending").removeClass("hidden"); $(id + " .status-column .status-pending")
.removeClass("hidden");
if (window.ISADMIN) { if (window.ISADMIN) {
id = '#' + name +
' tr[data-uuid="' + uuid + '"] ';
// Bind a deny handler, // Bind a deny handler,
$(id + ' .deny-button').click(function() { $(id + ' .deny-button').click(function() {
DenyReservation($(this).closest('tr')); DenyReservation($(this).closest('tr'));
...@@ -118,6 +118,16 @@ $(function () ...@@ -118,6 +118,16 @@ $(function ()
$(id + ' .approve-button').removeClass("invisible"); $(id + ' .approve-button').removeClass("invisible");
} }
} }
if (value.approved &&
_.has(value, 'history') && value.history.length) {
$(id + " .resgraph-button").removeClass("invisible");
// Bind usage history graph.
$(id + ' .resgraph-button').click(function() {
DrawHistoryGraph(value);
return false;
});
}
}); });
// Format dates with moment before display. // Format dates with moment before display.
...@@ -203,7 +213,8 @@ $(function () ...@@ -203,7 +213,8 @@ $(function ()
event.preventDefault(); event.preventDefault();
sup.ShowModal('#' + "resusage-modal-" + uuid); sup.ShowModal('#' + "resusage-modal-" + uuid);
}); });
$(this).find(".resusage-button").removeClass("hidden"); $(this).find(".resusage-button")
.removeClass("invisible");
// Format dates in the modal with moment before display. // Format dates in the modal with moment before display.
$('#resusage-modal-' + uuid + ' .format-date').each(function() { $('#resusage-modal-' + uuid + ' .format-date').each(function() {
...@@ -224,6 +235,7 @@ $(function () ...@@ -224,6 +235,7 @@ $(function ()
placement: 'auto', placement: 'auto',
container: 'body', container: 'body',
}); });
$('#' + name).removeClass("hidden");
} }
var xmlthing = sup.CallServerMethod(null, "reserve", var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations", "ListReservations",
...@@ -241,7 +253,7 @@ $(function () ...@@ -241,7 +253,7 @@ $(function ()
var pid = $(row).attr('data-pid'); var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster'); var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table"); var table = $(row).closest("table");
// Callback for the delete request. // Callback for the delete request.
var callback = function (json) { var callback = function (json) {
sup.HideModal('#waitwait-modal'); sup.HideModal('#waitwait-modal');
...@@ -368,7 +380,7 @@ $(function () ...@@ -368,7 +380,7 @@ $(function ()
// This is what we are deleting. // This is what we are deleting.
var uuid = $(row).attr('data-uuid'); var uuid = $(row).attr('data-uuid');
var pid = $(row).attr('data-pid'); var pid = $(row).attr('data-pid');
var uid_idx = $(row).attr('data-creator_idx'); var uid_idx = $(row).attr('data-uid_idx');
var cluster = $(row).attr('data-cluster'); var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table"); var table = $(row).closest("table");
var warning = (which == "warn" ? 1 : 0); var warning = (which == "warn" ? 1 : 0);
...@@ -471,7 +483,31 @@ $(function () ...@@ -471,7 +483,31 @@ $(function ()
}) })
sup.ShowModal("#cancel-cancel-modal"); sup.ShowModal("#cancel-cancel-modal");
} }
// Draw the history bar graph.
function DrawHistoryGraph(details)
{
// Setup a handler to draw the large version graph in the modal.
$('#resusage-graph-modal').on('shown.bs.modal', function() {
window.DrawResHistoryGraph({"details" : details,
"graphid" : '#resusage-graph-modal',
"xaxislabel" : true});
});
// Make sure nothing left behind before we show it.
$('#resusage-graph-modal svg').html("");
// Gack, this stuff gets left behind.
d3.selectAll('.nvtooltip').remove();
$('#resusage-graph-modal .resusage-graph-details')
.html("(" + details.nodes + " " + details.type + " nodes)");
sup.ShowModal('#resusage-graph-modal', function () {
// Need to unbind the hook above.
$('#resusage-graph-modal').off('shown.bs.modal');
});
}
// Helper. // Helper.
function decodejson(id) { function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent)); return JSON.parse(_.unescape($(id)[0].textContent));
......
$(function ()
{
'use strict';
var template_list = ["reservation-history", "reservation-list",
"resusage-graph", "oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var mainTemplate = _.template(templates["reservation-history"]);
var listTemplate = _.template(templates["reservation-list"]);
var graphTemplate = _.template(templates["resusage-graph"]);
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var amlist = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
$('#main-body').html(mainTemplate({"amlist" : amlist,
"uid" : window.UID}));
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadData();
}
/*
* Load reservations from each am in the list and generate a table.
*/
function LoadData()
{
var amcount = Object.keys(amlist).length;
var rescount = 0;
_.each(amlist, function(urn, name) {
var callback = function(json) {
var error = null;
var reservations = null;
console.log("LoadData", json);
// Kill the spinner.
amcount--;
if (amcount <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.log("Could not get reservation history for " +
name + ": " + json.value);
$('#' + name + " .res-error").html(json.value);
$('#' + name + " .res-error").removeClass("hidden");
$('#' + name).removeClass("hidden");
return;
}
reservations = json.value.reservations;
rescount += reservations.length;
if (reservations.length == 0) {
if (amcount == 0 && rescount == 0) {
// No reservations at all, show the message.
$('#noreservations').removeClass("hidden");
}
return;
}
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showcontrols" : false,
"showproject" : true,
"showactivity" : false,
"showuser" : false,
"showusing" : false,
"showstatus" : false,
"name" : name,
"isadmin" : window.ISADMIN,
"error" : error,
});
$('#' + name + " .panel-body").html(html);
// On error, no need for the rest of this.
if (error)
return;
// Check for history.
_.each(reservations, function(value, uuid) {
var id = '#' + name +
' tr[data-uuid="' + uuid + '"] ';
if (_.has(value, 'history') && value.history.length) {
$(id + " .resgraph-button").removeClass("invisible");
// Bind usage history graph.
$(id + ' .resgraph-button').click(function() {
DrawHistoryGraph(value);
return false;
});
}
});
// Format dates with moment before display.
$('#' + name + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment(date).format("lll"));
}
});
$('#' + name + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
delay: {"hide" : 250, "show" : 250},
placement: 'auto',
});
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
placement: 'auto',
container: 'body',
});
$('#' + name).removeClass("hidden");
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ReservationHistory",
{"cluster" : name,
"uid" : window.UID});
xmlthing.done(callback);
});
}
// Draw the history bar graph.
function DrawHistoryGraph(details)
{
// Setup a handler to draw the large version graph in the modal.
$('#resusage-graph-modal').on('shown.bs.modal', function() {
window.DrawResHistoryGraph({"details" : details,
"graphid" : '#resusage-graph-modal',
"xaxislabel" : true});
});
// Make sure nothing left behind before we show it.
$('#resusage-graph-modal svg').html("");
// Gack, this stuff gets left behind.