All new accounts created on Gitlab now require administrator approval. If you invite any collaborators, please let Flux staff know so they can approve the accounts.

Commit 18217eb3 authored by Leigh B Stoller's avatar Leigh B Stoller

New admin pages for experiment listing and project approval.

1. Get rid of the old myexperiments page and change all uses to the user
   dashboard or landing page.

   Replaced with admins only page for listing all experiments, pending
   extensions, locked down, expires, and older then 45 days.

2. New page for approving projects using modern technology, as per Rob's
   request in issue #304.
parent 53465b19
This diff is collapsed.
<?php
#
# Copyright (c) 2000-2017 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 = "Approve Projects";
#
# 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";
# Place to hang the toplevel template.
echo "<div id='page-body'></div>\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";
REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
SPITREQUIRE("js/approve-projects.js");
AddTemplateList(array("approve-projects", "project-approval-list",
"waitwait-modal", "oops-modal"));
SPITFOOTER();
?>
<?php
#
# Copyright (c) 2000-2017 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 = "Experiments";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLoginOrRedirect();
$isadmin = (ISADMIN() ? 1 : 0);
$isfadmin = (ISFOREIGN_ADMIN() ? 1 : 0);
if (! (ISADMIN() || ISFOREIGN_ADMIN())) {
SPITUSERERROR("You do not have permission to view this page");
}
SPITHEADER(1);
echo "<link rel='stylesheet'
href='css/tablesorter.css'>\n";
# Place to hang the toplevel template.
echo "<div id='page-body'></div>\n";
echo "<script type='text/javascript'>\n";
echo " window.ISADMIN = $isadmin;\n";
echo " window.ISFADMIN = $isfadmin;\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";
REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
SPITREQUIRE("js/experiments.js");
AddTemplateList(array("experiments", "experiment-list", "waitwait-modal", "oops-modal"));
SPITFOOTER();
?>
......@@ -394,7 +394,7 @@ function Do_VerifySpeaksfor()
else {
$blob["url"] = ($this_user->webonly() ||
!Instance::UserHasInstances($this_user)
? "instantiate.php" : "myexperiments.php");
? "instantiate.php" : "landing.php");
}
session_destroy();
SPITAJAX_RESPONSE($blob);
......
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['approve-projects',
'project-approval-list',
'waitwait-modal',
'oops-modal']);
var mainString = templates['approve-projects'];
var listString = templates['project-approval-list'];
var waitwaitString = templates['waitwait-modal'];
var oopsString = templates['oops-modal'];
var table = null;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
$('#page-body').html(mainString);
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadTable();
}
function LoadTable()
{
var callback = function(json) {
console.info(json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
var template = _.template(listString);
var html = template({"projects" : json.value});
$('#projects-content').html(html);
InitTable();
SetupActionButtons();
SetupProjectWhy();
$('#projects-loading').addClass("hidden");
$('#projects-loaded').removeClass("hidden");
};
sup.CallServerMethod(null, "approve-projects", "ProjectList",
null, callback);
}
function InitTable()
{
var tablename = "#projects-table";
var searchname = "#projects-search";
// Format dates with moment before display.
$('.format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html())
.format("ll"));
}
});
table = $(tablename)
.tablesorter({
theme : 'green',
// initialize zebra and filter widgets
widgets: ["zebra", "filter", "resizable"],
widgetOptions: {
// include child row content while filtering, if true
filter_childRows : true,
// include all columns in the search.
filter_anyMatch : true,
// 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,
}
});
// Bind the search box.
$.tablesorter.filter.bindSearch(table, $(searchname));
// Initial sort.
table.find('th:eq(2)').trigger('sort');
}
//
// Setup the action modals and buttons.
//
function SetupActionButtons()
{
$(".approve-button").click(function (event) {
event.preventDefault();
var pid = $(this).closest('tr').data("pid");
$("#approve-confirm").click(function(event) {
event.preventDefault();
Approve(pid);
});
sup.ShowModal("#approve-modal",
function () {
$("#approve-confirm").off("click");
});
});
$(".deny-button").click(function (event) {
event.preventDefault();
var pid = $(this).closest('tr').data("pid");
$("#deny-confirm").click(function(event) {
event.preventDefault();
Deny(pid);
});
sup.ShowModal("#deny-modal",
function () {
$("#deny-confirm").off("click");
});
});
$(".request-info-button").click(function (event) {
event.preventDefault();
var pid = $(this).closest('tr').data("pid");
$("#request-info-confirm").click(function(event) {
event.preventDefault();
MoreInfo(pid);
});
sup.ShowModal("#request-info-modal",
function () {
$("#request-info-confirm").off("click");
});
});
}
//
// Setup the description textareas for editing/save.
//
function SetupProjectWhy()
{
$('.collapse').on('show.bs.collapse', function () {
var pid = $(this).closest('tr').data("pid");
var chev = "#chevron-" + pid;
$(chev).removeClass("glyphicon-chevron-right");
$(chev).addClass("glyphicon-chevron-down");
});
$('.collapse').on('hide.bs.collapse', function () {
var pid = $(this).closest('tr').data("pid");
var chev = "#chevron-" + pid;
$(chev).removeClass("glyphicon-chevron-down");
$(chev).addClass("glyphicon-chevron-right");
});
$(".project-why")
.on("change input paste keyup", function() {
var pid = $(this).closest('tr').data("pid");
var save = "#why-save-button-" + pid;
$(save).removeClass('hidden');
});
$('.why-save-button').click(function (event) {
event.preventDefault();
var pid = $(this).closest('tr').data("pid");
var save = "#why-save-button-" + pid;
SaveProjectWhy(this, function () {
$(save).addClass('hidden');
});
});
}
// Target is the textarea that changed, need to find the pid from <tr>
function SaveProjectWhy(target, done)
{
var pid = $(target).closest('tr').data("pid");
var area = "#textarea-" + pid;
var description = $(area).val();
console.info("SaveProjectWhy: ", pid, description);
var callback = function(json) {
if (json.code) {
sup.SpitOops("oops", "Failed to save project description: " +
json.value);
return;
}
done();
};
var xmlthing = sup.CallServerMethod(null, "approve-projects",
"SaveDescription",
{"pid" : pid,
"description" : description});
xmlthing.done(callback);
}
// Request more info.
function MoreInfo(pid)
{
var message = $('#info-body').val();
sup.HideModal("#request-info-modal");
console.info("MoreInfo", pid, message);
var callback = function(json) {
if (json.code) {
sup.SpitOops("oops", "Failed to send request for more info: " +
json.value);
return;
}
};
var xmlthing = sup.CallServerMethod(null, "approve-projects",
"MoreInfo",
{"pid" : pid,
"message" : message});
xmlthing.done(callback);
}
// Approve
function Approve(pid)
{
sup.HideModal("#approve-modal");
var message = $('#approve-body').val();
console.info("Approve", pid, message);
var callback = function(json) {
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", "Failed to approve project: " +
json.value);
return;
}
// Remove the project from the list. There are two rows.
$('tr[data-pid="' + pid + '"]').remove();
table.trigger('update');
// First project.
if (json.value) {
sup.ShowModal('#first-project-modal');
}
};
sup.ShowWaitWait("Approving project, this takes a minute or two. " +
"Patience please.");
var xmlthing = sup.CallServerMethod(null, "approve-projects",
"Approve",
{"pid" : pid,
"message" : message});
xmlthing.done(callback);
}
// Deny
function Deny(pid)
{
sup.HideModal("#deny-modal");
var message = $('#deny-body').val();
var deleteuser = ($('#deny-delete-user').is(":checked") ? 1 : 0);
console.info("Deny", pid, message, deleteuser);
var callback = function(json) {
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", "Failed to destroy project: " +
json.value);
return;
}
// Remove the project from the list. There are two rows.
$('tr[data-pid="' + pid + '"]').remove();
table.trigger('update');
};
sup.ShowWaitWait("Destroying project, this takes a minute. " +
"Patience please.");
var xmlthing = sup.CallServerMethod(null, "approve-projects",
"Deny",
{"pid" : pid,
"message" : message,
"deleteuser" : deleteuser});
xmlthing.done(callback);
}
$(document).ready(initialize);
});
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['experiments',
'experiment-list',
'waitwait-modal',
'oops-modal']);
var mainString = templates['experiments'];
var listString = templates['experiment-list'];
var waitwaitString = templates['waitwait-modal'];
var oopsString = templates['oops-modal'];
function initialize()
{
window.APT_OPTIONS.initialize(sup);
$('#page-body').html(mainString);
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadTable();
}
function LoadTable()
{
var callback = function(json) {
console.info(json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
var template = _.template(listString);
var html = template({"experiments" : json.value,
"showCreator" : true,
"showProject" : true,
});
$('#experiments_content').html(html);
InitTable();
$('#experiments_loading').addClass("hidden");
$('#experiments_loaded').removeClass("hidden");
};
sup.CallServerMethod(null, "experiments", "ExperimentList",
null, callback);
}
function InitTable()
{
var tablename = "#experiments_table";
var searchname = "#experiments_search";
// Format dates with moment before display.
$('.format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html())
.format("ll"));
}
});
// This activates the tooltip subsystem.
$('[data-toggle="tooltip"]').tooltip({
delay: {"hide" : 500, "show" : 150},
placement: 'auto',
});
var table = $(tablename)
.tablesorter({
theme : 'green',
// initialize zebra and filter widgets
widgets: ["zebra", "filter", "resizable"],
headers: {
0: {
sorter : "text",
}
},
widgetOptions: {
// include child row content while filtering, if true
filter_childRows : true,
// include all columns in the search.
filter_anyMatch : true,
// 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,
}
});
/*
* We have to implement our own live search cause we want to combine
* the search box with the checkbox filters. To do that, we have to
* call SetFilters() on the table directly.
*/
var search_timeout = null;
$("#experiments_search").on("search keyup", function (event) {
var userInput = $("#experiments_search").val();
window.clearTimeout(search_timeout);
search_timeout =
window.setTimeout(function() {
var filters = $.tablesorter.getFilters(table);
filters[12] = userInput;
//console.info("Search", filters);
$.tablesorter.setFilters(table, filters, true);
}, 500);
});
// Bind handlers for the radio buttons
$('#radio-buttons input').change(function (e) {
e.preventDefault();
/*
* The use of data-id is to avoid page jumping when changing
* the page hash; it wants to jump to the radio buttons.
*/
// Change hash for page-reload
var hash = $(e.target).data("id");
window.location.hash = hash;
// SetFilters() is called below in hashchange handler.
});
// Update the count of matched experiments
table.bind('filterEnd', function(e, filter) {
$('#experiments_count').text(filter.filteredRows);
});
// Javascript to enable link to radio button
var hash = document.location.hash;
if (hash) {
/*
* The use of data-id is to avoid page jumping when changing
* the page hash; it wants to jump to the radio buttons.
*/
$('#radio-buttons [data-id="' + hash +'"]').prop("checked", true);
}
// Set the correct radio when a user uses their back/forward button
$(window).on('hashchange', function (e) {
var hash = window.location.hash;
if (hash == "") {
hash = "#all";
}
/*
* The use of data-id is to avoid page jumping when changing
* the page hash; it wants to jump to the radio buttons.
*/
$('#radio-buttons [data-id="' + hash +'"]').prop("checked", true);
SetFilters(table);
});
SetFilters(table);
// Initial sort.
table.find('th:eq(0)').trigger('sort');
}
function SetFilters(table)
{
var tmp = [];
var filters = $.tablesorter.getFilters(table);
// The "any" filter needs a value or everything disappears.
// If there is a term in the search box, it will have a value.
if (filters[12] === undefined) {
filters[12] = "";
}
if ($('#radio-buttons [data-id="#extending"]').is(":checked")) {
tmp.push("extending");
}
if ($('#radio-buttons [data-id="#locked"]').is(":checked")) {
tmp.push("locked");
}
if ($('#radio-buttons [data-id="#expired"]').is(":checked")) {
tmp.push("expired");
}
if ($('#radio-buttons [data-id="#old"]').is(":checked")) {
tmp.push("old");
}
if (tmp.length) {
// regex search, plain | does not work.
filters[11] = "/" + tmp.join("|") + "/";
}
else {
// Hmm, an empty string will get everything.
filters[11] = "";
}
//console.info("SetFilters", filters);
$.tablesorter.setFilters(table, filters, true);
}
$(document).ready(initialize);
});
......@@ -423,36 +423,37 @@ echo " <li class='divider'></li>
}
echo " </a></li>\n";
}
echo " <li><a href='dashboard.php'>DashBoard</a></li>";
echo " <li><a href='cluster-status.php'>Cluster Status</a></li>";
$then = time() - (30 * 3600 * 24);
echo " <li><a href='activity.php?min=$then'>
echo " <li><a href='dashboard.php'>DashBoard</a></li>";
echo " <li><a href='cluster-status.php'>Cluster Status</a></li>";
$then = time() - (30 * 3600 * 24);
echo " <li><a href='activity.php?min=$then'>