Commit c201620c authored by David Johnson's avatar David Johnson

Add geni-lib script parameter warnings/errors, groups, helpdocs.

(The bulk of the code is in the parameter form formatter/decorator JS
code; I only applied it to the wizard for now... but it's
straightforward to copy it into the old parameter code ;))

The portal can now render the parameter form in much more complex ways.
It supports a notion of advanced parameter groups, a panel containing
parameters that is closed by default but expandable; generic parameter
groups; per-parameter detailed helpdocs in an expandable subpanel (and
an expand-all-help link that will also expand all the parameter group
panels), and error messages and warnings.  A summary of both errors and
warnings is displayed at the top of the form, and specific error message
details are displayed near parameters.  The error message display is
flexible to generic user messages -- basically if the error is a proper
geni-lib portal error/warning, but not a parameter error/warning, it
will be displayed at the top of the parameter form (along with any
others).  If it's an "improper" one, we'll still do our best to display
it.  If you warn a user, you can provide a set of parameter values that
"fix" the warning, and the Portal UI will change the form values and
tell the user it did so.  You can't do this on error; the assumption is
the user has to fix the error.

Finally, the portal now tries to rungenilib in warnings-are-fatal mode
the first time parameter bind (to generate rspec) is attempted.  If they
go backwards to re-parameterize, warnings should be fatal again.  It's
too hard to figure out when we should stop warnings-fatal mode; we can't
block the user's progress if they really want to proceed in the face of
warnings.
parent 42ce02e4
......@@ -46,11 +46,12 @@ sub usage()
exit(-1);
}
my $optlist = "do:pb:";
my $optlist = "do:pb:W";
my $debug = 0;
my $getparams = 0;
my $paramfile;
my $ofile;
my $warningsfatal = 0;
#
# Configure variables
......@@ -111,6 +112,9 @@ if (defined($options{"d"})) {
if (defined($options{"p"})) {
$getparams = 1;
}
if (defined($options{"W"})) {
$warningsfatal = 1;
}
if (defined($options{"b"})) {
$paramfile = $options{"b"};
# Must taint check!
......@@ -187,6 +191,7 @@ system("touch $outfile") == 0 or
my $cmdargs = "$TB/libexec/rungenilib.proxy ";
$cmdargs .= " -u " . $this_user->uid();
$cmdargs .= ($getparams ? " -p " : "");
$cmdargs .= ($warningsfatal ? " -W " : "");
#
# We want to send over both files via STDIN, so combine them, and pass
......
......@@ -39,10 +39,11 @@ sub usage()
exit(-1);
}
my $optlist = "u:vpb:";
my $optlist = "u:vpb:W";
my $user;
my $getparams= 0;
my $paramsize;
my $warningsfatal = 0;
#
# Configure variables
......@@ -100,6 +101,9 @@ if (defined($options{"b"})) {
if (defined($options{"u"})) {
$user = $options{"u"};
}
if (defined($options{"W"})) {
$warningsfatal = 1;
}
#
# First option has to be the -u option, the user to run this script as.
......@@ -193,6 +197,9 @@ if ($getparams) {
elsif (defined($paramsize) && $paramsize) {
$ENV{'GENILIB_PORTAL_PARAMS_PATH'} = $pfile;
}
if ($warningsfatal) {
$ENV{'GENILIB_PORTAL_WARNINGS_ARE_FATAL'} = "1";
}
#
# Fork a child process to run the parser in.
......
......@@ -20,6 +20,7 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
var multisite = 0;
var RSPEC = null;
var configuredone_callback = null;
var warningsfatal = 1;
//
// Moved into a separate function since we want to regen the form
......@@ -49,13 +50,113 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
event.preventDefault();
HandleSubmit();
});
//
// Handle the toggle-all help panels link. Bootstrap
// doesn't give us a simple way to collapse multiple panels
// unless they're in an accordion... so do it the
// old-fashioned way, manual modal state in a hidden div.
//
$('#pp-param-help-panel-toggle-link').on('click',function() {
//event.preventDefault();
var state = $('#pp-param-help-panel-toggle-state').html();
var list = document.getElementsByClassName("pp-param-help-panel");
for (var i = 0; i < list.length; ++i) {
if (state == 'opened') {
$(list[i]).collapse('hide');
}
else {
$(list[i]).collapse('show');
}
}
if (state == 'opened') {
$('#pp-param-help-panel-toggle-state').html('closed');
$('#pp-param-help-panel-toggle-link-span').html('&nbsp;&nbsp; Show All Parameter Help');
$('#pp-param-help-panel-toggle-glyph-span').removeClass('glyphicon-minus-sign');
$('#pp-param-help-panel-toggle-glyph-span').addClass('glyphicon-plus-sign');
}
else {
$('#pp-param-help-panel-toggle-state').html('opened');
$('#pp-param-help-panel-toggle-link-span').html('&nbsp;&nbsp; Hide All Parameter Help');
$('#pp-param-help-panel-toggle-glyph-span').removeClass('glyphicon-plus-sign');
$('#pp-param-help-panel-toggle-glyph-span').addClass('glyphicon-minus-sign');
}
// Now open all the param group panels, in case they have help:
list = $('#ppmodal-body').find(".pp-param-group-subpanel-collapse");
for (var i = 0; i < list.length; ++i) {
$(list[i]).collapse('show');
}
});
}
// Formatter for the form. This did not work out nicely at all!
function formatter(fieldString, errors)
{
var root = $(fieldString);
var list = root.find('.format-me');
var form = root.find('#pp_form');
var hasHelp = false;
var groupNames = new Array();
var groupsWithErrors = new Array();
var groupErrorOpenerScript = "";
var numTypeErrors = 0;
var numParameterErrors = 0;
var numOtherErrors = 0;
var otherErrorText = "";
var numParameterWarnings = 0;
var numOtherWarnings = 0;
var otherWarningText = "";
var fixedValuesChanges = 0;
// Compute the general warning and error message text now.
if (Array.isArray(errors)) {
for (var i = 0; i < errors.length; ++i) {
var error = errors[i];
if (error.hasOwnProperty('errorType')) {
if (error['errorType'] == 'ParameterWarning')
++numParameterWarnings;
else if (error['errorType'] == 'ParameterError')
++numParameterErrors;
else if (error['errorType'].endsWith('Warning')) {
++numOtherWarnings;
if (numOtherWarnings > 1)
otherWarningText += "<br>";
otherWarningText += '<b>' + error['errorType'] +
'</b>: ' + error['message'];
}
else if (error['errorType'].endsWith('Error')) {
++numOtherErrors;
if (numOtherErrors > 1)
otherErrorText += "<br>";
otherErrorText += '<b>' + error['errorType'] +
'</b>: ' + error['message'];
}
else {
// For this one, we rely on the fact that
// this came to us as JSON data, so it must
// be JSON-stringifiable!
++numOtherErrors;
if (numOtherErrors > 1)
otherErrorText += "<br>";
otherErrorText += '<b>' + error['errorType'] +
'</b>: "' + JSON.stringify(error) + '"';
}
}
else {
// For this one, we rely on the fact that this
// came to us as JSON data, so it must be
// JSON-stringifiable!
++numOtherErrors;
if (numOtherErrors > 1)
otherErrorText += "<br>";
otherErrorText += '<b>Unrecognized Error</b>: "' +
JSON.stringify(error) + '"';
}
}
}
list.each(function (index, item) {
if (item.dataset) {
var key = item.dataset['key'];
......@@ -65,6 +166,172 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
var outerdiv = $("<div class='form-group' " +
" style='margin-bottom: " + margin +
"px;'></div>");
var help_panel = "";
var glParamErrors = new Array();
var glParamWarnings = new Array();
var groupId = null;
var pDiv;
var changeMsg = "";
if (Array.isArray(errors)) {
// Check to see if any of these errors are
// geni-lib ParameterErrors or
// ParameterWarnings, for this key. Note, we
// only loop through the numeric keys of this
// array, because we're expecting a bare JSON
// list of dictionaries, each describing an
// error.
for (var i = 0; i < errors.length; ++i) {
var error = errors[i];
if (!error.hasOwnProperty('params')
|| !Array.isArray(error['params'])
|| error['params'].indexOf(key) == -1)
continue;
if (error.hasOwnProperty('errorType')
&& error['errorType'] == 'ParameterWarning') {
glParamWarnings.push(error);
}
// Note, we don't worry about errorType for
// this check; assume the message is an error for
// this param.
else {
glParamErrors.push(error);
}
// Maybe change form values if the error/warning
// suggests a fixedValue.
if (error.hasOwnProperty('fixedValues')) {
jQuery.each(error['fixedValues'],function(k,v) {
var oldV = null;
// If we're not changing the value
// for this param, don't mention it
// here -- only mention changes
// right beside that specific
// parameter.
if (k != key)
return;
//
// Well, this sucks. We can't just
// change the element values via
// $(elm).val(foo). Have to
// actually change the HTML attrs.
//
if ($(item).prop('tagName') == 'SELECT') {
oldV = $(item).val();
var sch = $(item).children();
for (var j = 0; j < sch.length; ++j) {
var opt = $(sch[j]);
// get rid of anything selected
if (opt.attr('selected'))
opt.attr('selected',null);
// select only the new fixed value
if (opt.attr('value') == v)
opt.attr('selected',true);
}
}
else if ($(item).prop('tagName') == 'INPUT'
&& $(item).attr('type') == 'text') {
oldV = $(item).val();
$(item).attr('value',v);
}
else if ($(item).prop('tagName') == 'INPUT'
&& $(item).attr('type') == 'checkbox') {
oldV = $(item).val();
if (v)
$(item).attr('checked',true);
else
$(item).attr('checked',null);
}
else {
// we don't have any other
// elements currently being
// generated in
// profile_defs.php, so forget
// it for now.
;
}
// Ok, did we change something?
if (oldV != null) {
++fixedValuesChanges;
if (changeMsg)
changeMsg += "<br>";
changeMsg += '<b><span class="glyphicon glyphicon-exclamation-sign"></span>&nbsp;' +
' We changed the value of "' +
item.dataset['label'] + '" from "' +
oldV + '" to "' + v + '", because ' +
' the profile\'s geni-lib script ' +
' suggested it to resolve the' +
' problem.</b>';
}
});
}
}
}
if ($(item).attr('pp-param-group')
&& !groupNames.hasOwnProperty($(item).attr('pp-param-group'))) {
// This item is part of a group. So, we process
// it like usual, but we have to shove it off
// into the right collapsable group panel. If
// the panel doesn't exist yet, we have to
// create it.
groupId = $(item).attr('pp-param-group');
var groupName;
if ($(item).attr('pp-param-group-name')) {
groupName = $(item).attr('pp-param-group-name');
groupNames[groupId] = groupName;
}
else {
groupName = groupId;
groupNames[groupId] = null;
}
pDiv = $('<div class="row">' +
'<div class="col-xs-offset-0">' +
'<div id="pp-param-group-panel-' + groupId + '"' +
' class="panel" style="border-width: 0px; border: none; box-shadow: none;">' +
'<div class="panel-heading">' +
'<h5>' +
'<a id="pp-param-group-link-' + groupId + '"' +
' href="#pp-param-group-subpanel-' + groupId + '"' +
' data-toggle="collapse">' +
'<span class="glyphicon glyphicon-plus-sign pull-left"' +
' style="font-weight: bold;"></span>' +
'<span style="font-weight: bold;">&nbsp;&nbsp; ' + groupName + '</span>' +
'</a>' +
'</h5>' +
'</div>' +
'<div id="pp-param-group-subpanel-' + groupId + '"' +
' class="panel-collapse collapse pp-param-group-subpanel-collapse"' +
' style="height: auto;">' +
'<div id="pp-param-group-subpanel-body-' + groupId + '"' +
' class="panel-body"></div>' +
'</div></div></div></div>');
form.append(pDiv);
}
else if ($(item).attr('pp-param-group')) {
groupId = $(item).attr('pp-param-group');
if (groupNames[groupId] == null && $(item).attr('pp-param-group-name')) {
groupNames[groupId] = $(item).attr('pp-param-group-name');
}
}
if ((glParamErrors.length > 0
|| glParamWarnings.length > 0
|| changeMsg != "")
&& !groupsWithErrors.hasOwnProperty(groupId)) {
groupsWithErrors[groupId] = 1;
groupErrorOpenerScript += '<script>' +
'$("#pp-param-group-subpanel-' + groupId + '")' +
'.collapse("show");' +
'</script>';
}
if ($(item).attr('data-label')) {
var label_text =
......@@ -73,16 +340,25 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
item.dataset['label'];
if ($(item).attr('data-help')) {
var help_panel_url = key + "_help_subpanel_collapse";
label_text = label_text +
"<a href='#' class='btn btn-xs' " +
" data-toggle='popover' " +
"<span class='pp-param-tooltip' " +
" data-toggle='tooltip' " +
" data-html='true' " +
" data-delay='{\"hide\":1000}' " +
" data-content='" +
item.dataset['help'] + "'>" +
"<span class='glyphicon " +
"glyphicon-question-sign'>" +
" </span></a>";
//" data-delay='{\"hide\":1000}' " +
" title='" + item.dataset['help'] + "'>" +
" <a href='#" + help_panel_url + "'" +
" data-toggle='collapse'>" +
"<i class='glyphicon glyphicon-question-sign'></i>" +
"</a></span>";
var help_panel_id = key + "_help_subpanel_collapse";
help_panel =
"<div id='" + help_panel_id + "'" +
" class='panel-collapse collapse panel panel-info col-sm-9 pp-param-help-panel'" +
" style='background-color: #e6f6fa; height: auto; margin: auto; margin-left: 5%; margin-top: 0px; margin-bottom: 15px; padding: 10px;' data-toggle='collapse'>" +
item.dataset['help'] + "</div>";
hasHelp = true;
}
label_text = label_text + "</label>";
outerdiv.append($(label_text));
......@@ -91,18 +367,183 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
var innerdiv = $("<div class='col-sm-" +
colsize + "'></div>");
innerdiv.html($(item).clone());
// Handle the easy type-checked errors from the PHP/ajax call.
if (errors && _.has(errors, key)) {
outerdiv.addClass('has-error');
innerdiv.append('<label class="control-label" ' +
'for="inputError">' +
_.escape(errors[key]) + '</label>');
}
outerdiv.append(innerdiv);
$(item).after(outerdiv);
$(item).remove();
else {
if (glParamErrors.length > 0) {
var errorMsg = "";
for (var i = 0; i < glParamErrors.length; ++i) {
var error = glParamErrors[i];
if (errorMsg)
errorMsg += "<br>";
errorMsg += error['errorType'] + ": " +
error['message'];
}
outerdiv.addClass('has-error');
innerdiv.append('<label class="control-label" ' +
'for="inputError">' +
errorMsg + '</label>');
}
if (glParamWarnings.length > 0) {
var warningMsg = "";
for (var i = 0; i < glParamWarnings.length; ++i) {
var warning = glParamWarnings[i];
if (warningMsg)
warningMsg += "<br>";
warningMsg += warning['errorType'] + ": " +
warning['message'];
}
outerdiv.addClass('has-warning');
innerdiv.append('<label class="control-label" ' +
'for="inputWarning">' +
warningMsg + '</label>');
}
if (changeMsg != "") {
innerdiv.append('<br><label class="control-label" ' +
'for="inputWarning">' +
changeMsg + '</label>');
}
}
// Ok, now slot the new form element (outerdiv) into
// the right panel. If it's not in a panel group,
// then just do the regular thing; otherwise, put it
// in its panel.
if (groupId != null) {
outerdiv.append(innerdiv);
var pInnerDiv =
root.find('#pp-param-group-subpanel-body-' + groupId);
pInnerDiv.append(outerdiv);
pInnerDiv.append(help_panel);
$(item).remove();
}
else {
outerdiv.append(innerdiv);
// Do these in reverse order, because of .after! In
// this order, outerdiv first, then help_panel.
$(item).after(help_panel);
$(item).after(outerdiv);
$(item).remove();
}
}
});
// Setup the help-all toggle, if there were help items.
if (hasHelp) {
root.prepend('<div id="pp-param-help-panel-toggle-state" ' +
' style="display: none">closed</div>' +
'<div class="row">' +
'<div class="col-sm-12">' +
'<div id="help_show_all_panel" class="panel" ' +
' style="border-width: 0px; border: none; box-shadow: none;">' +
'<h5>' +
'<a id="pp-param-help-panel-toggle-link" href="#">' +
'<span id="pp-param-help-panel-toggle-glyph-span" ' +
' class="glyphicon glyphicon-plus-sign pull-left" style="font-weight: bold; "></span>' +
'<span id="pp-param-help-panel-toggle-link-span" ' +
' style="font-weight: bold; ">' +
'&nbsp;&nbsp; Show All Parameter Help</span>' +
'</a>' +
'</h5>' +
'</div></div></div>');
}
// Show primary error and warning notifications, and changes!
if (fixedValuesChanges > 0) {
var ht =
'<div class="row">' +
'<div class="col-sm-12">' +
'<div id="pp-param-changes-panel" ' +
' class="panel panel-success">' +
'<div class="panel-heading">' +
'We changed ' + fixedValuesChanges + ' item ';
if (fixedValuesChanges > 1)
ht += 'values';
else
ht += 'value';
ht += ' in response to these bad parameter values, because' +
' this profile\'s geni-lib script suggested they would' +
' help. Please check them.';
ht += '</div></div></div></div>';
root.prepend(ht);
}
if (numParameterWarnings > 0 || numOtherWarnings > 0) {
var ht = "";
if (numOtherWarnings > 0)
ht += otherWarningText;
if (numParameterWarnings > 1) {
if (ht != "")
ht += '<br>';
ht += '<b>There were ' + numParameterWarnings +
' ParameterWarnings</b>. Please check the warning' +
' messages near each affected parameter; you will' +
' <b>not</b> be notified about subsequent warnings.';
}
else if (numParameterWarnings > 0) {
if (ht != "")
ht += '<br>';
ht += '<b>There was 1 ParameterWarning</b>. Please check' +
' the warning message near the affected parameter; you' +
' will <b>not</b> be notified about subsequent warnings.';
}
ht = '<div class="row">' +
'<div class="col-sm-12">' +
'<div id="pp-param-warning-panel" ' +
' class="panel panel-warning">' +
'<div class="panel-heading">' +
ht + '</div></div></div></div>';
root.prepend(ht);
}
if (numTypeErrors > 0
|| numParameterErrors > 0
|| numOtherErrors > 0) {
var ht = "";
if (numOtherErrors > 0)
ht += otherErrorText;
if (numParameterErrors > 1) {
if (ht != "")
ht += '<br>';
ht += '<b>There were ' + numParameterErrors +
' ParameterErrors</b>. Please check the error' +
' messages near each affected parameter and fix the' +
' errors.';
}
else if (numParameterErrors > 0) {
if (ht != "")
ht += '<br>';
ht += '<b>There was 1 ParameterError</b>. Please check' +
' the error message near the affected parameter and' +
' fix it.';
}
ht = '<div class="row">' +
'<div class="col-sm-12">' +
'<div id="pp-param-error-panel" class="panel panel-danger">' +
'<div class="panel-heading">' +
ht + '</div></div></div></div>';
root.prepend(ht);
}
// Tell Bootstrap to initialize the tooltips
root.append('<script>$("span.pp-param-tooltip").tooltip();</script>');
// Make sure group panels with errors are open, not closed.
root.append(groupErrorOpenerScript);
return root;
}
......@@ -127,6 +568,9 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
//
function ConfigureDone()
{
// warnings are fatal again if they go backwards
warningsfatal = 1;
configuredone_callback(RSPEC);
// Handler for instantiate submit button, which is in the page.
......@@ -152,11 +596,40 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
}
if (json.code) {
if (checkonly && json.code == 2) {
// Regenerate page with errors.
// Regenerate page with errors from the PHP fast
// type-checking code.
GenerateModalBody(formfields, json.value);
}
else {
sup.SpitOops("oops", json.value);
var newjsonval = null;
var ex;
//
// If geni-lib scripts error out, they can
// return a JSON list of errors and warnings.
// So, if the json.value return bits can be
// parsed by JSON.parse, assume they have
// meaning.
//
try {
newjsonval = JSON.parse(json.value);
}
catch (ex) {
newjsonval = null;
}
if (newjsonval != null) {
// Disable first-time warnings; too complicated
// to track which values caused warnings and have
// been changed...
warningsfatal = 0;
// These *are* the droids we're looking for...
GenerateModalBody(formfields, newjsonval);
}
else {
sup.SpitOops("oops", json.value);
}
}
steps_callback(false);
return;
......@@ -192,7 +665,8 @@ function(_, sup, JacksEditor, ppmodalString, ppbodyString, chooserString)
"BindParameters",
{"formfields" : formfields,
"uuid" : uuid,
"checkonly" : checkonly});
"checkonly" : checkonly,
"warningsfatal": warningsfatal});
xmlthing.done(callback);
}
......
......@@ -299,6 +299,12 @@ function Do_CheckScript()
SPITAJAX_ERROR(1, "Missing script");
return;
}
$warningsfatal = "";
if (isset($ajax_args["warningsfatal"]) && $ajax_args["warningsfatal"]) {
$warningsfatal = "-W";
}
$infname = tempnam("/tmp", "genilibin");
$outfname = tempnam("/tmp", "genilibout");
......@@ -312,7 +318,7 @@ function Do_CheckScript()
# Invoke the backend.
#
$retval = SUEXEC($this_uid, "nobody",
"webrungenilib -o $outfname $infname",
"webrungenilib $warningsfatal -o $outfname $infname",
SUEXEC_ACTION_IGNORE);
if ($retval != 0) {
......@@ -386,6 +392,12 @@ function Do_BindParameters()
SPITAJAX_RESPONSE(0);
return;
}
$warningsfatal = "";
if (isset($ajax_args["warningsfatal"]) && $ajax_args["warningsfatal"]) {
$warningsfatal = "-W";
}
$infname = tempnam("/tmp", "genilibin");
$parmfname = tempnam("/tmp", "genilibparm");
$outfname = tempnam("/tmp", "genilibout");
......@@ -405,7 +417,7 @@ function Do_BindParameters()
# Invoke the backend.
#
$retval = SUEXEC($this_uid, "nobody",
"webrungenilib -b $parmfname -o $outfname $infname",
"webrungenilib $warningsfatal -b $parmfname -o $outfname $infname",
SUEXEC_ACTION_CONTINUE);
if ($retval != 0) {
......
......@@ -506,18 +506,42 @@ class Profile
}
$fields = json_decode($json_data);
$defaults = array();
$form = "";
$formBasic = "";
$formAdvanced = "";
$formGroups = "";
while (list ($name, $val) = each ($fields)) {
$form = "";
$type = $val->type;
$prompt = $val->description;
$defval = $val->defaultValue;
$options = $val->legalValues;
$longhelp = $val->longDescription;
$advanced = $val->advanced;
$groupId = $val->groupId;
$groupName = $val->groupName;
$hasGroup = false;
$data_help_string = "";
$advanced_attr = "";
$defaults[$name] = $defval;
if (!isset($prompt) || !$prompt) {
$prompt = $name;
}
if (!isset($advanced)) {
$advanced = false;
}
# Let advanced-tagged params dominate groupId; we don't generate groupId yet anyway.
if ($advanced) {
$advanced_attr = " pp-param-group='advanced' pp-param-group-name='Advanced Parameters'";
}
else if (isset($groupId) && $groupId && isset($groupName) && $groupName) {