Commit 36c411bd authored by Leigh Stoller's avatar Leigh Stoller

Checkpoint new reservations UI.

parent 5fd70e57
...@@ -32,13 +32,13 @@ SUBDIRS = ...@@ -32,13 +32,13 @@ SUBDIRS =
BIN_SCRIPTS = manage_profile manage_instance manage_dataset \ BIN_SCRIPTS = manage_profile manage_instance manage_dataset \
create_instance rungenilib ns2rspec nsgenilib.py \ create_instance rungenilib ns2rspec nsgenilib.py \
rspec2genilib ns2genilib rspec2genilib ns2genilib manage_reservations
SBIN_SCRIPTS = apt_daemon aptevent_daemon portal_xmlrpc apt_checkup SBIN_SCRIPTS = apt_daemon aptevent_daemon portal_xmlrpc apt_checkup
LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm APT_Dataset.pm APT_Geni.pm \ LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm APT_Dataset.pm APT_Geni.pm \
APT_Aggregate.pm APT_Utility.pm APT_Aggregate.pm APT_Utility.pm
WEB_BIN_SCRIPTS = webmanage_profile webmanage_instance webmanage_dataset \ WEB_BIN_SCRIPTS = webmanage_profile webmanage_instance webmanage_dataset \
webcreate_instance webrungenilib webns2rspec webns2genilib \ webcreate_instance webrungenilib webns2rspec webns2genilib \
webrspec2genilib webrspec2genilib webmanage_reservations
WEB_SBIN_SCRIPTS= webportal_xmlrpc WEB_SBIN_SCRIPTS= webportal_xmlrpc
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS) LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
USERLIBEXEC = rungenilib.proxy genilib-jail genilib-iocage USERLIBEXEC = rungenilib.proxy genilib-jail genilib-iocage
......
This diff is collapsed.
This diff is collapsed.
...@@ -246,6 +246,9 @@ ProtoGeniDefs::AddModule("cluster", ...@@ -246,6 +246,9 @@ ProtoGeniDefs::AddModule("cluster",
"SliceOpenstackData" => \&GeniCluster::SliceOpenstackData, "SliceOpenstackData" => \&GeniCluster::SliceOpenstackData,
"SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation, "SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation,
"SliceMaxExtension" => \&GeniCluster::SliceMaxExtension, "SliceMaxExtension" => \&GeniCluster::SliceMaxExtension,
"Reserve" => \&GeniCluster::Reserve,
"Reservations" => \&GeniCluster::Reservations,
"DeleteReservation" => \&GeniCluster::DeleteReservation,
}}, }},
}); });
......
...@@ -51,6 +51,9 @@ $GENI_METHODS = { ...@@ -51,6 +51,9 @@ $GENI_METHODS = {
"SliceOpenstackData" => \&GeniCluster::SliceOpenstackData, "SliceOpenstackData" => \&GeniCluster::SliceOpenstackData,
"SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation, "SliceCheckReservation"=> \&GeniCluster::SliceCheckReservation,
"SliceMaxExtension" => \&GeniCluster::SliceMaxExtension, "SliceMaxExtension" => \&GeniCluster::SliceMaxExtension,
"Reserve" => \&GeniCluster::Reserve,
"Reservations" => \&GeniCluster::Reservations,
"DeleteReservation" => \&GeniCluster::DeleteReservation,
}; };
1; 1;
......
...@@ -23,9 +23,13 @@ ...@@ -23,9 +23,13 @@
# #
# #
#
# This needs to go into the DB.
#
class Aggregate class Aggregate
{ {
var $aggregate; var $aggregate;
var $typeinfo;
# #
# Constructor by lookup by urn # Constructor by lookup by urn
...@@ -41,6 +45,19 @@ class Aggregate ...@@ -41,6 +45,19 @@ class Aggregate
return; return;
} }
$this->aggregate = mysql_fetch_array($query_result); $this->aggregate = mysql_fetch_array($query_result);
$this->typeinfo = array();
#
# Get the type info
#
$query_result =
DBQueryWarn("select * from apt_aggregate_nodetypes ".
"where urn='$safe_urn'");
while ($row = mysql_fetch_array($query_result)) {
$type = $row["type"];
$this->typeinfo[$type] = array("count" => $row["count"],
"free" => $row["free"]);
}
} }
# accessors # accessors
function field($name) { function field($name) {
...@@ -52,6 +69,7 @@ class Aggregate ...@@ -52,6 +69,7 @@ class Aggregate
function abbreviation() { return $this->field('abbreviation'); } function abbreviation() { return $this->field('abbreviation'); }
function weburl() { return $this->field('weburl'); } function weburl() { return $this->field('weburl'); }
function has_datasets() { return $this->field('has_datasets'); } function has_datasets() { return $this->field('has_datasets'); }
function reservations() { return $this->field('reservations'); }
function isfederate() { return $this->field('isfederate'); } function isfederate() { return $this->field('isfederate'); }
function portals() { return $this->field('portals'); } function portals() { return $this->field('portals'); }
...@@ -70,6 +88,22 @@ class Aggregate ...@@ -70,6 +88,22 @@ class Aggregate
return null; return null;
} }
function LookupByNickname($nickname) {
$safe_nickname = addslashes($nickname);
$query_result =
DBQueryWarn("select urn from apt_aggregates ".
"where nickname='$safe_nickname'");
if (!$query_result || !mysql_num_rows($query_result)) {
return null;
}
$row = mysql_fetch_array($query_result);
$urn = $row['urn'];
return Aggregate::Lookup($urn);
}
# #
# Generate the free nodes URL from the web url. # Generate the free nodes URL from the web url.
# #
...@@ -99,6 +133,30 @@ class Aggregate ...@@ -99,6 +133,30 @@ class Aggregate
return $result; return $result;
} }
#
# Return a list of aggregates supporting reservations,
#
function SupportsReservations() {
$result = array();
global $PORTAL_GENESIS;
$query_result =
DBQueryFatal("select urn from apt_aggregates ".
"where disabled=0 and reservations=1 and ".
" FIND_IN_SET('$PORTAL_GENESIS', portals)");
while ($row = mysql_fetch_array($query_result)) {
$urn = $row["urn"];
if (! ($aggregate = Aggregate::Lookup($urn))) {
TBERROR("Aggregate::SupportsReservations: ".
"Could not load aggregate $urn!", 1);
}
$result[] = $aggregate;
}
return $result;
}
# #
# Return the list of allowed aggregates based on the portal in use. # Return the list of allowed aggregates based on the portal in use.
# #
......
...@@ -53,12 +53,16 @@ $(function () { ...@@ -53,12 +53,16 @@ $(function () {
if (item.dataset) { if (item.dataset) {
var key = item.dataset['key']; var key = item.dataset['key'];
var margin = 15; var margin = 15;
var colsize = 12; var colsize = null;
// Squeeze vertical space for this field. // Squeeze vertical space for this field.
if (_.has(item.dataset, "compact")) { if (_.has(item.dataset, "compact")) {
margin = 5; margin = 5;
} }
// Column size per row,
if (_.has(item.dataset, "colsize")) {
colsize = item.dataset['colsize'];;
}
/* /*
* Wrap in a div we can name. We assume the form * Wrap in a div we can name. We assume the form
...@@ -94,7 +98,9 @@ $(function () { ...@@ -94,7 +98,9 @@ $(function () {
} }
label_text = label_text + "</label>"; label_text = label_text + "</label>";
wrapper.append($(label_text)); wrapper.append($(label_text));
colsize = (wide ? 9 : 6); if (!colsize) {
colsize = (wide ? 9 : 6);
}
} }
var innerdiv = var innerdiv =
$("<div class='col-sm-" + colsize + "'></div>"); $("<div class='col-sm-" + colsize + "'></div>");
...@@ -148,20 +154,20 @@ $(function () { ...@@ -148,20 +154,20 @@ $(function () {
* the page. Only allows a single form, but that would be easy * the page. Only allows a single form, but that would be easy
* to change if we needed it. * to change if we needed it.
*/ */
var form_modified = false;
function EnableUnsavedWarning(form, modified_callback) { function EnableUnsavedWarning(form, modified_callback) {
var modified = false;
$(form + ' :input').change(function () { $(form + ' :input').change(function () {
console.info("changed"); //console.info("changed");
if (modified_callback) { if (modified_callback) {
modified_callback(); modified_callback();
} }
modified = true; form_modified = true;
}); });
// Warn user if they have not saved changes. // Warn user if they have not saved changes.
window.onbeforeunload = function() { window.onbeforeunload = function() {
if (! modified) if (! form_modified)
return null; return null;
return "You have unsaved changes!"; return "You have unsaved changes!";
} }
...@@ -169,6 +175,9 @@ $(function () { ...@@ -169,6 +175,9 @@ $(function () {
function DisableUnsavedWarning(form) { function DisableUnsavedWarning(form) {
window.onbeforeunload = null; window.onbeforeunload = null;
} }
function MarkFormUnsaved() {
form_modified = true;
}
function ClearFormErrors(form) { function ClearFormErrors(form) {
$(form).find(".format-me").each(function () { $(form).find(".format-me").each(function () {
...@@ -183,6 +192,19 @@ $(function () { ...@@ -183,6 +192,19 @@ $(function () {
} }
} }
}); });
$('#general_error').html("");
}
/*
* Update a form contents from an array.
*/
function UpdateForm(form, formfields) {
_.each(formfields, function(value, name) {
$(form).find("[name=" + name + "]").each(function () {
console.log(this, this.type);
$(this).val(value);
});
});
} }
/* /*
...@@ -202,7 +224,7 @@ $(function () { ...@@ -202,7 +224,7 @@ $(function () {
ClearFormErrors(form); ClearFormErrors(form);
var checkonly_callback = function(json) { var checkonly_callback = function(json) {
console.info(json); console.info("CheckForm", json);
/* /*
* We deal with these errors, the caller handles other errors. * We deal with these errors, the caller handles other errors.
...@@ -236,8 +258,8 @@ $(function () { ...@@ -236,8 +258,8 @@ $(function () {
formfields[field.name] = field.value; formfields[field.name] = field.value;
}); });
var submit_callback = function(json) { var submit_callback = function(json) {
console.info(json); console.info("SubmitForm", json);
sup.HideModal("#waitwait-modal"); sup.HideWaitWait();
DisableUnsavedWarning(form); DisableUnsavedWarning(form);
callback(json); callback(json);
}; };
...@@ -260,6 +282,8 @@ $(function () { ...@@ -260,6 +282,8 @@ $(function () {
"GenerateFormErrors" : GenerateFormErrors, "GenerateFormErrors" : GenerateFormErrors,
"EnableUnsavedWarning" : EnableUnsavedWarning, "EnableUnsavedWarning" : EnableUnsavedWarning,
"DisableUnsavedWarning" : DisableUnsavedWarning, "DisableUnsavedWarning" : DisableUnsavedWarning,
"MarkFormUnsaved" : MarkFormUnsaved,
"UpdateForm" : UpdateForm,
}; };
} }
)(); )();
......
$(function ()
{
'use strict';
var template_list = ["reservation-list", "oops-modal", "confirm-modal",
"waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var listTemplate = _.template(templates["reservation-list"]);
var confirmString = templates["confirm-modal"];
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var amlist = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
$('#confirm_div').html(confirmString);
LoadData();
}
/*
* Load reservations from each am in the list and generate a table.
*/
function LoadData()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(urn, name) {
var callback = function(json) {
console.log(json);
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.log("Could not get reservation data for " +
name + ": " + json.value);
return;
}
var reservations = json.value;
if (reservations.length == 0)
return;
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showidx" : true,
"showproject" : true,
"showuser" : true,
"name" : name,
});
html =
"<div class='row' id='" + name + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#main-body').prepend(html);
// Format dates with moment before display.
$('#' + name + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
$('#' + name + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
// Bind a delete handler.
$('#' + name + ' .delete-button').click(function() {
DeleteReservation($(this).closest('tr'));
return false;
});
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
{"cluster" : name});
xmlthing.done(callback);
});
}
/*
* Delete a reservation. When complete, delete the table row.
*/
function DeleteReservation(row) {
// This is what we are deleting.
var idx = $(row).attr('data-idx');
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');
console.log("delete", json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
$(row).remove();
table.trigger('update');
};
// Bind the confirm button in the modal. Do the deletion.
$('#confirm_modal #confirm_delete').click(function () {
sup.HideModal('#confirm_modal');
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve",
"Delete",
{"idx" : idx,
"pid" : pid,
"cluster" : cluster});
xmlthing.done(callback);
});
// Handler so we know the user closed the modal. We need to
// clear the confirm button handler.
$('#confirm_modal').on('hidden.bs.modal', function (e) {
$('#confirm_modal #confirm_delete').unbind("click");
$('#confirm_modal').off('hidden.bs.modal');
})
sup.ShowModal("#confirm_modal");
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
$(function ()
{
'use strict';
var template_list = ["reserve-request", "reservation-list",
"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 fields = null;
var projlist = null;
var amlist = null;
var isadmin = false;
var editing = false;
var buttonstate = "check";
function initialize()
{
window.APT_OPTIONS.initialize(sup);
isadmin = window.ISADMIN;
editing = window.EDITING;
fields = JSON.parse(_.unescape($('#form-json')[0].textContent));
projlist = JSON.parse(_.unescape($('#projects-json')[0].textContent));
amlist = JSON.parse(_.unescape($('#amlist-json')[0].textContent));
GeneratePageBody(fields);
// Now we can do this.
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
/*
* In edit mode, we ask for the reservation details from the
* backend cluster and then update the form.
*/
if (editing) {
PopulateReservation();
}
}
//
// Moved into a separate function since we want to regen the form
// after each submit, which happens via ajax on this page.
//
function GeneratePageBody(formfields)
{
// Generate the template.
var html = mainTemplate({
formfields: formfields,
projects: projlist,
amlist: amlist,
isadmin: isadmin,
editing: false,
});
html = aptforms.FormatFormFieldsHorizontal(html);
$('#main-body').html(html);
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
// Handler for cluster change to show the type list.
$('#reserve-request-form #cluster').change(function (event) {
$("#reserve-request-form #cluster option:selected").
each(function() {
HandleClusterChange($(this).val());
return;
});
});
// Handle submit button.
$('#reserve-submit-button').click(function (event) {
event.preventDefault();
if (buttonstate == "check") {
CheckForm();
}
else {
Reserve();
}
});
// Handle modal submit button.
$('#confirm-reservation #commit-reservation').click(function (event) {
if (buttonstate == "submit") {
Reserve();
}
});
// Insert datepickers after html inserted.
$("#reserve-request-form #start_day").datepicker({
showButtonPanel: true,
});
$("#reserve-request-form #end_day").datepicker({
showButtonPanel: true,
});
/*
* Callback when something changes so that we can toggle the
* button from Submit to Check.
*/
var modified_callback = function () {
ToggleSubmit(true, "check");
};
aptforms.EnableUnsavedWarning('#reserve-request-form',
modified_callback);
LoadReservations();
}
//
// Check form validity. This does not check whether the reservation
// is valid.
//
function CheckForm()
{
var checkonly_callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
// Now check the actual reservation validity.
ValidateReservation();
}
/*
* Before we submit, set the start/end fields to UTC time.
*/
var start_day = $('#reserve-request-form [name=start_day]').val();
var start_hour = $('#reserve-request-form [name=start_hour]').val();
if (start_day && start_hour) {
var start = moment(start_day, "MM/DD/YYYY");
start.hour(start_hour);
console.log("start", start);
$('#reserve-request-form [name=start]').val(start.format());
}
var end_day = $('#reserve-request-form [name=end_day]').val();
var end_hour = $('#reserve-request-form [name=end_hour]').val();
if (end_day && end_hour) {
var end = moment(end_day, "MM/DD/YYYY");
end.hour(end_hour);
console.log("end", end);
$('#reserve-request-form [name=end]').val(end.format());
}
aptforms.CheckForm('#reserve-request-form', "reserve",
"Validate", checkonly_callback);
}
/*
* 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(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;
}
var reservations = json.value;
if (reservations.length == 0)
return;
// Generate the main template.
var html = listTemplate({
"reservations" : reservations,
"showidx" : false,
"showproject" : false,
"showuser" : false,
"name" : details.name,
});
html =
"<div class='row' id='" + details.nickname + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#reservation-lists').prepend(html);
// 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 + ' .tablesorter')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
}
var xmlthing = sup.CallServerMethod(null, "reserve",
"ListReservations",
{"cluster" : details.nickname,
"anonymous" : 1});
xmlthing.done(callback);
});
}
//
// Validate the reservation.
//
function ValidateReservation()
{
var callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
// User can submit.
ToggleSubmit(true, "submit");
// Make sure we still warn about an unsaved form.
aptforms.MarkFormUnsaved();
sup.ShowModal('#confirm-reservation');
};
aptforms.SubmitForm('#reserve-request-form', "reserve",
"Validate", callback,
"Checking to see if your request can be "+
"accommodated");
}
/*
* And do it.
*/
function Reserve()
{
var reserve_callback = function(json) {
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
window.location.replace(json.value);
};
aptforms.SubmitForm('#reserve-request-form', "reserve",
"Reserve", reserve_callback,
"Submitting your reservation request; "+
"patience please");