Commit 2bf0890c authored by Leigh Stoller's avatar Leigh Stoller

Another weekend project; replace the Edit Site Variables page

with a modern implementation.
parent 5d202969
/*! Widget: cssStickyHeaders - updated 5/24/2017 (v2.28.11) *//*
* Requires a modern browser, tablesorter v2.8+
*/
/*jshint jquery:true, unused:false */
;(function($, window){
'use strict';
var ts = $.tablesorter;
ts.addWidget({
id: 'cssStickyHeaders',
priority: 10,
options: {
cssStickyHeaders_offset : 0,
cssStickyHeaders_addCaption : false,
// jQuery selector or object to attach sticky header to
cssStickyHeaders_attachTo : null,
cssStickyHeaders_filteredToTop : true
},
init : function(table, thisWidget, c, wo) {
var offst, adjustY,
$table = c.$table,
$attach = $(wo.cssStickyHeaders_attachTo),
// target all versions of IE
isIE = 'ActiveXObject' in window || window.navigator.userAgent.indexOf('Edge') > -1,
namespace = c.namespace + 'cssstickyheader ',
$thead = $table.children('thead'),
$caption = $table.children('caption'),
$win = $attach.length ? $attach : $(window),
$parent = $table.parent().closest('table.' + ts.css.table),
$parentThead = $parent.length && ts.hasWidget($parent[0], 'cssStickyHeaders') ? $parent.children('thead') : [],
borderTopWidth = ( parseInt( $table.css('border-top-width'), 10 ) || 0 ),
// Fixes for Safari
tableH = $table.height(),
lastCaptionSetting = wo.cssStickyHeaders_addCaption,
// table offset top changes while scrolling in FF
adjustOffsetTop = false,
addCaptionHeight = false,
setTransform = function( $elms, y ) {
var translate = y === 0 ? '' : 'translate(0px,' + y + 'px)';
$elms.css({
'transform' : translate,
'-ms-transform' : translate,
'-webkit-transform' : translate
});
};
// Firefox fixes
if ($caption.length) {
// Firefox does not include the caption height when getting the table height
// see https://bugzilla.mozilla.org/show_bug.cgi?id=820891, so lets detect it instead of browser sniff
$caption.hide();
addCaptionHeight = $table.height() === tableH;
$caption.show();
// Firefox changes the offset().top when translating the table caption
offst = $table.offset().top;
setTransform( $caption, 20 );
adjustOffsetTop = $table.offset().top !== offst;
setTransform( $caption, 0 );
}
$win
.unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') )
.bind('scroll resize '.split(' ').join(namespace), function() {
// make sure "wo" is current otherwise changes to widgetOptions
// are not dynamic (like the add caption button in the demo)
wo = c.widgetOptions;
if ( adjustOffsetTop ) {
// remove transform from caption to get the correct offset().top value
setTransform( $caption, 0 );
adjustY = $table.offset().top;
}
// Fix for safari, when caption present, table
// height changes while scrolling
if ($win.scrollTop() < $caption.outerHeight(true)) {
tableH = $table.height();
}
var top = $attach.length ? $attach.offset().top : $win.scrollTop(),
// add caption height; include table padding top & border-spacing or text may be above the fold (jQuery UI themes)
// border-spacing needed in Firefox, but not webkit... not sure if I should account for that
captionHeight = ( $caption.outerHeight(true) || 0 ) +
( parseInt( $table.css('padding-top'), 10 ) || 0 ) +
( parseInt( $table.css('border-spacing'), 10 ) || 0 ),
bottom = tableH + ( addCaptionHeight && wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) -
$thead.height() - ( $table.children('tfoot').height() || 0 ) -
( wo.cssStickyHeaders_addCaption ? captionHeight : ( addCaptionHeight ? 0 : captionHeight ) ),
parentTheadHeight = $parentThead.length ? $parentThead.height() : 0,
// get bottom of nested sticky headers
nestedStickyBottom = $parentThead.length ? (
isIE ? $parent.data('cssStickyHeaderBottom') + parentTheadHeight :
$parentThead.offset().top + parentTheadHeight - $win.scrollTop()
) : 0,
// in this case FF's offsetTop changes while scrolling, so we get a saved offsetTop before scrolling occurs
// but when the table is inside a wrapper ($attach) we need to continually update the offset top
tableOffsetTop = adjustOffsetTop ? adjustY : $table.offset().top,
offsetTop = addCaptionHeight ? tableOffsetTop - ( wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) : tableOffsetTop,
// Detect nested tables - fixes #724
deltaY = top - offsetTop + nestedStickyBottom + borderTopWidth + ( wo.cssStickyHeaders_offset || 0 ) -
( wo.cssStickyHeaders_addCaption ? ( addCaptionHeight ? captionHeight : 0 ) : captionHeight ),
finalY = deltaY > 0 && deltaY <= bottom ? deltaY : 0,
// All IE (even IE11) can only transform header cells - fixes #447 thanks to @gakreol!
$cells = isIE ? $thead.children().children() : $thead;
// more crazy IE stuff...
if (isIE) {
// I didn't bother testing 3 nested tables deep in IE, because I hate it
c.$table.data( 'cssStickyHeaderBottom', ( $parentThead.length ? parentTheadHeight : 0 ) -
( wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) );
}
if (wo.cssStickyHeaders_addCaption) {
$cells = $cells.add($caption);
}
if (lastCaptionSetting !== wo.cssStickyHeaders_addCaption) {
lastCaptionSetting = wo.cssStickyHeaders_addCaption;
// reset caption position if addCaption option is dynamically changed to false
if (!lastCaptionSetting) {
setTransform( $caption, 0 );
}
}
setTransform( $cells, finalY );
});
$table
.unbind( ('filterEnd' + namespace).replace(/\s+/g, ' ') )
.bind('filterEnd' + namespace, function() {
if (wo.cssStickyHeaders_filteredToTop) {
// scroll top of table into view
window.scrollTo(0, $table.position().top);
}
});
},
remove: function(table, c, wo, refreshing) {
if (refreshing) { return; }
var namespace = c.namespace + 'cssstickyheader ';
$(window).unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') );
c.$table
.unbind( ('filterEnd scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') )
.add( c.$table.children('thead').children().children() )
.children('thead, caption').css({
'transform' : '',
'-ms-transform' : '',
'-webkit-transform' : ''
});
}
});
})(jQuery, window);
......@@ -339,6 +339,45 @@ function VerifySpeaksfor(speaksfor, signature)
$xmlthing.done(callback);
}
function ConfirmModal(args)
{
var modal = '#' + args.modal;
var cancel_function = args.cancel_function;
var confirm_function = args.confirm_function;
var function_data = args.function_data;
if (args.prompt) {
$(modal + ' .prompt').html(args.prompt);
}
else {
$(modal + ' .prompt').html("Confirm?");
}
$(modal).on('hidden.bs.modal', function (event) {
$(this).unbind(event);
$(modal + ' .confirm-button').off("click");
$(modal + ' .cancel-button').off("click");
if (cancel_function !== undefined && cancel_function) {
cancel_function(function_data);
}
});
$(modal + ' .confirm-button').click(function (event) {
$(modal).off('hidden.bs.modal');
HideModal(modal, function(event) {
$(modal + ' .confirm-button').off("click");
$(modal + ' .cancel-button').off("click");
if (confirm_function !== undefined && confirm_function) {
confirm_function(function_data);
}
});
});
$(modal + ' .cancel-button').click(function (event) {
// cancel callback called above.
HideModal(modal);
});
ShowModal(modal);
}
// Input is an image urn.
// Returns a pretty image name.
function ImageDisplay(v)
......@@ -378,6 +417,7 @@ return {
StartGeniLogin: StartGeniLogin,
InitGeniLogin: InitGeniLogin,
ImageDisplay: ImageDisplay,
ConfirmModal: ConfirmModal,
};
})();
});
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['sitevars',
'waitwait-modal', 'oops-modal',
'confirm-something']);
var template = _.template(templates['sitevars']);
var waitwait = templates['waitwait-modal'];
var oops = templates['oops-modal'];
var confirmstr = templates['confirm-something'];
var sitevars = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
$('#waitwait_div').html(waitwait);
$('#oops_div').html(oops);
$('#confirm_div').html(confirmstr);
LoadSitevars();
}
function LoadSitevars()
{
var callback = function(json) {
console.log(json);
if (json.code) {
console.log("Could not get status data: " + json.value);
return;
}
sitevars = json.value;
RenderPage();
};
var args = null;
var xmlthing = sup.CallServerMethod(null, "sitevars",
"GetSitevars", args);
xmlthing.done(callback);
}
function RenderPage()
{
var html = template({"sitevars" : sitevars});
$('#page-body').html(html);
var table = $('#sitevars-table')
.tablesorter({
theme : 'green',
// initialize zebra and filter widgets
widgets: ["zebra", "stickyHeaders", "filter"],
widgetOptions: {
// include child row content while filtering, if true
filter_childRows : true,
// include all columns in the search.
filter_anyMatch : false,
// class name applied to filter row and each input
filter_cssFilter : 'form-control',
// search from beginning
filter_startsWith : false,
// Set this option to false for case sensitive search
filter_ignoreCase : true,
// Only one search box.
filter_columnFilters : false,
},
});
$.tablesorter.filter.bindSearch(table, $('.form-control.search'));
// Bind the row edit buttons.
$(".edit-button").click(function (event) {
event.preventDefault();
HandleEditButton($(this));
});
// Bind the row reset buttons.
$(".reset-button").click(function (event) {
event.preventDefault();
HandleResetButton($(this));
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
placement: 'auto',
});
}
// Handle the edit button.
function HandleEditButton(target)
{
var row = $(target).closest("tr");
var name = $(row).attr('data-name');
var cval = $(row).find(".current-value");
var editing = false;
// We are in edit mode if the editing area is visible
if (! $(cval).find(".editing").hasClass("hidden")) {
editing = true;
}
console.info(name, editing, target);
// If already editing, ignore the button.
if (editing) {
return;
}
// Switch the column to the editable textarea.
$(cval).find(".notediting").addClass("hidden");
$(cval).find(".editing").removeClass("hidden");
// Bind handlers for save/cancel buttons.
$(cval).find(".cancel-button").click(function (event) {
console.info("cancel edit");
// Switch the column back to the normal display
$(cval).find(".notediting").removeClass("hidden");
$(cval).find(".editing").addClass("hidden");
$('.cancel-button').off("click");
$('.save-button').off("click");
});
$(cval).find(".save-button").click(function (event) {
console.info("save edit");
sup.ConfirmModal({
"modal" : "confirm-something",
"prompt" : "Save new site variable value?",
"cancel_function" : function (data) {
// Nothing to do, user still sees the edit view.
},
"confirm_function" : function (data) {
var newvalue = $(cval).find("textarea").val();
var callback = function (json) {
console.info(json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
// Switch the column back to the normal display
$(cval).find(".notediting").html(_.escape(newvalue));
$(cval).find(".notediting").removeClass("hidden");
$(cval).find(".editing").addClass("hidden");
$('.cancel-button').off("click");
$('.save-button').off("click");
};
var args = {
"name" : name,
"value" : newvalue,
};
sup.CallServerMethod(null, "sitevars", "SetSitevar",
args, callback);
},
});
});
}
// Handle the reset button.
function HandleResetButton(target)
{
var row = $(target).closest("tr");
var name = $(row).attr('data-name');
var cval = $(row).find(".current-value");
console.info("HandleResetButton");
// We are in edit mode if the editing area is visible
if (! $(cval).find(".editing").hasClass("hidden")) {
alert("Already editing, please cancel or save the edit first");
return;
}
sup.ConfirmModal({
"modal" : "confirm-something",
"prompt" : "Reset site variable to default value?",
"cancel_function" : function (data) {
// Nothing to do
},
"confirm_function" : function (data) {
var callback = function (json) {
console.info(json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
// Set the current value to the default value from DB.
$(cval).find(".notediting").html(_.escape(json.value));
// Also the textarea in case the user then edits the value.
$(cval).find("textarea").html(json.value);
};
var args = {
"name" : name,
};
sup.CallServerMethod(null, "sitevars", "ResetSitevar",
args, callback);
},
});
}
$(document).ready(initialize);
});
......@@ -503,8 +503,8 @@ if (!$login_user->portal()) {
Users/Projects</a></li>
<li><a href='approve-projects.php'>
Approve new projects</a></li>
<li><a href='edit-news.php'>
Add a news item</a></li>";
<li><a href='sitevars.php'>
Edit Site Variables</a></li>";
echo " </ul>
</li>\n";
}
......
......@@ -439,6 +439,15 @@ $routing = array("geni-login" =>
"guest" => false,
"methods" => array("AggregateStatus" =>
"Do_AggregateStatus")),
"sitevars" =>
array("file" => "sitevars.ajax",
"guest" => false,
"methods" => array("GetSitevars" =>
"Do_GetSitevars",
"SetSitevar" =>
"Do_SetSitevar",
"ResetSitevar" =>
"Do_ResetSitevar")),
);
#
......
<?php
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
function Do_GetSitevars()
{
global $this_user;
global $ajax_args;
$blob = array();
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Not enough permission");
return -1;
}
$result =
DBQueryFatal("select * from sitevariables order by name");
while ($row = mysql_fetch_array($result)) {
$name = $row["name"];
$curvalue = $row["value"];
$defvalue = $row["defaultvalue"];
$description = $row["description"];
$blob[$name] =
array("name" => $name,
"current_value" => $curvalue,
"default_value" => $defvalue,
"description" => $description);
}
SPITAJAX_RESPONSE($blob);
}
function Do_SetSitevar()
{
global $this_user;
global $ajax_args;
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Not enough permission");
return -1;
}
if (!isset($ajax_args["name"]) || $ajax_args["name"] == "") {
SPITAJAX_ERROR(1, "Missing name argument");
return 1;
}
if (!isset($ajax_args["value"])) {
SPITAJAX_ERROR(1, "Missing value argument");
return 1;
}
$name = $ajax_args["name"];
$value = $ajax_args["value"];
if (!TBSiteVarExists($name)) {
SPITAJAX_ERROR(1, "No such site variable");
return 1;
}
# The backend does this test, so do it here for better error message.
if (!TBcheck_dbslot($value, "sitevariables", "value",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
SPITAJAX_ERROR(1, "Illegal value: " . TBFieldErrorString());
return 1;
}
if (1) {
$result = SetSiteVar($name, array("value" => $value), $errors);
if (!$result) {
SPITAJAX_ERROR(1, "Could not update site variable!");
return 1;
}
}
SPITAJAX_RESPONSE(1);
}
function Do_ResetSitevar()
{
global $this_user;
global $ajax_args;
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Not enough permission");
return -1;
}
if (!isset($ajax_args["name"]) || $ajax_args["name"] == "") {
SPITAJAX_ERROR(1, "Missing name argument");
return 1;
}
$name = $ajax_args["name"];
$safe_name = addslashes($name);
if (!TBSiteVarExists($name)) {
SPITAJAX_ERROR(1, "No such site variable");
return 1;
}
if (1) {
$result = SetSiteVar($name, array("reset" => 1), $errors);
if (!$result) {
SPITAJAX_ERROR(1, "Could not reset to default value!");
return 1;
}
}
$query_result =
DBQueryFatal("select defaultvalue from sitevariables ".
"where name='$safe_name'");
if (!mysql_num_rows($query_result)) {
SPITAJAX_ERROR(1, "Could not get default value after reset!");
return 1;
}
$row = mysql_fetch_array($query_result);
$defaultvalue = $row["defaultvalue"];
SPITAJAX_RESPONSE($defaultvalue);
}
# Local Variables:
# mode:php
# End:
?>
<?php
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include("defs.php3");
include_once("geni_defs.php");
chdir("apt");
include("quickvm_sup.php");
$page_title = "Site Variables";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLoginOrRedirect();
$isadmin = (ISADMIN() ? 1 : 0);
if (!ISADMIN()) {
SPITUSERERROR("You do not have permission to view this page");
}
SPITHEADER(1);
echo "<link rel='stylesheet'
href='css/tablesorter.css'>\n";
echo "<div>
<div id='page-body'></div>
<div id='waitwait_div'></div>
<div id='oops_div'></div>
<div id='confirm_div'></div>
</div>\n";
echo "<script type='text/javascript'>\n";
echo " window.ISADMIN = $isadmin;\n";
echo "</script>\n";
echo "<script src='js/lib/jquery-2.0.3.min.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.min.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.widgets.min.js'></script>\n";
echo "<script src='js/lib/jquery.tablesorter.widget-stickyHeaders.js'></script>\n";
REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
SPITREQUIRE("js/sitevars.js");
AddTemplateList(array("sitevars", "waitwait-modal", "oops-modal",
"confirm-something"));
SPITFOOTER();
?>
<div id='confirm-something' class='modal fade'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class='modal-header'>
<center><h4 class="prompt">Confirm?</h4></center>
</div>
<div class='modal-body'>
<center>
<button style='margin-right: 20px;'
class='btn btn-primary btn-sm cancel-button'>
Cancel</button>
<button class='btn btn-danger btn-sm confirm-button'>
Confirm</button>
</center>
</div>
</div>
</div>
</div>
<style>
.tablesorter .filtered {
display: none;
}
</style>
<div class="container-fluid">
<div class="row">
<table class='tablesorter' id='sitevars-table'>
<thead>
<tr>
<th>Name</th>
<th class="sorter-false">
Default Value</th>
<th class="sorter-false">
Current Value</th>
</tr>
<tr>
<th class="sorter-false">
<input class='form-control search'
style="margin-left: -15px;"
type='search' data-column='all'
placeholder='Search'></th>
<th colspan="2" class="sorter-false">Description</th>
</tr>
</thead>
<tbody>
<% _.each(sitevars, function(value, name) { %>
<tr data-name="<%- value.name %>">
<td rowspan="2"><%- value.name %>
<a href="#" class="pull-right edit-button"
style="margin-left: 5px;">
<span class="glyphicon glyphicon-edit"
style='margin-bottom: 4px;'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='Change current value'></span></a>
<a href="#" class="pull-right reset-button">
<span class="glyphicon glyphicon-erase"
style='margin-bottom: 4px;'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='Reset to default value'></span></a>
</td>
<td class="default-value">
<% if (value.default_value) { %>
<%- value.default_value %>
<% } else { %>&nbsp;<% } %>
</td>