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

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:
?>
This diff is collapsed.
$(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
// toggle table cells instead of the row
// Find add/even and add that to child rows so that
// zebra strip is the same for its children.
var stripe = "odd";
if ($(this).closest('tr').hasClass("even")) {
stripe = "even";
}
$(this)
.closest('tr')
.nextUntil('tr.tablesorter-hasChildRow',
'.image-version')
.each(function() {
// If going to hide the row, want to hide the
// expanded profile tables too.
if (! $(this).hasClass("hidden") &&
! $(this).next(".profile-version")
.hasClass("hidden")) {
$(this)
.find(".toggle-version")
.trigger("click");
}
$(this)
.toggleClass('hidden')
.addClass(stripe);
});
$(this).find(".glyphicon")
.toggleClass("glyphicon-chevron-right")
.toggleClass("glyphicon-chevron-down");
return false;
});
table.find(".toggle-version")
.click(function(event) {
event.preventDefault();
$(this).closest('tr')
.next('tr').toggleClass('hidden');
$(this).find(".glyphicon")
.toggleClass("glyphicon-chevron-left")
.toggleClass("glyphicon-chevron-down");
});
// Bind a delete handler.
table.find(".delete-button").click(function(event) {
event.preventDefault();
DeleteImage(name, $(this).closest('tr'));
return false;
});
};
TableInit('images-table-no-profiles');
TableInit('images-table-one-profile');
TableInit('images-table-multi-profile');
// This activates the popover subsystem.
$('#' + name + ' [data-toggle="popover"]').popover({
trigger: 'hover',
placement: 'auto',
container: 'body',
});
}
var xmlthing = sup.CallServerMethod(null, "images",
"ListImages",
{"cluster" : name,
"uid" : window.TARGET_USER});
xmlthing.done(callback);
});
}
/*
* Delete an Image. Delete the table row when completed
*/
function DeleteImage(cluster, row) {
var urn = $(row).attr('data-urn');
var table = $(row).closest("table");
console.info(cluster, urn);
// Callback for the delete request.
var callback = function (json) {
sup.HideWaitWait();
console.log("delete", json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
// Now to delete the row. This has a little trickiness.
if ($(row).hasClass("image-version")) {
//
// Individual version, delete the row. There should not be
// a following profile versions row, but watch for it
// anyway.
//
if ($(row).next().hasClass("profile-version")) {
$(row).next().remove();
}
/*
* Is this is the last version of the image, then delete
* the image row too. We determine this by looking to see
* if the previous row is an image row, and the next row
* is an image row (or no row, so last image in the table).
*/
var prev = $(row).closest('tr').prev('tr');
var next = $(row).closest('tr').next('tr');
if ($(prev).is("tr.tablesorter-hasChildRow") &&
($(next).is("tr.tablesorter-hasChildRow") ||
!$(next).is("tr"))) {
$(prev).remove();
}
$(row).remove();
}
else {
//
// Entire image delete (all versions). Need to delete the
// main row and all rows up to the next image.
//
$(row).closest('tr')
.nextUntil('tr.tablesorter-hasChildRow', '.image-version')
.remove();
$(row).remove();
}
table.trigger('update');
};
var args = {"urn" : urn,
"uid" : window.TARGET_USER,
"cluster" : cluster};
/*
* Look to see if this is a row with a profile in it, which
* should be deleted along with the image. Pass that along,
* the backend is going to check anyway.
*/
if ($(row).find("td.delete-profile").length) {
var uuid = $(row).find("td.delete-profile").attr('data-uuid');
args["profile-delete"] = uuid;
}
/*
* The confirm modal is a template in case we need to warn
* about profiles that will be deleted. Need to find that
* list in the saved data structure.
*/
var profiles = null;
if ($(row).find("td.delete-profile").length) {
_.each(imagelist[cluster], function(image, index) {
_.each(image.versions, function(version, index) {
if (version.urn == urn) {
profiles = version.using;
}
});
});
}
var html = confirmTemplate({
"profiles" : profiles,
});
$('#confirm_div').html(html);
// Format dates with moment before display.
$('#confirm_div .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
// Bind the confirm button in the modal. Do the deletion.
$('#confirm-delete-image-modal #confirm-delete-image')
.click(function () {
sup.HideModal('#confirm-delete-image-modal');
sup.ShowWaitWait('It take a moment to delete an image; ' +
'patience please');
var xmlthing = sup.CallServerMethod(null, "images",
"DeleteImage", args);
xmlthing.done(callback);
});
sup.ShowModal("#confirm-delete-image-modal",
// Delete handler no matter how it hides.
function () {
$('#confirm-delete-image-modal #confirm-delete-image')
.unbind("click");
});
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
}
$(document).ready(initialize);
});
......@@ -2,7 +2,7 @@ $(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['manage-profile', 'waitwait-modal', 'renderer-modal', 'showtopo-modal', 'oops-modal', 'rspectextview-modal', 'guest-instantiate', 'publish-modal', 'instantiate-modal', 'share-modal', 'gitrepo-picker']);
var templates = APT_OPTIONS.fetchTemplateList(['manage-profile', 'waitwait-modal', 'renderer-modal', 'showtopo-modal', 'oops-modal', 'rspectextview-modal', 'guest-instantiate', 'publish-modal', 'instantiate-modal', 'share-modal', 'gitrepo-picker','profile-list-modal','confirm-delete-profile']);
var manageString = templates['manage-profile'];
var waitwaitString = templates['waitwait-modal'];
var rendererString = templates['renderer-modal'];
......@@ -14,6 +14,8 @@ $(function ()
var instantiateString = templates['instantiate-modal'];
var shareString = templates['share-modal'];
var gitrepoString = templates['gitrepo-picker'];
var plistString = templates['profile-list-modal'];
var deleteString = templates['confirm-delete-profile'];
var profile_uuid = null;
var profile_name = '';
......@@ -44,6 +46,7 @@ $(function ()
var InstTemplate = _.template(instantiateString);
var shareTemplate = _.template(shareString);
var gitrepoTemplate = _.template(gitrepoString);
var plistTemplate = _.template(plistString);
var stepsInitialized = false;
var pythonRe = /^import/m;
......@@ -64,9 +67,14 @@ $(function ()
var errors = JSON.parse(_.unescape($('#error-json')[0].textContent));
var projlist = JSON.parse(_.unescape($('#projects-json')[0].textContent));
var versions = null;
var sorted_versions = null;
if (window.VIEWING) {
versions =
JSON.parse(_.unescape($('#versions-json')[0].textContent));
sorted_versions = _.sortBy(versions, function(profile) {
return versions.length - profile.version;
});
}
amlist = JSON.parse(_.unescape($('#amlist-json')[0].textContent));
......@@ -134,7 +142,9 @@ $(function ()
general_error: (errors.error || ''),
isapt: window.ISAPT,
disabled: window.DISABLED,
nodelete: window.NODELETE,
versions: versions,
sorted_versions: sorted_versions,
withpublishing: window.WITHPUBLISHING,
genilib_editor: false,
canrepo: window.CANREPO,
......@@ -165,6 +175,7 @@ $(function ()
var rspectext_html = rspectextTemplate({});
$('#rspectext_div').html(rspectext_html);
$('#share_div').html(shareTemplate({formfields: fields}))
$('#confirm_delete_div').html(deleteString);
// Fireoff repo stuff now.
if (fromrepo) {
......@@ -392,7 +403,7 @@ $(function ()
})
// Confirm Delete profile.
$('#delete-confirm').click(function (event) {
$('#confirm-delete-button').click(function (event) {
event.preventDefault();
DeleteProfile();
});
......@@ -453,6 +464,7 @@ $(function ()
$('#profile_who_private').change(function() { ProfileModified(); });
$('#profile_topdog').change(function() { ProfileModified(); });
$('#profile_disabled').change(function() { ProfileModified(); });
$('#profile_nodelete').change(function() { ProfileModified(); });
/*
* A double click handler that will render the instructions
......@@ -1070,27 +1082,49 @@ $(function ()
//
// Delete profile.
//
function DeleteProfile()
function DeleteProfile(force, keepimages)
{
var delete_all = $('#delete-all-versions').is(':checked') ? 1 : 0;
var callback = function(json) {
sup.HideModal("#waitwait-modal");
//console.info(json.value);
sup.HideWaitWait();
console.info(json.value);
if (json.code) {
if (json.code == 2) {
ShowDeletionWarning(json.value);
return;
}
sup.SpitOops("oops", json.value);
return;
}
window.location.replace(json.value);
}
sup.HideModal('#delete_modal');
WaitWait();
var xmlthing = sup.CallServerMethod(ajaxurl,
"manage_profile",
"DeleteProfile",
{"uuid" : version_uuid,
"all" : delete_all});
var args = {
"uuid" : version_uuid,
"all" : delete_all,
};
if (force) {
args["force"] = 1;
if (keepimages) {
args["keepimages"] = 1;
}
}
console.info("DeleteProfile", args);
var xmlthing = sup.CallServerMethod(null, "manage_profile",
"DeleteProfile", args);
// Came from ShowDeletionWarning() if force is set.
if (!force) {
sup.HideModal('#confirm-delete-profile-modal');
}
if (force && !keepimages) {
WaitWait("Deleting images takes a minute; patience please");
}
else {
WaitWait();
}
xmlthing.done(callback);
}
......@@ -1355,5 +1389,43 @@ $(function ()
window.location.href = 'genilib-editor.php?profile=' + profile_name + '&project=' + profile_pid + '&version=' + profile_version;
}
function ShowDeletionWarning(images)
{
/*
* See if we have any profiles to warn about. If only images, then
* the warning is different.
*/
var noprofiles = 1;
_.each(images, function(profiles, imagename) {
_.each(profiles, function(value, name) {
noprofiles = 0;