Commit 772c047b authored by Leigh Stoller's avatar Leigh Stoller

Powder changes; Show availability graphs on the instantiate page

finalize step. There are two graphs, one for Radios and one for
Servers. This code is very Powder specific, please don't say we want
it on any other portals!
parent 0d3c0588
......@@ -1027,7 +1027,8 @@ class ExtensionInfo
}
# $amlist, $fedlist, and $status are all output arrays
function CalculateAggregateStatus(&$amlist, &$fedlist, &$status) {
function CalculateAggregateStatus(&$amlist, &$fedlist, &$status,
$extended = false) {
global $TBMAINSITE, $DEFAULT_AGGREGATE_URN, $CHECKLOGIN_USER;
$am_array = Instance::DefaultAggregateList();
......@@ -1038,8 +1039,14 @@ function CalculateAggregateStatus(&$amlist, &$fedlist, &$status) {
$aggregate = $am_array[$DEFAULT_AGGREGATE_URN];
$urn = $aggregate->urn();
$am = $aggregate->name();
$amlist[$urn] = $am;
if ($extended) {
$amlist[$urn] = array("urn" => $urn,
"name" => $am,
"nickname" => $aggregate->nickname());
}
else {
$amlist[$urn] = $am;
}
$freevms = $vmcount = 0;
TBVMCounts($vmcount, $freevms);
......@@ -1055,7 +1062,14 @@ function CalculateAggregateStatus(&$amlist, &$fedlist, &$status) {
while (list($ignore, $aggregate) = each($am_array)) {
$urn = $aggregate->urn();
$am = $aggregate->name();
$amlist[$urn] = $am;
if ($extended) {
$amlist[$urn] = array("urn" => $urn,
"name" => $am,
"nickname" => $aggregate->nickname());
}
else {
$amlist[$urn] = $am;
}
#
# We need to mark federated sites for the cluster dropdown.
#
......@@ -1080,11 +1094,11 @@ function CalculateAggregateStatus(&$amlist, &$fedlist, &$status) {
}
}
function SpitAggregateStatus() {
function SpitAggregateStatus($extended = false) {
$amlist = array();
$fedlist = array();
$status = array();
CalculateAggregateStatus($amlist, $fedlist, $status);
CalculateAggregateStatus($amlist, $fedlist, $status, $extended);
echo "<script type='text/plain' id='amlist-json'>\n";
echo htmlentities(json_encode($amlist));
echo "</script>\n";
......
<?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
#
......@@ -319,7 +319,7 @@ function SPITFORM($formfields, $newuser, $errors)
global $TBBASE, $APTMAIL, $ISAPT, $ISCLOUD, $ISPNET, $PORTAL_NAME;
global $profile_array, $this_user, $profilename, $profile;
global $projlist, $skipfirststep, $maxduration, $TBMAINSITE;
global $refspec;
global $refspec, $ISPOWDER;
$showabout = ($ISAPT && !$this_user ? 1 : 0);
$registered = (isset($this_user) ? "true" : "false");
......@@ -342,6 +342,7 @@ function SPITFORM($formfields, $newuser, $errors)
SPITHEADER(1);
echo "<link rel='stylesheet' href='css/picker.css'>\n";
echo "<link rel='stylesheet' href='css/nv.d3.css'>\n";
# I think this will take care of XSS prevention?
echo "<script type='text/plain' id='form-json'>\n";
......@@ -374,12 +375,12 @@ function SPITFORM($formfields, $newuser, $errors)
echo htmlentities(json_encode($projlist));
echo "</script>\n";
}
#
# And AM list if that is allowed.
#
if (isset($this_user) && !$this_user->webonly() && !$ISAPT && !$ISPNET) {
SpitAggregateStatus();
}
SpitAggregateStatus(true);
echo "<script type='text/plain' id='skiptypes-json'>\n";
echo htmlentities(json_encode(Instance::NodeTypePruneList()));
echo "</script>\n";
SpitOopsModal("oops");
echo "<script type='text/javascript'>\n";
echo " window.PROFILE = '" . $formfields["profile"] . "';\n";
......@@ -412,7 +413,17 @@ function SPITFORM($formfields, $newuser, $errors)
else {
echo " window.FROMREPO = false;\n";
}
# Do we show an aggregate selector?
if (isset($this_user) && !$this_user->webonly()
&& !$ISAPT && !$ISPNET && !$ISPOWDER) {
echo " window.CLUSTERSELECT = true;\n";
}
else {
echo " window.CLUSTERSELECT = false;\n";
}
echo "</script>\n";
echo "<script src='js/lib/d3.v3.js'></script>\n";
echo "<script src='js/lib/nv.d3.js'></script>\n";
echo "<script src='js/lib/jquery-2.0.3.min.js'></script>\n";
echo "<script src='https://www.emulab.net/protogeni/jacksmod/stable/jacksmod.js'></script>";
echo "<script src='https://www.emulab.net/protogeni/jacksmod/stable/imagepicker.js'></script>";
......@@ -429,6 +440,7 @@ function SPITFORM($formfields, $newuser, $errors)
REQUIRE_MOMENT();
REQUIRE_JACKS();
REQUIRE_JQUERY_STEPS();
AddLibrary("js/resgraphs.js");
AddLibrary("js/gitrepo.js");
SPITREQUIRE("js/instantiate-new.js");
}
......@@ -518,7 +530,7 @@ if (!isset($create)) {
SPITFORM($defaults, false, array());
echo "<div style='display: none'><div id='jacks-dummy'></div></div>\n";
AddTemplateList(array("instantiate", "instantiate-new", "aboutapt", "aboutcloudlab", "aboutpnet", "waitwait-modal", "rspectextview-modal", "picker-template"));
AddTemplateList(array("instantiate", "instantiate-new", "aboutapt", "aboutcloudlab", "aboutpnet", "waitwait-modal", "rspectextview-modal", "picker-template","reservation-graph"));
SPITFOOTER();
return;
}
......
......@@ -2,7 +2,7 @@ $(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['instantiate-new', 'aboutapt', 'aboutcloudlab', 'aboutpnet', 'waitwait-modal', 'rspectextview-modal']);
var templates = APT_OPTIONS.fetchTemplateList(['instantiate-new', 'aboutapt', 'aboutcloudlab', 'aboutpnet', 'waitwait-modal', 'rspectextview-modal', 'reservation-graph']);
var instantiateString = templates['instantiate-new'];
var aboutaptString = templates['aboutapt'];
var aboutcloudString = templates['aboutcloudlab'];
......@@ -46,8 +46,11 @@ $(function ()
var types = null;
var hardware = null;
var resinfo = null;
var graphsdrawn = false;
var currentStep = 0;
var deprecatedList = [];
var mainTemplate = _.template(instantiateString);
var graphTemplate = _.template(templates["reservation-graph"]);
function initialize()
{
......@@ -70,7 +73,7 @@ $(function ()
if ($('#amlist-json').length) {
amlist = decodejson('#amlist-json');
_.each(_.keys(amlist), function (key) {
amValueToKey[amlist[key]] = key;
amValueToKey[amlist[key].name] = key;
});
amstatus = decodejson('#amstatus-json');
console.info(amstatus);
......@@ -123,11 +126,6 @@ $(function ()
});
var projcategories = MakeProfileCategories(profileToArray);
// Fire this off right away.
if (window.REGISTERED) {
LoadReservationInfo();
}
var html = mainTemplate({
formfields: decodejson('#form-json'),
profiles: profilelist,
......@@ -150,8 +148,15 @@ $(function ()
clustername: window.PORTAL_NAME,
admin: isadmin,
maxduration: window.MAXDURATION,
clusterselect: window.CLUSTERSELECT,
});
$('#main-body').html(html);
// Fire this off right away.
if (window.REGISTERED) {
LoadReservationInfo();
}
if (projlist)
UpdateGroupSelector();
......@@ -181,6 +186,8 @@ $(function ()
return StepChanging(this, event, currentIndex, newIndex);
},
onStepChanged: function(event, currentIndex, priorIndex) {
// Globally record what step we are on.
currentStep = currentIndex;
return StepChanged(this, event, currentIndex, priorIndex);
},
onFinishing: function(event, currentIndex) {
......@@ -565,6 +572,9 @@ $(function ()
if (currentIndex == 2) {
SwitchJacks('small');
}
if (currentIndex == 2 && newIndex != 2) {
HideClusterGraphs();
}
if (currentIndex == 0 && selected_uuid == null) {
return false;
}
......@@ -628,11 +638,15 @@ $(function ()
else if (currentIndex == 2 && priorIndex == 1) {
// Keep the two panes the same height
$('#inline_container').css('height',
$('#finalize_container').outerHeight());
// Chrome was having an issue where Jacks was not responding to
// the height change. Had to also add to Jacks root.
$('#inline_jacks').css('height',
$('#finalize_container').outerHeight());
$('#finalize_container').outerHeight() - 15);
// Chrome was having an issue where Jacks was not responding to
// the height change. Had to also add to Jacks root.
$('#inline_jacks').css('height',
$('#finalize_container').outerHeight() - 15);
}
if (currentIndex == 2) {
ShowClusterGraphs();
}
if (currentIndex < priorIndex) {
// Disable going forward by clicking on the labels
......@@ -940,6 +954,10 @@ $(function ()
if (monitor == null || $.isEmptyObject(monitor)) {
return;
}
// No need to do this if not showing selectors.
if (!window.CLUSTERSELECT) {
return;
}
$('#finalize_options .cluster-group').each(function() {
if ($(this).hasClass("pickered")) {
......@@ -999,10 +1017,17 @@ $(function ()
key: 'disabled'
}];
picker.MakePicker(pickerTarget, wt.StatusClickEvent, attributes, dividers, {class: 'cluster_picker_status'});
picker.MakePicker(pickerTarget,
function (container, that, target) {
ClusterSelected(that, true);
wt.StatusClickEvent(container, that, target);
},
attributes, dividers,
{class: 'cluster_picker_status'});
// Assign health ratings and icons
_.each(amlist, function(name, key) {
_.each(amlist, function(details, key) {
var name = details.name;
var data = monitor[key];
var rating, classes;
var target = $('#'+which+' .cluster_picker_status .dropdown-menu .enabled a:contains("'+name+'")');
......@@ -1642,6 +1667,11 @@ $(function ()
var count = 0;
sites = {};
// No need to do this if not showing selectors.
if (!window.CLUSTERSELECT) {
return;
}
var nodecount = $(xmlDoc).find("node").length;
if (nodecount > 100) {
doconstraints = 0;
......@@ -1700,7 +1730,8 @@ $(function ()
// Create the dropdown selection lists. If only one, then force
// that one to be selected.
var options = "";
_.each(amlist, function(name, key) {
_.each(amlist, function(details, key) {
var name = details.name;
options = options + "<option value='" + name + "'";
if (amlist.count == 1) {
options = options + " selected";
......@@ -1770,6 +1801,12 @@ $(function ()
$("#cluster_selector").html(html);
updateWhere();
$("#cluster_selector").removeClass("hidden");
// This event will be overriden when the fancy cluster status
// stuff is initialized.
$('.select_where').change(function (event) {
ClusterSelected(event, false);
});
}
/*
......@@ -2148,6 +2185,8 @@ $(function ()
function LoadReservationInfo()
{
InitClusterGraphs();
var callback = function(json) {
if (json.code) {
console.info("Could not get reservation info: " + json.value);
......@@ -2157,6 +2196,7 @@ $(function ()
resinfo = json.value;
ShowClusterReservations();
ShowClusterGraphs();
};
var $xmlthing =
sup.CallServerMethod(null, "reserve", "ReservationInfo", null);
......@@ -2188,5 +2228,127 @@ $(function ()
console.info("picker event", action, id, value);
ga('send', 'event', 'picker', action, id, value);
}
function InitClusterGraphs()
{
// Only the Powder portal for now.
if (!window.ISPOWDER) {
return;
}
// Per clusters rows filled in with templates.
// For POWDER there are two graphs, one for
_.each(amlist, function(details, urn) {
var graphid = "resgraph-" + details.nickname;
$('#' + details.nickname + " .resgraph-panel-radios")
.html(graphTemplate({"details" : details,
"graphid" : graphid + "-radios",
"title" : "Radio",
"urn" : urn,
"showhelp" : true,
"showfullscreen" : false}));
$('#' + details.nickname + " .resgraph-panel-servers")
.html(graphTemplate({"details" : details,
"graphid" : graphid + "-servers",
"title" : "Server",
"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');
});
});
}
function ShowClusterGraphs()
{
// Only the Powder portal for now.
if (!window.ISPOWDER) {
return;
}
if (currentStep != 2) {
return;
}
// Make visible, in case we hid it.
// Must be visible to draw graphs.
$('#resgraph-div').removeClass("hidden");
if (graphsdrawn || !resinfo) {
return;
}
var skiptypes = decodejson('#skiptypes-json');
// Per clusters rows filled in with templates.
_.each(amlist, function(details, urn) {
var graphid = 'resgraph-' + details.nickname;
if (! (_.has(resinfo, urn) && resinfo[urn])) {
$('#' + graphid).addClass("hidden");
return;
}
// Kill the spinners
$('#' + details.nickname + ' .resgraph-spinner')
.addClass("hidden");
ShowResGraph({"forecast" : resinfo[urn].forecast,
"selector" : graphid + "-radios",
"foralloc" : true,
"maxdays" : 7,
"showbrush" : false,
"skiptypes" : skiptypes,
"showtypes" : {"nuc5300" : true,
"nuc6260" : true,
"enodeb" : true,
"sdr" : true},
"click_callback" : null});
ShowResGraph({"forecast" : resinfo[urn].forecast,
"selector" : graphid + "-servers",
"foralloc" : true,
"maxdays" : 7,
"showbrush" : false,
"skiptypes" : skiptypes,
"showtypes" : {"d430" : true,
"d710" : true,
"pc3000" : true,
"d820" : true},
"click_callback" : null});
});
graphsdrawn = true;
}
function HideClusterGraphs()
{
// Only the Powder portal for now.
if (!window.ISPOWDER) {
return;
}
// Hide when switching to a different step.
$('#resgraph-div').addClass("hidden");
}
function ClusterSelected(selected, pickered)
{
console.info("ClusterSelected: ", selected);
var cluster = null;
window.foo = selected;
/*
* Dig out which cluster has been selected. Depending on whether
* it came from the plain drop down or the pickered dropdown.
*/
if (pickered) {
cluster = $(selected).attr("value");
}
else {
cluster = $(selected.target).find(":selected").val()
}
console.info("ClusterSelected: " + cluster);
}
$(document).ready(initialize);
});
......@@ -73,7 +73,11 @@ $(function ()
// Graph list(s).
html = "";
_.each(amlist, function(details, urn) {
var graphid = 'resgraph-' + details.nickname;
html += graphTemplate({"details" : details,
"graphid" : graphid,
"title" : details.nickname,
"urn" : urn,
"showhelp" : true,
"showfullscreen" : true});
......
......@@ -10,9 +10,12 @@ window.ShowResGraph = (function ()
var forecast = args.forecast;
// For the availablity page instead of reserve page.
var foralloc = args.foralloc;
var skiptypes= args.skiptypes;
var skiptypes= args.skiptypes;
var showtypes= args.showtypes;
var maxdays = args.maxdays;
var index = 0;
var datums = [];
var maxstamp = null;
if (foralloc === undefined) {
foralloc = false;
......@@ -20,6 +23,16 @@ window.ShowResGraph = (function ()
if (skiptypes === undefined) {
skiptypes = null;
}
if (showtypes === undefined) {
showtypes = null;
}
if (maxdays === undefined) {
maxdays = null;
}
else {
var now = Date.now() / 1000;
maxstamp = now + (maxdays * 3600 * 24);
}
/*
* For the interactive tooltip to work, every has data set has to
......@@ -34,6 +47,9 @@ window.ShowResGraph = (function ()
if (skiptypes && _.has(skiptypes, type)) {
continue;
}
if (showtypes && !_.has(showtypes, type)) {
continue;
}
// This is an array of objects.
var array = forecast[type];
......@@ -55,7 +71,8 @@ window.ShowResGraph = (function ()
*/
array = array.slice();
array.push($.extend({}, array[0]));
array[1].t = parseInt(array[1].t) + (45 * 3600 * 24);
array[1].t = parseInt(array[1].t) +
((maxdays ? maxdays : 45) * 3600 * 24);
}
else if (array.length > 1) {
/*
......@@ -81,6 +98,15 @@ window.ShowResGraph = (function ()
//console.info("toss2", type, data, nextdata);
continue;
}
/*
* Stop processing after we reach maxdays out.
*/
if (maxstamp) {
var t = parseInt(data.t);
if (t > maxstamp) {
break;
}
}
temp.push(data);
}
/*
......@@ -94,12 +120,26 @@ window.ShowResGraph = (function ()
data.t = parseInt(data.t);
temp.push(data);
temp.push($.extend({}, data));
temp[1].t = parseInt(temp[1].t) + (45 * 3600 * 24);
temp[1].t = parseInt(temp[1].t) +
((maxdays ? maxdays : 45) * 3600 * 24);
}
else {
// Tack on last one.
// Tack on last one unless it violates maxdays limit.
if (temp[temp.length - 1].t != array[array.length - 1].t) {
temp.push(array[array.length - 1]);
var data = array[array.length - 1];
var t = parseInt(data.t);
if (maxstamp && (t > maxstamp)) {
// If only one point need to generate another.
if (temp.length == 1) {
data = $.extend({}, data);
data.t = maxstamp;
temp.push(data);
}
}
else {
temp.push(data);
}
}
}
array = temp;
......@@ -261,17 +301,30 @@ window.ShowResGraph = (function ()
return datums;
}
function CreateGraph(datums, selector, click_callback) {
function CreateGraph(datums, selector, click_callback, showbrush) {
var id = '#' + selector;
$(id + ' svg').html("");
// New option
if (showbrush === undefined) {
showbrush = true;
}
window.nv.addGraph(function() {
var chart = window.nv.models.lineWithFocusChart();
var chart;
if (showbrush) {
chart = window.nv.models.lineWithFocusChart();
}
else {
chart = window.nv.models.lineChart();
}
chart.margin({"left":25,"right":15,"top":20,"bottom":20});
/*
* We need the min,max of the time stamps for the brush. We can use
* just one of the nodes.
*/
*/
var minTime = d3.min(datums[0].values,
function (d) { return d.x; });
var maxTime = d3.max(datums[0].values,
......@@ -280,13 +333,13 @@ window.ShowResGraph = (function ()
if (maxTime - minTime > (3600 * 24 * 7 * 1000)) {
maxTime = minTime + (3600 * 24 * 7 * 1000);
}
chart.brushExtent([minTime,maxTime]);
if (showbrush) {
chart.brushExtent([minTime,maxTime]);
chart.x2Axis.tickFormat(function(d) {
return d3.time.format('%m/%d')(new Date(d))
});
chart.margin({"left":25,"right":15,"top":20,"bottom":20});
chart.x2Axis.tickFormat(function(d) {
return d3.time.format('%m/%d')(new Date(d))
});
}
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%m/%d')(new Date(d))
});
......@@ -338,7 +391,8 @@ window.ShowResGraph = (function ()
return;
}
console.info("datums", datums);
CreateGraph(datums, args.selector, args.click_callback);
CreateGraph(datums, args.selector, args.click_callback,
args.showbrush);
};
}
)();
......
......@@ -46,12 +46,16 @@ $(function ()
$('#main-body').html(html);
// Per clusters rows filled in with templates.
_.each(amlist, function(details, urn) {
var graphid = 'resgraph-' + details.nickname;
$('#' + details.nickname + " .counts-panel")
.html(totalsTemplate({"details" : details,
"urn" : urn}));
$('#' + details.nickname + " .resgraph-panel")
.html(graphTemplate({"details" : details,
"graphid" : graphid,
"title" : details.nickname,
"urn" : urn,
"showhelp" : true,
"showfullscreen" : false}));
......
<style>
.panel-heading-list {
padding: 2px;
}
.panel-body-dashboard {
padding: 2px;
}
.resgraph-size {
max-height:300px;
height:300px;
}
.resgraph-modal-size {
max-height:600px;
height:600px;
}
svg {
display: block;
}
</style>
<div>
<div id='about_div'
class='col-lg-8 col-lg-offset-2
......@@ -349,7 +368,7 @@
</div>
</div>
<% } %>
<% if (amlist) { %>
<% if (clusterselect) { %>
<div id='aggregate_selector'>
<!-- The JS code fills this in -->
<div id='cluster_selector' class='hidden'></div>
......@@ -458,6 +477,27 @@
</form>
</div>
</div>
<% if (amlist) { %>
<div class='col-lg-10 col-lg-offset-1
col-md-10 col-md-offset-1
col-sm-10 col-sm-offset-1
col-xs-12 col-xs-offset-0 hidden'
style="margin-top: 10px;"
id='resgraph-div'>
<% _.each(amlist, function(details, urn) { %>
<div class='row' id='<%- details.nickname %>'>
<div class='col-sm-6 resgraph-panel-radios'
style="padding: 0px;">
<!-- Template goes here -->
</div>
<div class='col-sm-6 resgraph-panel-servers'
style="padding: 0px;">
<!-- Template goes here -->
</div>
</div>
<% }) %>
</div>
<% } %>
</div>
<!-- This is the user verify modal -->
<div id='verify_modal' class='modal fade'
......@@ -642,6 +682,36 @@
</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>
<li> Hover over and move around the graph to show a
tooltip that will provide the date and time, and the free
count for all of the node types at the time stamp you are
hovering over.</li>
<li> To reserve nodes for an experiment, please visit the
<a href="reserve.php">reservation</a> page.
</ul>
</p>
</div>
</div>
</div>
</div>
<div id='waitwait_div'></div>
<div id='rspecview_div'></div>
<div id='ppviewmodal_div'></div>
......
<div id='resgraph-<%- details.nickname %>'>
<div id='<%- graphid %>'>
<div class='col-xs-12 col-xs-offset-0 reservation-details'>
<div class='panel panel-default'>