Commit e0c9477d authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

Web UI part of image deletion.

parent d2414cce
/* Grouping widget css */
tr.group-header td {
background: #eee;
}
.group-name {
text-transform: uppercase;
font-weight: bold;
}
.group-count {
color: #999;
}
.group-hidden {
display: none !important;
}
.group-header, .group-header td {
user-select: none;
-moz-user-select: none;
}
/* collapsed arrow */
tr.group-header td i {
display: inline-block;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid #888;
border-right: 4px solid #888;
border-left: 4px solid transparent;
margin-right: 7px;
user-select: none;
-moz-user-select: none;
}
tr.group-header.collapsed td i {
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #888;
border-right: 0;
margin-right: 10px;
}
<?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_once("webtask.php");
chdir("apt");
# We set this in CheckPageArgs
$target_user = null;
#
# Need to check the permission, since we allow admins to mess with
# other accounts.
#
function CheckPageArgs()
{
global $this_user, $target_user;
global $ajax_args;
global $TB_USERINFO_READINFO;
if (!isset($ajax_args["uid"])) {
SPITAJAX_ERROR(-1, "Missing target uid");
return -1;
}
$uid = $ajax_args["uid"];
if (!TBvalid_uid($uid)) {
SPITAJAX_ERROR(-1, "Invalid target uid");
return -1;
}
$target_user = User::Lookup($uid);
if (!$target_user) {
sleep(2);
SPITAJAX_ERROR(-1, "Unknown target uid");
return -1;
}
if ($uid == $this_user->uid())
return 0;
if (!ISADMIN() && !ISFOREIGN_ADMIN() &&
!$target_user->AccessCheck($this_user, $TB_USERINFO_READINFO)) {
SPITAJAX_ERROR(-1, "Not enough permission");
return -1;
}
return 0;
}
#
# List images at a cluster (for a user).
#
function Do_ListImages()
{
global $this_user, $target_user;
global $ajax_args;
global $TB_PROJECT_CREATEEXPT, $suexec_output;
if (CheckPageArgs()) {
return;
}
if (!isset($ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Missing cluster");
return;
}
if (!preg_match("/^[-\w]+$/", $ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Invalid cluster name");
return;
}
$aggregate = Aggregate::LookupByNickname($ajax_args["cluster"]);
if (!$aggregate) {
SPITAJAX_ERROR(-1, "No such cluster");
return;
}
$uid = $target_user->uid();
$urn = $aggregate->urn();
$webtask = WebTask::CreateAnonymous();
$webtask_id = $webtask->task_id();
$retval = SUEXEC($uid, "nobody",
"webmanage_images -t $webtask_id list -a '$urn'",
SUEXEC_ACTION_CONTINUE);
if ($retval) {
$webtask->Delete();
SPITAJAX_ERROR(-1, $suexec_output);
return;
}
$webtask->Refresh();
$images = $webtask->TaskValue("value");
$webtask->Delete();
SPITAJAX_RESPONSE($images);
}
#
# Delete image at a cluster (for a user).
#
function Do_DeleteImage()
{
global $this_user, $target_user;
global $ajax_args;
global $suexec_output;
$pdarg = "";
if (CheckPageArgs()) {
return;
}
if (!isset($ajax_args["urn"]) || $ajax_args["urn"] == "") {
SPITAJAX_ERROR(-1, "Missing image urn");
return;
}
$image_urn = escapeshellarg($ajax_args["urn"]);
if (!isset($ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Missing cluster");
return;
}
if (!preg_match("/^[-\w]+$/", $ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Invalid cluster name");
return;
}
$aggregate = Aggregate::LookupByNickname($ajax_args["cluster"]);
if (!$aggregate) {
SPITAJAX_ERROR(-1, "No such cluster");
return;
}
if (isset($ajax_args["profile-delete"]) &&
$ajax_args["profile-delete"] != "") {
if (!preg_match("/^[-\w]+$/", $ajax_args["profile-delete"])) {
SPITAJAX_ERROR(-1, "Invalid profile uuid for deletion");
return;
}
$pdarg = "-d " . escapeshellarg($ajax_args["profile-delete"]);
}
$uid = $target_user->uid();
$aggurn = $aggregate->urn();
$webtask = WebTask::CreateAnonymous();
$webtask_id = $webtask->task_id();
$retval = SUEXEC($uid, "nobody",
"webmanage_images -t $webtask_id ".
" delete -a '$aggurn' -n $pdarg $image_urn",
SUEXEC_ACTION_CONTINUE);
if ($retval) {
$webtask->Delete();
SPITAJAX_ERROR(-1, $suexec_output);
return;
}
$webtask->Delete();
SPITAJAX_RESPONSE(0);
}
# Local Variables:
# mode:php
# End:
?>
/*! Widget: grouping - updated 3/5/2015 (v2.21.0) *//*
* Requires tablesorter v2.8+ and jQuery 1.7+
* by Rob Garrison
*/
/*jshint browser:true, jquery:true, unused:false */
/*global jQuery: false */
;(function($){
"use strict";
var ts = $.tablesorter;
ts.grouping = {
types : {
number : function(c, $column, txt, num, group){
var value, word;
if (num > 1 && txt !== '') {
if ($column.hasClass(ts.css.sortAsc)) {
value = Math.floor(parseFloat(txt)/num) * num;
return value > parseFloat(group || 0) ? value : parseFloat(group || 0);
} else {
value = Math.ceil(parseFloat(txt)/num) * num;
return value < parseFloat(group || num) - value ? parseFloat(group || num) - value : value;
}
} else {
word = (txt + '').match(/\d+/g);
return word && word.length >= num ? word[num - 1] : txt || '';
}
},
separator : function(c, $column, txt, num){
var word = (txt + '').split(c.widgetOptions.group_separator);
return $.trim(word && num > 0 && word.length >= num ? word[(num || 1) - 1] : '');
},
word : function(c, $column, txt, num){
var word = (txt + ' ').match(/\w+/g);
return word && word.length >= num ? word[num - 1] : txt || '';
},
letter : function(c, $column, txt, num){
return txt ? (txt + ' ').substring(0, num) : '';
},
date : function(c, $column, txt, part, group){
var wo = c.widgetOptions,
time = new Date(txt || ''),
hours = time.getHours();
return part === 'year' ? time.getFullYear() :
part === 'month' ? wo.group_months[time.getMonth()] :
part === 'monthyear' ? wo.group_months[time.getMonth()] + ' ' + time.getFullYear() :
part === 'day' ? wo.group_months[time.getMonth()] + ' ' + time.getDate() :
part === 'week' ? wo.group_week[time.getDay()] :
part === 'time' ? ('00' + (hours > 12 ? hours - 12 : hours === 0 ? hours + 12 : hours)).slice(-2) + ':' +
('00' + time.getMinutes()).slice(-2) + ' ' + ('00' + wo.group_time[hours >= 12 ? 1 : 0]).slice(-2) :
wo.group_dateString(time);
}
},
update : function(table, c, wo){
if ($.isEmptyObject(c.cache)) { return; }
var rowIndex, tbodyIndex, currentGroup, $rows, groupClass, grouping, norm_rows, saveName, direction,
lang = wo.grouping_language,
group = '',
savedGroup = false,
column = c.sortList[0] ? c.sortList[0][0] : -1;
c.$table
.find('tr.group-hidden').removeClass('group-hidden').end()
.find('tr.group-header').remove();
if (wo.group_collapsible) {
// clear pager saved spacer height (in case the rows are collapsed)
c.$table.data('pagerSavedHeight', 0);
}
if (column >= 0 && !c.$headerIndexed[column].hasClass('group-false')) {
wo.group_currentGroup = ''; // save current groups
wo.group_currentGroups = {};
// group class finds "group-{word/separator/letter/number/date/false}-{optional:#/year/month/day/week/time}"
groupClass = (c.$headerIndexed[column].attr('class') || '').match(/(group-\w+(-\w+)?)/g);
// grouping = [ 'group', '{word/separator/letter/number/date/false}', '{#/year/month/day/week/time}' ]
grouping = groupClass ? groupClass[0].split('-') : ['group','letter',1]; // default to letter 1
// save current grouping
if (wo.group_collapsible && wo.group_saveGroups && ts.storage) {
wo.group_currentGroups = ts.storage( table, 'tablesorter-groups' ) || {};
// include direction when grouping numbers > 1 (reversed direction shows different range values)
direction = (grouping[1] === 'number' && grouping[2] > 1) ? 'dir' + c.sortList[0][1] : '';
// combine column, sort direction & grouping as save key
saveName = wo.group_currentGroup = '' + column + direction + grouping.join('');
if (!wo.group_currentGroups[saveName]) {
wo.group_currentGroups[saveName] = [];
} else {
savedGroup = true;
}
}
for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++) {
norm_rows = c.cache[tbodyIndex].normalized;
group = ''; // clear grouping across tbodies
$rows = c.$tbodies.eq(tbodyIndex).children('tr').not('.' + c.cssChildRow);
for (rowIndex = 0; rowIndex < $rows.length; rowIndex++) {
if ( $rows.eq(rowIndex).is(':visible') ) {
// fixes #438
if (ts.grouping.types[grouping[1]]) {
currentGroup = norm_rows[rowIndex] ?
ts.grouping.types[grouping[1]]( c, c.$headerIndexed[column], norm_rows[rowIndex][column], /date/.test(groupClass) ?
grouping[2] : parseInt(grouping[2] || 1, 10) || 1, group, lang ) : currentGroup;
if (group !== currentGroup) {
group = currentGroup;
// show range if number > 1
if (grouping[1] === 'number' && grouping[2] > 1 && currentGroup !== '') {
currentGroup += ' - ' + (parseInt(currentGroup, 10) +
((parseInt(grouping[2],10) - 1) * (c.$headerIndexed[column].hasClass(ts.css.sortAsc) ? 1 : -1)));
}
if ($.isFunction(wo.group_formatter)) {
currentGroup = wo.group_formatter((currentGroup || '').toString(), column, table, c, wo) || currentGroup;
}
$rows.eq(rowIndex).before('<tr class="group-header ' + c.selectorRemove.slice(1) +
'" unselectable="on"' + ( c.tabIndex ? ' tabindex="0"' : '' ) + '><td colspan="' +
c.columns + '">' + (wo.group_collapsible ? '<i/>' : '') + '<span class="group-name">' +
currentGroup + '</span><span class="group-count"></span></td></tr>');
if (wo.group_saveGroups && !savedGroup && wo.group_collapsed && wo.group_collapsible) {
// all groups start collapsed
wo.group_currentGroups[wo.group_currentGroup].push(currentGroup);
}
}
}
}
}
}
c.$table.find('tr.group-header')
.bind('selectstart', false)
.each(function(){
var isHidden, $label, name,
$row = $(this),
$rows = $row.nextUntil('tr.group-header').filter(':visible');
if (wo.group_count || $.isFunction(wo.group_callback)) {
$label = $row.find('.group-count');
if ($label.length) {
if (wo.group_count) {
$label.html( wo.group_count.replace(/\{num\}/g, $rows.length) );
}
if ($.isFunction(wo.group_callback)) {
wo.group_callback($row.find('td'), $rows, column, table);
}
}
}
if (wo.group_saveGroups && wo.group_currentGroups.length && wo.group_currentGroups[wo.group_currentGroup].length) {
name = $row.find('.group-name').text().toLowerCase();
isHidden = $.inArray( name, wo.group_currentGroups[wo.group_currentGroup] ) > -1;
$row.toggleClass('collapsed', isHidden);
$rows.toggleClass('group-hidden', isHidden);
} else if (wo.group_collapsed && wo.group_collapsible) {
$row.addClass('collapsed');
$rows.addClass('group-hidden');
}
});
c.$table.trigger(wo.group_complete);
}
},
bindEvents : function(table, c, wo){
if (wo.group_collapsible) {
wo.group_currentGroups = [];
// .on() requires jQuery 1.7+
c.$table.on('click toggleGroup keyup', 'tr.group-header', function(event){
event.stopPropagation();
// pressing enter will toggle the group
if (event.type === 'keyup' && event.which !== 13) { return; }
var isCollapsed, $groups, indx,
$this = $(this),
name = $this.find('.group-name').text().toLowerCase();
// use shift-click to toggle ALL groups
if (event.shiftKey && (event.type === 'click' || event.type ==='keyup')) {
$this.siblings('.group-header').trigger('toggleGroup');
}
$this.toggleClass('collapsed');
// nextUntil requires jQuery 1.4+
$this.nextUntil('tr.group-header').toggleClass('group-hidden', $this.hasClass('collapsed') );
// save collapsed groups
if (wo.group_saveGroups && ts.storage) {
$groups = c.$table.find('.group-header');
isCollapsed = $this.hasClass('collapsed');
if (!wo.group_currentGroups[wo.group_currentGroup]) {
wo.group_currentGroups[wo.group_currentGroup] = [];
}
if (isCollapsed && wo.group_currentGroup) {
wo.group_currentGroups[wo.group_currentGroup].push( name );
} else if (wo.group_currentGroup) {
indx = $.inArray( name, wo.group_currentGroups[wo.group_currentGroup] );
if (indx > -1) {
wo.group_currentGroups[wo.group_currentGroup].splice( indx, 1 );
}
}
ts.storage( table, 'tablesorter-groups', wo.group_currentGroups );
}
});
}
$(wo.group_saveReset).on('click', function(){
ts.grouping.clearSavedGroups(table);
});
c.$table.on('pagerChange.tsgrouping', function(){
ts.grouping.update(table, c, wo);
});
},
clearSavedGroups: function(table){
if (table && ts.storage) {
ts.storage(table, 'tablesorter-groups', '');
ts.grouping.update(table, table.config, table.config.widgetOptions);
}
}
};
ts.addWidget({
id: 'group',
priority: 100,
options: {
group_collapsible : true, // make the group header clickable and collapse the rows below it.
group_collapsed : false, // start with all groups collapsed
group_saveGroups : true, // remember collapsed groups
group_saveReset : null, // element to clear saved collapsed groups
group_count : ' ({num})', // if not false, the "{num}" string is replaced with the number of rows in the group
group_separator : '-', // group name separator; used when group-separator-# class is used.
group_formatter : null, // function(txt, column, table, c, wo) { return txt; }
group_callback : null, // function($cell, $rows, column, table){}, callback allowing modification of the group header labels
group_complete : 'groupingComplete', // event triggered on the table when the grouping widget has finished work
// checkbox parser text used for checked/unchecked values
group_checkbox : [ 'checked', 'unchecked' ],
// change these default date names based on your language preferences
group_months : [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
group_week : [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ],
group_time : [ 'AM', 'PM' ],
// this function is used when "group-date" is set to create the date string
// you can just return date, date.toLocaleString(), date.toLocaleDateString() or d.toLocaleTimeString()
// reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#Conversion_getter
group_dateString : function(date) { return date.toLocaleString(); }
},
init: function(table, thisWidget, c, wo){
ts.grouping.bindEvents(table, c, wo);
},
format: function(table, c, wo) {
ts.grouping.update(table, c, wo);
},
remove : function(table, c, wo){
c.$table
.off('click', 'tr.group-header')
.off('pagerChange.tsgrouping')
.find('.group-hidden').removeClass('group-hidden').end()
.find('tr.group-header').remove();
}
});
})(jQuery);
$(function ()
{
'use strict';
var template_list = ["image-list", "oops-modal", "confirm-delete-image",
"waitwait-modal"];
var templates = APT_OPTIONS.fetchTemplateList(template_list);
var listTemplate = _.template(templates["image-list"]);
var confirmTemplate = _.template(templates["confirm-delete-image"]);
var oopsString = templates["oops-modal"];
var waitwaitString = templates["waitwait-modal"];
var amlist = null;
// Results for each AM so we can get it later.
var imagelist = [];
function initialize()
{
window.APT_OPTIONS.initialize(sup);
amlist = decodejson('#amlist-json');
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
LoadData();
}
/*
* Load images from each am in the list and generate a table.
*/
function LoadData()
{
var count = Object.keys(amlist).length;
_.each(amlist, function(urn, name) {
var callback = function(json) {
console.info(json);
// Kill the spinner.
count--;
if (count <= 0) {
$('#spinner').addClass("hidden");
}
if (json.code) {
console.info("Could not get image list for " +
name + ": " + json.value);
return;
}
var images = json.value;
if (images.length == 0)
return;
// Save for later
imagelist[name] = images;
// Generate the main template.
var html = listTemplate({
"images" : images,
"showproject" : false,
"showuser" : false,
"name" : name,
});
html =
"<div class='row' id='" + name + "'>" +
" <div class='col-xs-12 col-xs-offset-0'>" + html +
" </div>" +
"</div>";
$('#main-body').prepend(html);
// Format dates with moment before display.
$('#' + name + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
var TableInit = function(tablename) {
var table =
$('#' + name + ' #' + tablename)
.tablesorter({
theme : 'green',
widgets: ["zebra"],
cssChildRow : 'tablesorter-childRow-versions',
});
table.find('.tablesorter-childRow-versions')
.addClass('hidden');
/*
* This little diddy sums up the filesizes for each
* image version, and writes into the filesize for
* the entire image.
*/
table.find('tr.tablesorter-hasChildRow')
.each(function() {
var sum = 0;
var re = /^(\d+)MB$/;
$(this).nextUntil('tr.tablesorter-hasChildRow',
'.image-version')
.each(function() {
var size =
$(this).find('td.version-filesize')
.text();
var match = size.match(re);
if (match) {
sum = sum + parseInt(match[1]);
}
});
$(this).find('td.image-filesize').text(sum + "MB");
});
table.trigger('update');
// Toggle child row content. Using delegate cause the
// tablesorter example page says to.
table.delegate('.toggle-image', 'click', function() {
// use "nextUntil" to toggle multiple child rows