Commit 776dc702 authored by Leigh B Stoller's avatar Leigh B Stoller

Add profile editing, deletion, and quick link to instantiate.

parent c9f7c976
......@@ -232,5 +232,45 @@ sub Stringify($)
return "[Profile: $pid,$name]";
}
#
# Perform some updates ...
#
sub Update($$)
{
my ($self, $argref) = @_;
# Must be a real reference.
return -1
if (! ref($self));
my $idx = $self->idx();
my $query = "update apt_profiles set ".
join(",", map("$_=" . DBQuoteSpecial($argref->{$_}), keys(%{$argref})));
$query .= " where idx='$idx'";
return -1
if (! DBQueryWarn($query));
return Refresh($self);
}
sub Delete($)
{
my ($self) = @_;
# Must be a real reference.
return -1
if (! ref($self));
my $idx = $self->idx();
DBQueryWarn("delete from apt_profiles where idx='$idx'") or
return -1;
return 0;
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -33,13 +33,15 @@ use CGI;
#
sub usage()
{
print("Usage: manage_profile [-v] <xmlfile>\n");
print("Usage: manage_profile [-u] <xmlfile>\n");
print("Usage: manage_profile -r profile\n");
exit(-1);
}
my $optlist = "dv";
my $optlist = "dur";
my $debug = 0;
my $verify = 0; # Check data and return status only.
my $update = 0;
my $delete = 0;
my $skipadmin = 0;
#
......@@ -73,6 +75,7 @@ use APT_Profile;
# Protos
sub fatal($);
sub UserError();
sub DeleteProfile($);
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -88,6 +91,9 @@ if (defined($options{"d"})) {
if (defined($options{"v"})) {
$verify = 1;
}
if (defined($options{"u"})) {
$update = 1;
}
if (@ARGV != 1) {
usage();
}
......@@ -102,7 +108,7 @@ if (! defined($this_user)) {
# Remove profile.
if (defined($options{"r"})) {
exit(DeleteProfile());
exit(DeleteProfile($ARGV[0]));
}
my $xmlfile = shift(@ARGV);
......@@ -112,6 +118,7 @@ my $xmlfile = shift(@ARGV);
my $SLOT_OPTIONAL = 0x1; # The field is not required.
my $SLOT_REQUIRED = 0x2; # The field is required and must be non-null.
my $SLOT_ADMINONLY = 0x4; # Only admins can set this field.
my $SLOT_UPDATE = 0x8; # Allowed to update.
#
# XXX We should encode all of this in the DB so that we can generate the
# forms on the fly, as well as this checking code.
......@@ -121,9 +128,9 @@ my %xmlfields =
("profile_name" => ["name", $SLOT_REQUIRED],
"profile_pid" => ["pid", $SLOT_REQUIRED],
"profile_creator" => ["creator", $SLOT_OPTIONAL],
"profile_description" => ["description", $SLOT_REQUIRED],
"profile_public" => ["public", $SLOT_OPTIONAL],
"rspec" => ["rspec", $SLOT_REQUIRED],
"profile_description" => ["description", $SLOT_REQUIRED|$SLOT_UPDATE],
"profile_public" => ["public", $SLOT_OPTIONAL|$SLOT_UPDATE],
"rspec" => ["rspec", $SLOT_REQUIRED|$SLOT_UPDATE],
);
#
......@@ -161,6 +168,7 @@ UserError()
# We build up an array of arguments to create.
#
my %new_args = ();
my %update_args = ();
foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
......@@ -206,6 +214,8 @@ foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
next;
}
$new_args{$dbslot} = $value;
$update_args{$dbslot} = $value
if ($update && ($required & $SLOT_UPDATE));
}
UserError()
if (keys(%errors));
......@@ -222,14 +232,31 @@ elsif (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) {
$errors{"profile_pid"} = "Not enough permission in this project";
}
my $usererror;
my $profile = APT_Profile->Create($project, $this_user, \%new_args, \$usererror);
if (!defined($profile)) {
if (defined($usererror)) {
$errors{"profile_name"} = $usererror;
my $profile = APT_Profile->Lookup($new_args{"pid"}, $new_args{"name"});
if ($update) {
if (!defined($profile)) {
$errors{"profile_name"} = "No such profile exists";
UserError();
}
fatal("Could not create new profile");
$profile->Update(\%update_args) == 0 or
fatal("Could not update profile record");
}
else {
my $usererror;
if (defined($profile)) {
$errors{"profile_name"} = "Already in use";
UserError();
}
$profile =
APT_Profile->Create($project, $this_user, \%new_args, \$usererror);
if (!defined($profile)) {
if (defined($usererror)) {
$errors{"profile_name"} = $usererror;
UserError();
}
fatal("Could not create new profile");
}
}
exit(0);
......@@ -269,3 +296,18 @@ sub escapeshellarg($)
$str =~ s/[^[:alnum:]]/\\$&/g;
return $str;
}
#
# Delete a profile.
#
sub DeleteProfile($)
{
my ($name) = @_;
my $profile = APT_Profile->Lookup($name);
if (!defined($profile)) {
fatal("No such profile exists");
}
$profile->Delete() == 0 or
fatal("Could not delete profile");
return 0;
}
......@@ -10,12 +10,14 @@ window.APT_OPTIONS.config = function ()
'formhelpers': 'formhelpers/js/bootstrap-formhelpers',
'dateformat': 'js/lib/date.format',
'd3': 'js/lib/d3.v3',
'filestyle': 'js/lib/filestyle',
},
shim: {
'bootstrap': { deps: ['jquery'] },
'formhelpers': { deps: ['bootstrap']},
'dateformat': { exports: 'dateFormat' },
'd3': { exports: 'd3' }
'd3': { exports: 'd3' },
'filestyle': { deps: ['bootstrap']},
},
});
}
......
......@@ -2,7 +2,7 @@ window.APT_OPTIONS.config();
require(['jquery', 'js/quickvm_sup',
// jQuery modules
'bootstrap', 'formhelpers'],
'bootstrap'],
function ($, sup)
{
'use strict';
......@@ -10,6 +10,12 @@ function ($, sup)
function initialize()
{
window.APT_OPTIONS.initialize(sup);
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
});
$('body').show();
}
$(document).ready(initialize);
......
window.APT_OPTIONS.config();
require(['jquery', 'js/quickvm_sup',
require(['jquery', 'js/quickvm_sup',
// jQuery modules
'bootstrap', 'formhelpers'],
'bootstrap','filestyle'],
function ($, sup)
{
'use strict';
......@@ -16,7 +16,7 @@ function ($, sup)
reader.onload = function(event) {
var content = event.target.result;
sup.ShowUploadedRspec(content);
ShowRspecContent(content);
};
reader.readAsText(this.files[0]);
});
......@@ -24,6 +24,29 @@ function ($, sup)
catch (e) {
alert(e);
}
$('#showtopo_modal_button').click(function (event) {
event.preventDefault();
// The rspec is taken from the text area.
ShowRspecContent($('#profile_rspec').val());
});
}
//
// Show the rspec text in the modal.
//
function ShowRspecContent(content)
{
var xmlDoc = $.parseXML(content);
var xml = $(xmlDoc);
var topo = sup.ConvertManifestToJSON(null, xml);
console.info(topo);
sup.ShowModal("#quickvm_topomodal");
sup.maketopmap("#showtopo_div",
($("#showtopo_dialog").outerWidth()) - 90,
300, topo);
}
$(document).ready(initialize);
......
window.APT_OPTIONS.config();
require(['jquery', 'js/quickvm_sup',
// jQuery modules
'bootstrap'],
function ($, sup)
{
'use strict';
function initialize()
{
window.APT_OPTIONS.initialize(sup);
sup.UpdateProfileSelection($('#profile_name li[value = ' +
window.PROFILE + ']'));
$('#quickvm_topomodal').on('hidden.bs.modal', function() {
sup.ShowProfileList($('.current'))
});
$('button#profile').click(function (event) {
event.preventDefault();
sup.ShowModal('#quickvm_topomodal');
});
$('li.profile-item').click(function (event) {
event.preventDefault();
sup.ShowProfileList(event.target);
});
$('button#showtopo_select').click(function (event) {
event.preventDefault();
sup.UpdateProfileSelection($('.selected'));
sup.HideModal('#quickvm_topomodal');
});
}
$(document).ready(initialize);
});
......@@ -30,6 +30,7 @@ function ($, sup)
$('button#showtopo_select').click(function (event) {
event.preventDefault();
sup.UpdateProfileSelection($('.selected'));
sup.HideModal('#quickvm_topomodal');
});
}
......
......@@ -2,7 +2,7 @@ window.APT_OPTIONS.config();
require(['jquery', 'js/quickvm_sup',
// jQuery modules
'bootstrap', 'formhelpers'],
'bootstrap'],
function ($, sup)
{
'use strict';
......
......@@ -142,38 +142,39 @@ function ShowTopo(uuid)
function UpdateProfileSelection(selectedElement)
{
console.log(selectedElement);
var profile = $(selectedElement).text();
$('#selected_profile').attr('value', profile);
$('#selected_profile_text').html("" + profile);
if (!$(selectedElement).hasClass('current'))
{
$('#profile_name li').each(function() {
$(this).removeClass('current');
});
$(selectedElement).addClass('current');
}
ShowProfileList(selectedElement);
var profile_name = $(selectedElement).text();
var profile_value = $(selectedElement).attr('value');
$('#selected_profile').attr('value', profile_value);
$('#selected_profile_text').html("" + profile_name);
if (!$(selectedElement).hasClass('current')) {
$('#profile_name li').each(function() {
$(this).removeClass('current');
});
$(selectedElement).addClass('current');
}
ShowProfileList(selectedElement);
}
function ShowProfileList(selectedElement)
{
var profile = $(selectedElement).attr('value');
if (!$(selectedElement).hasClass('selected'))
{
$('#profile_name li').each(function() {
$(this).removeClass('selected');
});
var profile = $(selectedElement).attr('value');
$(selectedElement).addClass('selected');
}
if (!$(selectedElement).hasClass('selected')) {
$('#profile_name li').each(function() {
$(this).removeClass('selected');
});
$(selectedElement).addClass('selected');
}
var callback = function(json) {
var callback = function(json) {
console.info(json.value);
if (json.code) {
alert("Could not get profile: " + json.value);
return;
}
var xmlDoc = $.parseXML(json.value.rspec);
var xml = $(xmlDoc);
var topo = ConvertManifestToJSON(profile, xml);
......@@ -185,9 +186,9 @@ function ShowProfileList(selectedElement)
maketopmap("#showtopo_div",
($("#showtopo_div").outerWidth()),
300, topo);
}
var $xmlthing = CallMethod("getprofile", null, 0, profile);
$xmlthing.done(callback);
}
var $xmlthing = CallMethod("getprofile", null, 0, profile);
$xmlthing.done(callback);
}
function ShowProfile(direction)
......@@ -979,12 +980,15 @@ return {
RequestExtension: RequestExtension,
resetForm: resetForm,
ShowModal: ShowModal,
HideModal: HideModal,
ShowProfileList: ShowProfileList,
StartSSH: StartSSH,
Terminate: Terminate,
UpdateProfileSelection: UpdateProfileSelection,
ShowUploadedRspec: ShowUploadedRspec,
LoginByModal: LoginByModal,
Logout: Logout
Logout: Logout,
ConvertManifestToJSON: ConvertManifestToJSON,
maketopmap: maketopmap,
};
});
......@@ -25,6 +25,7 @@ chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
$page_title = "Login";
#
# Verify page arguments.
......
......@@ -25,6 +25,7 @@ chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
$page_title = "Manage Profile";
#
# Get current user.
......@@ -35,6 +36,8 @@ $this_user = CheckLogin($check_status);
# Verify page arguments.
#
$optargs = OptionalPageArguments("create", PAGEARG_STRING,
"action", PAGEARG_STRING,
"idx", PAGEARG_INTEGER,
"formfields", PAGEARG_ARRAY);
#
......@@ -42,8 +45,19 @@ $optargs = OptionalPageArguments("create", PAGEARG_STRING,
#
function SPITFORM($formfields, $errors)
{
global $this_user, $projlist;
global $this_user, $projlist, $action, $idx;
$editing = 0;
if ($action == "edit") {
$button_label = "Modify";
$title = "Modify Profile";
$editing = 1;
}
else {
$button_label = "Create";
$title = "Create Profile";
}
# XSS prevention.
while (list ($key, $val) = each ($formfields)) {
$formfields[$key] = CleanString($val);
......@@ -64,7 +78,8 @@ function SPITFORM($formfields, $errors)
echo " <label for='$field' ".
" class='col-sm-3 control-label'>$label ";
if ($help) {
echo "<a href='#' data-toggle='tooltip' title='$help'>".
echo "<a href='#' class='btn btn-xs'
data-toggle='popover' data-content='$help'>".
"<span class='glyphicon glyphicon-question-sign'></span></a>";
}
echo " </label>\n";
......@@ -87,9 +102,7 @@ function SPITFORM($formfields, $errors)
col-xs-12'>\n";
echo " <div class='panel panel-default'>
<div class='panel-heading'>
<h3 class='panel-title'>
Create Profile
</h3>
<h3 class='panel-title'>$title</h3>
</div>
<div class='panel-body'>\n";
......@@ -107,22 +120,43 @@ function SPITFORM($formfields, $errors)
if ($errors && array_key_exists("error", $errors)) {
echo "<font color=red>" . $errors["error"] . "</font>";
}
# Mark as editing mode on post.
if ($editing) {
echo "<input type='hidden' name='action' value='edit'>\n";
}
$formatter("profile_name", "Profile Name",
"<input name=\"formfields[profile_name]\"
# In editing mode, pass through static values.
if ($editing) {
$formatter("profile_name", "Profile Name",
"<p class='form-control-static'>" .
$formfields["profile_name"] . "</p>");
echo "<input type='hidden' name='formfields[profile_name]' ".
"value='" . $formfields["profile_name"] . "'>\n";
}
else {
$formatter("profile_name", "Profile Name",
"<input name=\"formfields[profile_name]\"
id='profile_name'
value='" . $formfields["profile_name"] . "'
class='form-control'
placeholder='' type='text'>",
"alphanumeric, dash, underscore, no whitespace");
"alphanumeric, dash, underscore, no whitespace");
}
#
# If user is a member of only one project, then just pass it
# through, no need for the user to see it. Otherwise we have
# to let the user choose.
#
if (count($projlist) == 1) {
if (count($projlist) == 1 || $editing) {
$pid = ($editing ? $formfields["profile_pid"] : $projlist[0]);
if ($editing) {
$formatter("profile_pid", "Project",
"<p class='form-control-static'>$pid</p>");
}
echo "<input type='hidden' name='formfields[profile_pid]' ".
"value='" . $projlist[0] . "'>\n";
"value='$pid'>\n";
}
else {
$pid_options = "";
......@@ -148,9 +182,38 @@ function SPITFORM($formfields, $errors)
placeholder=''
type='textarea'>" .
$formfields["profile_description"] . "</textarea>");
$formatter("rspecfile", "Your rspec",
"<input name='rspecfile' id='rspecfile'
type=file class='form-control'>");
#
# In edit mode, display current rspec in text area inside a modal.
# See below for the modal. So, we need buttons to display the source
# modal, the topo modal, in addition to a file chooser for a new rspec.
#
if ($editing) {
$rspec_html =
"<div class='row'>
<div class='col-xs-3'>
<button class='btn btn-primary btn-xs'
id='showtopo_modal_button'>
Show Topology</button>
</div>
<div class='col-xs-3'>
<button class='btn btn-primary btn-xs'
data-toggle='modal' data-target='#rspec_modal'>
Edit rspec</button>
</div>
<div class='col-xs-6'>
<input name='rspecfile' id='rspecfile' type=file
class='filestyle'
data-classButton='btn btn-primary btn-xs'
data-input='false' data-buttonText='Choose new file'>
</div>
</div>\n";
}
else {
$rspec_html = "<input name='rspecfile' id='rspecfile'
type=file class='form-control'>";
}
$formatter("profile_rspec", "Your rspec", $rspec_html);
$formatter("profile_public", "Public?",
"<div class='checkbox'>
......@@ -164,14 +227,51 @@ function SPITFORM($formfields, $errors)
echo "<div class='form-group'>
<div class='col-sm-offset-2 col-sm-10'>
<button class='btn btn-primary btm-sm pull-right'
<button class='btn btn-primary btm-xs pull-right'
id='profile_submit_button'
type='submit' name='create'>Create</button>
</div>
</div>\n";
type='submit' name='create'>$button_label</button>\n";
echo " <a class='btn btn-primary btm-xs pull-right'
style='margin-right: 10px;'
href='quickvm.php?profile=$idx'
type='submit' name='create'>Instantiate</a>\n";
if ($editing) {
echo " <a class='btn btn-danger btm-xs pull-left'
style='margin-right: 10px;'
href='manage_profile.php?action=delete&idx=$idx'
type='button' name='delete'>Delete</a>\n";
}
echo " </div>\n";
echo "</div>\n";
echo " </div>\n";
echo " </div>\n";
echo "<!-- This is the rspec text view modal -->
<div id='rspec_modal' class='modal fade'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class='modal-header'>
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>
&times;</button>
<h3>rspec XML</h3>
</div>
<div class='modal-body'>
<div class='panel panel-default'>
<div class='panel-body'>
<textarea name=\"formfields[profile_rspec]\"
id='profile_rspec'
rows=20
class='form-control'
placeholder=''
type='textarea'>" .
$formfields["profile_rspec"] . "</textarea>
</div>
</div>
</div>
</div>
</div>
</div>\n";
echo " </form></div>\n";
echo " </div>\n";
echo " </div>\n";
......@@ -200,7 +300,8 @@ function SPITFORM($formfields, $errors)
</div>
</div>\n";
echo "<script src='js/lib/require.js' data-main='js/manage_profile'></script>";
echo "<script src='js/lib/require.js' data-main='js/manage_profile'>
</script>";
SPITFOOTER();
}
......@@ -214,6 +315,7 @@ if (!$this_user) {
RedirectLoginPage();
exit();
}
$this_idx = $this_user->uid_idx();