Commit 6228a1b9 authored by Leigh Stoller's avatar Leigh Stoller

Add admin list/search page for users and projects. The first two tabs are

recently and currently active users and projects (these lists are portal
specific!). The second two tabs do dynamic search of the database via ajax
calls, to populate the lists. This is cause listing all users statically on
the page would be insane (tablesorter does not perform well on such giant
lists).

Plus other little related tweaks.
parent aacd1ef1
......@@ -22,6 +22,76 @@
# }}}
#
function ClassicExperimentList($which, $target, $state = "active")
{
global $urn_mapping, $TBBASE;
global $this_user;
if ($state == "active") {
$stateclause = "and (e.state='active' or e.state='activating')";
}
elseif ($state == "inactive") {
$stateclause = "and (e.state='swapped')";
}
if ($which == "user") {
$target_idx = $target->uid_idx();
$whereclause = "where e.swapper_idx='$target_idx'";
}
else {
$target_pid = $target->pid();
$whereclause = "where e.pid='$target_pid'";
}
$results = array();
$query_result =
DBQueryFatal("select idx from experiments as e ".
"$whereclause $stateclause");
while ($row = mysql_fetch_array($query_result)) {
$idx = $row["idx"];
$experiment = Experiment::Lookup($idx);
$resources = $experiment->GetResources();
$stats = $experiment->GetStats();
$url = $TBBASE . "/" . CreateURL("showexp", $experiment);
$creator_uid = $experiment->creator();
$pid = $experiment->pid();
$eid = $experiment->eid();
$swapped = DateStringGMT($stats->swapin_last());
$created = DateStringGMT($experiment->created());
$pcount = $experiment->PCCount();
$phours = sprintf("%.2f",(($pcount * $experiment->SwapSeconds()) /
3600.0));
$blob = array();
$blob["idx"] = $idx;
$blob["pid"] = "<a href='show-project.php?project=$pid'>$pid</a>";
$blob["eid"] = "<a href='$url'>$eid</a>";
if (ISADMIN() || $which == "project") {
$blob["creator"] =
"<a href='user-dashboard.php?user=$creator_uid'>".
"$creator_uid</a>";
}
else {
$blob["creator"] = $creator_uid;
}
$cluster = "Emulab";
$blob["cluster"] = "Emulab";
$blob["pcount"] = $pcount;
$blob["phours"] = $phours;
$blob["vcount"] = $resources->vnodes();
$blob["swapped"] = $swapped;
$blob["created"] = $created;
$blob["state"] = $experiment->state();
$blob["description"] = CleanString($experiment->description());
$results["$pid:$eid"] = $blob;
}
return $results;
}
function ExperimentList($which, $target)
{
global $urn_mapping, $TBBASE;
......
require(window.APT_OPTIONS.configObject,
['underscore', 'js/quickvm_sup',
'js/lib/text!template/lists.html'
],
function (_, sup, mainString)
{
'use strict';
var mainTemplate = _.template(mainString);
function initialize()
{
window.APT_OPTIONS.initialize(sup);
var userlist = decodejson('#users-json');
var projlist = decodejson('#projects-json');
// Generate the main template.
var html = mainTemplate({
"users" : userlist,
"projects" : projlist,
});
$('#main-body').html(html);
InitTable("users");
InitTable("projects");
// Start out as empty tables.
$('#search_users_table')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
$('#search_projects_table')
.tablesorter({
theme : 'green',
// initialize zebra
widgets: ["zebra"],
});
// Javascript to enable link to tab
var hash = document.location.hash;
if (hash) {
$('.nav-tabs a[href='+hash+']').tab('show');
}
// Change hash for page-reload
$('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
window.location.hash = e.target.hash;
});
var search_users_timeout = null;
$("#search_users_search").on("keyup", function (event) {
var userInput = $("#search_users_search").val();
userInput = userInput.toLowerCase();
window.clearTimeout(search_users_timeout);
search_users_timeout =
window.setTimeout(function() {
if (userInput.length < 3) {
return;
}
UpdateUserSearch(userInput);
}, 500);
});
var search_projects_timeout = null;
$("#search_projects_search").on("keyup", function (event) {
var userInput = $("#search_projects_search").val();
userInput = userInput.toLowerCase();
window.clearTimeout(search_users_timeout);
search_users_timeout =
window.setTimeout(function() {
if (userInput.length < 3) {
return;
}
UpdateProjectSearch(userInput);
}, 500);
});
}
function InitTable(name)
{
var tablename = "#" + name + "_table";
var searchname = "#" + name + "_search";
var table = $(tablename)
.tablesorter({
theme : 'green',
// initialize zebra and filter widgets
widgets: ["zebra", "filter"],
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,
}
});
// Target the $('.search') input using built in functioning
// this binds to the search using "search" and "keyup"
// Allows using filter_liveSearch or delayed search &
// pressing escape to cancel the search
$.tablesorter.filter.bindSearch(table, $(searchname));
}
function UpdateUserSearch(text)
{
var callback = function(json) {
console.info(json);
if (json.code) {
console.info(json.value);
return;
}
var html = "";
for (var i in json.value) {
var user = json.value[i];
html = html +
"<tr>" +
"<td><a href='user-dashboard.php?user=" + user.usr_uid + "'>" +
user.usr_uid + "</a></td>" +
"<td>" + user.usr_name + "</td>" +
"<td>" + user.usr_affil + "</td></tr>";
}
$('#search_users_table tbody').html(html);
$('#search_users_table').trigger("update", [false]);
};
var xmlthing = sup.CallServerMethod(null,
"lists", "SearchUsers",
{"text" : text});
xmlthing.done(callback);
}
function UpdateProjectSearch(text)
{
var callback = function(json) {
console.info(json);
if (json.code) {
console.info(json.value);
return;
}
var html = "";
for (var i in json.value) {
var project = json.value[i];
html = html +
"<tr>" +
"<td><a href='show-project.php?project=" + project.pid + "'>" +
project.pid + "</a></td>" +
"<td><a href='user-dashboard.php?user=" + project.usr_uid + "'>" +
project.usr_name + "</a></td>" +
"<td>" + project.usr_affil + "</td></tr>";
}
$('#search_projects_table tbody').html(html);
$('#search_projects_table').trigger("update", [false]);
};
var xmlthing = sup.CallServerMethod(null,
"lists", "SearchProjects",
{"text" : text});
xmlthing.done(callback);
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
......@@ -5,9 +5,11 @@ require(window.APT_OPTIONS.configObject,
'js/lib/text!template/profile-list.html',
'js/lib/text!template/member-list.html',
'js/lib/text!template/project-profile.html',
'js/lib/text!template/classic-explist.html',
],
function (_, sup, moment, mainString,
experimentString, profileString, memberString, detailsString)
experimentString, profileString, memberString, detailsString,
classicString)
{
'use strict';
var mainTemplate = _.template(mainString);
......@@ -44,7 +46,9 @@ function (_, sup, moment, mainString,
LoadUsage();
LoadExperimentTab();
LoadClassicExperiments();
LoadProfileTab();
LoadClassicProfiles();
LoadMembersTab();
LoadProjectTab();
}
......@@ -131,6 +135,41 @@ function (_, sup, moment, mainString,
xmlthing.done(callback);
}
function LoadClassicExperiments()
{
var callback = function(json) {
console.info("classic", json);
if (json.code) {
console.info(json.value);
return;
}
var template = _.template(classicString);
$('#classic_experiments_content')
.html(template({"experiments" : json.value,
"showCreator" : true,
"showProject" : false,
"asProfiles" : false}));
// Format dates with moment before display.
$('#classic_experiments_content .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("ll"));
}
});
var table = $('#classic_experiments_content .tablesorter')
.tablesorter({
theme : 'green',
});
};
var xmlthing = sup.CallServerMethod(null,
"show-project", "ClassicExperimentList",
{"pid" : window.TARGET_PROJECT});
xmlthing.done(callback);
}
function LoadProfileTab()
{
var callback = function(json) {
......@@ -193,6 +232,41 @@ function (_, sup, moment, mainString,
xmlthing.done(callback);
}
function LoadClassicProfiles()
{
var callback = function(json) {
console.info("classic profiles", json);
if (json.code) {
console.info(json.value);
return;
}
var template = _.template(classicString);
$('#classic_profiles_content')
.html(template({"experiments" : json.value,
"showCreator" : true,
"showProject" : false,
"asProfiles" : true}));
// Format dates with moment before display.
$('#classic_profiles_content .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("ll"));
}
});
var table = $('#classic_profiles_content .tablesorter')
.tablesorter({
theme : 'green',
});
};
var xmlthing = sup.CallServerMethod(null,
"show-project", "ClassicProfileList",
{"pid" : window.TARGET_PROJECT});
xmlthing.done(callback);
}
function ShowTopology(profile)
{
var index;
......
......@@ -7,10 +7,11 @@ require(window.APT_OPTIONS.configObject,
'js/lib/text!template/user-profile.html',
'js/lib/text!template/oops-modal.html',
'js/lib/text!template/waitwait-modal.html',
'js/lib/text!template/classic-explist.html',
],
function (_, sup, moment, mainString,
experimentString, profileListString, projectString,
profileString, oopsString, waitwaitString)
profileString, oopsString, waitwaitString, classicString)
{
'use strict';
var mainTemplate = _.template(mainString);
......@@ -49,8 +50,10 @@ function (_, sup, moment, mainString,
LoadUsage();
LoadExperimentTab();
LoadClassicExperiments();
// Should we do these on demand?
LoadProfileListTab();
LoadClassicProfiles();
LoadProjectsTab();
LoadProfileTab();
......@@ -145,6 +148,41 @@ function (_, sup, moment, mainString,
xmlthing.done(callback);
}
function LoadClassicExperiments()
{
var callback = function(json) {
console.info("classic", json);
if (json.code) {
console.info(json.value);
return;
}
var template = _.template(classicString);
$('#classic_experiments_content')
.html(template({"experiments" : json.value,
"showCreator" : false,
"showProject" : true,
"asProfiles" : false}));
// Format dates with moment before display.
$('#classic_experiments_content .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("ll"));
}
});
var table = $('#classic_experiments_content .tablesorter')
.tablesorter({
theme : 'green',
});
};
var xmlthing = sup.CallServerMethod(null,
"user-dashboard", "ClassicExperimentList",
{"uid" : window.TARGET_USER});
xmlthing.done(callback);
}
function LoadProfileListTab()
{
var callback = function(json) {
......@@ -207,6 +245,40 @@ function (_, sup, moment, mainString,
xmlthing.done(callback);
}
function LoadClassicProfiles()
{
var callback = function(json) {
console.info("classic profiles", json);
if (json.code) {
console.info(json.value);
return;
}
var template = _.template(classicString);
$('#classic_profiles_content')
.html(template({"experiments" : json.value,
"showCreator" : false,
"showProject" : true,
"asProfiles" : true}));
$('#classic_profiles_content .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("ll"));
}
});
var table = $('#classic_profiles_content .tablesorter')
.tablesorter({
theme : 'green',
});
};
var xmlthing = sup.CallServerMethod(null,
"user-dashboard", "ClassicProfileList",
{"uid" : window.TARGET_USER});
xmlthing.done(callback);
}
function ShowTopology(profile)
{
var index;
......
<?php
#
# Copyright (c) 2000-2016 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("..");
chdir("apt");
include_once("profile_defs.php");
include_once("instance_defs.php");
include_once("ajax-routines.ajax");
# We set this in CheckPageArgs
$target_project = null;
#
# Need to check the permission, since we allow admins to mess with
# other accounts.
#
function CheckPageArgs()
{
global $this_user;
global $ajax_args;
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Not enough permission");
return -1;
}
return 0;
}
function Do_SearchUsers()
{
global $this_user;
global $ajax_args;
global $PORTAL_GENESIS;
$results = array();
if (CheckPageArgs()) {
return;
}
if (!isset($ajax_args["text"])) {
SPITAJAX_ERROR(-1, "Missing text to search for");
return -1;
}
$text = $ajax_args["text"];
if (!preg_match("/^\w*$/", $text)) {
SPITAJAX_ERROR(-1, "Illegal text to search for");
return -1;
}
$query_result =
DBQueryFatal("select uid,usr_name,usr_affil,genesis from users ".
"where genesis='$PORTAL_GENESIS' and ".
" (uid like '%${text}%' or ".
" usr_name like '%${text}%') ".
"order by uid");
while ($row = mysql_fetch_array($query_result)) {
$blob = array();
$blob["usr_uid"] = $row["uid"];
$blob["usr_name"] = $row["usr_name"];
$blob["usr_affil"] = $row["usr_affil"];
$results[] = $blob;
}
SPITAJAX_RESPONSE($results);
}
function Do_SearchProjects()
{
global $this_user;
global $ajax_args;
global $PORTAL_GENESIS;
$results = array();
if (CheckPageArgs()) {
return;
}
if (!isset($ajax_args["text"])) {
SPITAJAX_ERROR(-1, "Missing text to search for");
return -1;
}
$text = $ajax_args["text"];
if (!preg_match("/^\w*$/", $text)) {
SPITAJAX_ERROR(-1, "Illegal text to search for");
return -1;
}
$query_result =
DBQueryFatal("select pid,u.uid,u.usr_name,u.usr_affil,p.genesis ".
" from projects as p ".
"left join users as u on u.uid_idx=p.head_idx ".
"where p.genesis='$PORTAL_GENESIS' and ".
" (pid like '%${text}%' or ".
" u.usr_name like '%${text}%' or ".
" u.usr_affil like '%${text}%') ".
"order by pid");
while ($row = mysql_fetch_array($query_result)) {
$blob = array();
$blob["pid"] = $row["pid"];
$blob["usr_uid"] = $row["uid"];
$blob["usr_name"] = $row["usr_name"];
$blob["usr_affil"] = $row["usr_affil"];
$results[] = $blob;
}
SPITAJAX_RESPONSE($results);
}
# Local Variables:
# mode:php
# End:
?>
<?php
#
# Copyright (c) 2000-2016 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");
chdir("apt");
include("quickvm_sup.php");
# Must be after quickvm_sup.php since it changes the auth domain.
$page_title = "Ranking";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLoginOrRedirect();
$this_idx = $this_user->uid_idx();
$this_uid = $this_user->uid();
# Recent means current login or did something in last N days.
$days = 30;
#
# Verify page arguments.
#
SPITHEADER(1);
if (!ISADMIN()) {
SPITUSERERROR("You do not have permission to view this information!");
return;
}
echo "<link rel='stylesheet'
href='css/tablesorter.css'>\n";
# Place to hang the toplevel template.
echo "<div id='main-body'></div>\n";
function SpitUserList($days)
{
global $PORTAL_GENESIS, $APTHOST;
$query_result =
DBQueryFatal("(select distinct u.uid,u.usr_name,u.usr_affil ".
" from login as l ".
" left join users as u on u.uid_idx=l.uid_idx ".
" where l.portal='$PORTAL_GENESIS' and ".
" l.timeout > UNIX_TIMESTAMP(now())) ".
"union ".
" (select distinct u.uid,u.usr_name,u.usr_affil ".
" from apt_instances as i ".
" left join users as u on u.uid_idx=i.creator_idx ".
" where i.servername='$APTHOST')");
while ($row = mysql_fetch_array($query_result)) {
$blob = array();
$blob["usr_uid"] = $row["uid"];
$blob["usr_name"] = $row["usr_name"];
$blob["usr_affil"] = $row["usr_affil"];
$results[$row[0]] = $blob;
}
echo "<script type='text/plain' id='users-json'>\n";
echo json_encode($results);
echo "</script>\n";
}
function SpitProjectList($target, $days)
{
global $PORTAL_GENESIS, $APTHOST;
$query_result =
DBQueryFatal("(select distinct i.pid,u.uid,u.usr_name,u.usr_affil ".
" from apt_instances as i ".
" left join projects as p on p.pid_idx=i.pid_idx ".
" left join users as u on u.uid_idx=p.head_idx ".
" where i.servername='$APTHOST') ".
"union ".
"(select distinct i.pid,u.uid,u.usr_name,u.usr_affil ".
" from apt_instance_history as i ".
" left join projects as p on p.pid_idx=i.pid_idx ".
" left join users as u on u.uid_idx=p.head_idx ".
" where i.servername='$APTHOST' and ".
" i.created>DATE_SUB(curdate(), INTERVAL 2 MONTH))");
$results = array();
while ($row = mysql_fetch_array($query_result)) {
$blob = array();
$blob["usr_uid"] = $row["uid"];
$blob["usr_name"] = $row["usr_name"];
$blob["usr_affil"] = $row["usr_affil"];
$results[$row[0]] = $blob;
}
echo "<script type='text/plain' id='projects-json'>\n";
echo json_encode($results);
echo "</script>\n";
}
SpitUserList($days);
SpitProjectList($days);
SPITREQUIRE("lists",
"<script src='js/lib/jquery.tablesorter.min.js'></script>".
"<script src='js/lib/jquery.tablesorter.widgets.min.js'></script>".
"<script src='js/lib/sugar.min.js'></script>".
"<script src='js/lib/jquery.tablesorter.parser-date.js'></script>");
SPITFOOTER();
?>
......@@ -287,7 +287,9 @@ $PAGEHEADER_FUNCTION = function($thinheader = 0, $ignore1 = NULL,
<li><a href='myprofiles.php?all=1'>
All Profiles</a></li>
<li><a href='list-datasets.php?all=1'>
All Datasets</a></li>";