Commit 26f77c59 authored by Leigh Stoller's avatar Leigh 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
#
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -118,7 +118,7 @@ REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
REQUIRE_IDLEGRAPHS();
AddLibrary("js/resgraphs.js");
SPITREQUIRE("js/adminextend.js",
"<script src='js/lib/d3.v3.js'></script>".
"<script src='js/lib/nv.d3.js'></script>".
......@@ -145,6 +145,6 @@ if (count($extensions)) {
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();
?>
......@@ -2,7 +2,7 @@ $(function ()
{
'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 waitwaitString = templates['waitwait-modal'];
var oopsString = templates['oops-modal'];
......@@ -16,6 +16,7 @@ $(function ()
var firstrowTemplate = null;
var secondrowTemplate = null;
var extensionsTemplate = null;
var listTemplate = null;
var maxextension = null;
var GENIRESPONSE_REFUSED = 7;
......@@ -30,6 +31,7 @@ $(function ()
firstrowTemplate = _.template(firstrowString);
secondrowTemplate = _.template(secondrowString);
extensionsTemplate = _.template(historyString);
listTemplate = _.template(templates["reservation-list"]);
LoadFirstRow();
// Need to serialize this stuff cause of locking in the backend.
......@@ -564,6 +566,7 @@ $(function ()
$('#max-extension-warning').addClass("hidden");
$('#max-extension-warning .max-extension-date').html("");
});
console.info("DoMaxExtension: ", json);
if (json.code) {
console.info("Failed to get max extension", json);
......@@ -593,6 +596,80 @@ $(function ()
}
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.
maxextension = moment(json.value.maxextension);
......@@ -765,6 +842,30 @@ $(function ()
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.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
......
......@@ -2,11 +2,14 @@ $(function ()
{
'use strict';
var template_list = ["reservation-list", "resusage-list",
"oops-modal", "confirm-modal", "waitwait-modal"];
var template_list = ["list-reservations", "reservation-list",
"confirm-modal", "resusage-list", "resusage-graph",
"oops-modal", "waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var mainTemplate = _.template(templates["list-reservations"]);
var listTemplate = _.template(templates["reservation-list"]);
var usageTemplate = _.template(templates["resusage-list"]);
var graphTemplate = _.template(templates["resusage-graph"]);
var confirmString = templates["confirm-modal"];
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
......@@ -17,10 +20,11 @@ $(function ()
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
$('#main-body').html(mainTemplate({"amlist" : amlist}));
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
$('#confirm_div').html(confirmString);
LoadData();
}
......@@ -47,40 +51,36 @@ $(function ()
if (json.code) {
console.log("Could not get reservation data for " +
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;
rescount += reservations.length;
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;
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,
"showidx" : true,
"showcontrols" : true,
"showproject" : true,
"showactivity" : true,
"showuser" : true,
"showusing" : true,
"anonymous" : false,
"showstatus" : true,
"name" : name,
"isadmin" : window.ISADMIN,
"error" : error,
});
html =
"<div class='row' id='" + name + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#main-body').prepend(html);
$('#' + name + " .panel-body").html(html);
// On error, no need for the rest of this.
if (error)
......@@ -89,21 +89,21 @@ $(function ()
// Show the proper status now, we might change it later.
_.each(reservations, function(value, uuid) {
var id = '#' + name +
' tr[data-uuid="' + uuid + '"] .status-column';
' tr[data-uuid="' + uuid + '"] ';
if (value.cancel) {
$(id + " .status-canceled").removeClass("hidden");
$(id + " .status-column .status-canceled")
.removeClass("hidden");
}
else if (value.approved) {
$(id + " .status-approved").removeClass("hidden");
$(id + " .status-column .status-approved")
.removeClass("hidden");
}
else {
$(id + " .status-pending").removeClass("hidden");
$(id + " .status-column .status-pending")
.removeClass("hidden");
if (window.ISADMIN) {
id = '#' + name +
' tr[data-uuid="' + uuid + '"] ';
// Bind a deny handler,
$(id + ' .deny-button').click(function() {
DenyReservation($(this).closest('tr'));
......@@ -118,6 +118,16 @@ $(function ()
$(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.
......@@ -203,7 +213,8 @@ $(function ()
event.preventDefault();
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.
$('#resusage-modal-' + uuid + ' .format-date').each(function() {
......@@ -224,6 +235,7 @@ $(function ()
placement: 'auto',
container: 'body',
});
$('#' + name).removeClass("hidden");
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
......@@ -241,7 +253,7 @@ $(function ()
var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table");
// Callback for the delete request.
var callback = function (json) {
sup.HideModal('#waitwait-modal');
......@@ -368,7 +380,7 @@ $(function ()
// This is what we are deleting.
var uuid = $(row).attr('data-uuid');
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 table = $(row).closest("table");
var warning = (which == "warn" ? 1 : 0);
......@@ -471,7 +483,31 @@ $(function ()
})
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.
function decodejson(id) {
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.
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.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
......@@ -3,12 +3,14 @@ $(function ()
'use strict';
var template_list = ["reserve-request", "reserve-faq",
"reservation-graph", "oops-modal", "waitwait-modal"];
"reservation-graph", "oops-modal", "waitwait-modal",
"resusage-graph"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var mainTemplate = _.template(templates["reserve-request"]);
var graphTemplate = _.template(templates["reservation-graph"]);
var usageTemplate = _.template(templates["resusage-graph"]);
var fields = null;
var projlist = null;
var amlist = null;
......@@ -707,8 +709,8 @@ $(function ()
var details = json.value;
$('#reserve-request-form [name=uuid]').val(details.uuid);
$('#reserve-request-form [name=pid]').val(details.pid);
$('#reserve-request-form [name=count]').val(details.count);
$('#reserve-request-form [name=cluster]').val(details.cluster);
$('#reserve-request-form [name=count]').val(details.nodes);
$('#reserve-request-form [name=cluster]').val(details.cluster_urn);
$('#reserve-request-form [name=cluster_id]').val(details.cluster_id);
$('#reserve-request-form [name=type]').val(details.type);
$('#reserve-request-form [name=reason]').val(details.notes);
......@@ -748,14 +750,14 @@ $(function ()
$('#unapproved-warning').removeClass("hidden");
}
// Local user gets a link.
if (_.has(details, 'creator_idx')) {
if (_.has(details, 'uid_idx')) {
$('#reserve-requestor').html(
"<a target=_blank href='user-dashboard.php?user=" +
details.creator_idx + "'>" +
details.creator_uid + "</a>");
details.uid_idx + "'>" +
details.uid + "</a>");
}
else {
$('#reserve-requestor').html(details.creator_uid);
$('#reserve-requestor').html(details.uid);
}
/*
......@@ -776,6 +778,9 @@ $(function ()
// Now we can load the graph since we know the project.
LoadReservations(details.pid);
// Add append history graph under the reservation graph.
DrawHistoryGraph(details);
};
sup.ShowWaitWait();
var xmlthing = sup.CallServerMethod(null, "reserve",
......@@ -874,5 +879,36 @@ $(function ()
}
buttonstate = which;
}
// Draw the history bar graph.
function DrawHistoryGraph(details)
{
if (!_.has(details, 'history') || !details.history.length) {
return;
}
var graphid = "history-graph";
var html = usageTemplate({"graphid" : graphid,
"showfullscreen" : true});
$('#reservation-lists').append(html);
window.DrawResHistoryGraph({"details" : details,
"graphid" : '#' + graphid});
// Setup a handler to draw the large version graph in the modal.
$('#resusage-modal').on('shown.bs.modal', function() {
window.DrawResHistoryGraph({"details" : details,
"graphid" : '#resusage-modal',
"xaxislabel" : true});
});
// When modal shows, we draw.
$('#' + graphid + ' .resusage-fullscreen').click(function (event) {
// Make sure nothing left behind.
$('#resusage-modal svg').html("");
sup.ShowModal('#resusage-modal', function () {
// Need to unbind the hook above.
$('#resusage-modal').off('shown.bs.modal');
});
});
}
$(document).ready(initialize);
});
......@@ -396,4 +396,88 @@ window.ShowResGraph = (function ()
};
}
)();
window.DrawResHistoryGraph = (function ()
{
return function(args)
{
var details = args.details;
var history = details.history;
var graphid = args.graphid;
var xlabel = false;
var uvalues = [];
var pvalues = [];
if (_.has(args, "xaxislabel")) {
xlabel = args.xaxislabel;
}
for (var i = 0; i < history.length; i++) {
var record = history[i];
var stamp = parseInt(record.t) * 1000;
var reserved = record.reserved;
var allocated = record.allocated;
// If this is before or after the reservation, reserved will
// be empty. Skip it.
if (Array.isArray(reserved)) {
continue;
}
var pcount = allocated[details.pid][details.type];
// Watch for nothing allocated by the user at this time stamp
var ucount = 0;
if (_.has(allocated, details.uid)) {
ucount = allocated[details.uid][details.type];
}
uvalues.push({"x" : stamp, "y" : parseInt(ucount)});
pvalues.push({"x" : stamp, "y" : parseInt(pcount)});
}
var data = [{"key" : "User", "values" : uvalues},
{"key" : "Project", "values" : pvalues}];
//console.info("usage datums", data);
nv.addGraph(function() {
var chart = nv.models.multiBarChart()
.reduceXTicks(true) // Do not show every tick.
.rotateLabels(0) // Angle to rotate x-axis labels.
.showControls(false)
.useInteractiveGuideline(true)
.duration(100)
.groupSpacing(0.1); // Distance between each group of bars.
// Matches margins in resgraphs.js
chart.margin({"left":25,"right":15,"top":20,"bottom":40});
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%m/%d')(new Date(d))
});
chart.yAxis
.tickFormat(d3.format(',d'));
if (xlabel) {
var start = moment(details.start);
var end = moment(details.end);
chart.xAxis.axisLabel(start.format('lll') + " ... " +
end.format('lll'));
}
// 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));
});
d3.select(graphid + ' svg'