Add ability to tag images from the UI, including moving existing tags to different images

This commit is contained in:
Joseph Schorr 2014-02-28 00:12:09 -05:00
parent 9371c70941
commit 20ad666308
11 changed files with 581 additions and 200 deletions

View file

@ -1316,6 +1316,8 @@ quayApp.directive('logsView', function () {
}
},
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
@ -1353,35 +1355,37 @@ quayApp.directive('logsView', function () {
};
var logKinds = {
'account_change_plan': 'Change plan',
'account_change_cc': 'Update credit card',
'account_change_password': 'Change password',
'account_convert': 'Convert account to organization',
'create_robot': 'Create Robot Account',
'delete_robot': 'Delete Robot Account',
'create_repo': 'Create Repository',
'push_repo': 'Push to repository',
'pull_repo': 'Pull repository',
'delete_repo': 'Delete repository',
'change_repo_permission': 'Change repository permission',
'delete_repo_permission': 'Remove user permission from repository',
'change_repo_visibility': 'Change repository visibility',
'add_repo_accesstoken': 'Create access token',
'delete_repo_accesstoken': 'Delete access token',
'add_repo_webhook': 'Add webhook',
'delete_repo_webhook': 'Delete webhook',
'set_repo_description': 'Change repository description',
'build_dockerfile': 'Build image from Dockerfile',
'delete_tag': 'Delete Tag',
'org_create_team': 'Create team',
'org_delete_team': 'Delete team',
'org_add_team_member': 'Add team member',
'org_remove_team_member': 'Remove team member',
'org_set_team_description': 'Change team description',
'org_set_team_role': 'Change team permission',
'create_prototype_permission': 'Create default permission',
'modify_prototype_permission': 'Modify default permission',
'delete_prototype_permission': 'Delete default permission'
'account_change_plan': 'Change plan',
'account_change_cc': 'Update credit card',
'account_change_password': 'Change password',
'account_convert': 'Convert account to organization',
'create_robot': 'Create Robot Account',
'delete_robot': 'Delete Robot Account',
'create_repo': 'Create Repository',
'push_repo': 'Push to repository',
'pull_repo': 'Pull repository',
'delete_repo': 'Delete repository',
'change_repo_permission': 'Change repository permission',
'delete_repo_permission': 'Remove user permission from repository',
'change_repo_visibility': 'Change repository visibility',
'add_repo_accesstoken': 'Create access token',
'delete_repo_accesstoken': 'Delete access token',
'add_repo_webhook': 'Add webhook',
'delete_repo_webhook': 'Delete webhook',
'set_repo_description': 'Change repository description',
'build_dockerfile': 'Build image from Dockerfile',
'delete_tag': 'Delete Tag',
'create_tag': 'Create Tag',
'move_tag': 'Move Tag',
'org_create_team': 'Create team',
'org_delete_team': 'Delete team',
'org_add_team_member': 'Add team member',
'org_remove_team_member': 'Remove team member',
'org_set_team_description': 'Change team description',
'org_set_team_role': 'Change team permission',
'create_prototype_permission': 'Create default permission',
'modify_prototype_permission': 'Modify default permission',
'delete_prototype_permission': 'Delete default permission'
};
var getDateString = function(date) {
@ -1468,7 +1472,9 @@ quayApp.directive('logsView', function () {
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
'original_role': 'th-large',
'image': 'archive',
'original_image': 'archive'
};
log.metadata['_ip'] = log.ip ? log.ip : null;
@ -1481,6 +1487,10 @@ quayApp.directive('logsView', function () {
for (var key in log.metadata) {
if (log.metadata.hasOwnProperty(key)) {
var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)';
if (key.indexOf('image') >= 0) {
value = value.substr(0, 12);
}
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
@ -3023,6 +3033,121 @@ quayApp.directive('dockerfileBuildForm', function () {
});
quayApp.directive('tagSpecificImagesView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/tag-specific-images-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'tag': '=tag',
'images': '=images'
},
controller: function($scope, $element) {
$scope.getFirstTextLine = getFirstTextLine;
$scope.hasImages = false;
$scope.tagSpecificImages = [];
$scope.getImageListingClasses = function(image) {
var classes = '';
if (image.ancestors.length > 1) {
classes += 'child ';
}
var currentTag = $scope.repository.tags[$scope.tag];
if (image.dbid == currentTag.image.dbid) {
classes += 'tag-image ';
}
return classes;
};
var forAllTagImages = function(tag, callback) {
if (!tag) { return; }
callback(tag.image);
if (!$scope.imageByDBID) {
$scope.imageByDBID = [];
for (var i = 0; i < $scope.images.length; ++i) {
var currentImage = $scope.images[i];
$scope.imageByDBID[currentImage.dbid] = currentImage;
}
}
var ancestors = tag.image.ancestors.split('/');
for (var i = 0; i < ancestors.length; ++i) {
var image = $scope.imageByDBID[ancestors[i]];
if (image) {
callback(image);
}
}
};
var refresh = function() {
if (!$scope.repository || !$scope.tag || !$scope.images) {
$scope.tagSpecificImages = [];
return;
}
var tag = $scope.repository.tags[$scope.tag];
if (!tag) {
$scope.tagSpecificImages = [];
return;
}
var getIdsForTag = function(currentTag) {
var ids = {};
forAllTagImages(currentTag, function(image) {
ids[image.dbid] = true;
});
return ids;
};
// Remove any IDs that match other tags.
var toDelete = getIdsForTag(tag);
for (var currentTagName in $scope.repository.tags) {
var currentTag = $scope.repository.tags[currentTagName];
if (currentTag != tag) {
for (var dbid in getIdsForTag(currentTag)) {
delete toDelete[dbid];
}
}
}
// Return the matching list of images.
var images = [];
for (var i = 0; i < $scope.images.length; ++i) {
var image = $scope.images[i];
if (toDelete[image.dbid]) {
images.push(image);
}
}
images.sort(function(a, b) {
var result = new Date(b.created) - new Date(a.created);
if (result != 0) {
return result;
}
return b.dbid - a.dbid;
});
$scope.tagSpecificImages = images;
};
$scope.$watch('repository', refresh);
$scope.$watch('tag', refresh);
$scope.$watch('images', refresh);
}
};
return directiveDefinitionObject;
});
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
return function( scope, elem, attrs ) {

View file

@ -384,9 +384,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$scope.getMoreCount = function(changes) {
if (!changes) { return 0; }
var addedDisplayed = Math.min(5, changes.added.length);
var removedDisplayed = Math.min(5, changes.removed.length);
var changedDisplayed = Math.min(5, changes.changed.length);
var addedDisplayed = Math.min(2, changes.added.length);
var removedDisplayed = Math.min(2, changes.removed.length);
var changedDisplayed = Math.min(2, changes.changed.length);
return (changes.added.length + changes.removed.length + changes.changed.length) -
addedDisplayed - removedDisplayed - changedDisplayed;
@ -415,57 +415,21 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$location.search('tag', null);
$location.search('image', imageId.substr(0, 12));
}
};
$scope.showAddTag = function(image) {
$scope.toTagImage = image;
$('#addTagModal').modal('show');
};
$scope.tagSpecificImages = function(tagName) {
if (!tagName) { return []; }
$scope.isOwnedTag = function(image, tagName) {
if (!image || !tagName) { return false; }
return image.tags.indexOf(tagName) >= 0;
};
var tag = $scope.repo.tags[tagName];
if (!tag) { return []; }
if ($scope.specificImages && $scope.specificImages[tagName]) {
return $scope.specificImages[tagName];
}
var getIdsForTag = function(currentTag) {
var ids = {};
forAllTagImages(currentTag, function(image) {
ids[image.dbid] = true;
});
return ids;
};
// Remove any IDs that match other tags.
var toDelete = getIdsForTag(tag);
for (var currentTagName in $scope.repo.tags) {
var currentTag = $scope.repo.tags[currentTagName];
if (currentTag != tag) {
for (var dbid in getIdsForTag(currentTag)) {
delete toDelete[dbid];
}
}
}
// Return the matching list of images.
var images = [];
for (var i = 0; i < $scope.images.length; ++i) {
var image = $scope.images[i];
if (toDelete[image.dbid]) {
images.push(image);
}
}
images.sort(function(a, b) {
var result = new Date(b.created) - new Date(a.created);
if (result != 0) {
return result;
}
return b.dbid - a.dbid;
});
$scope.specificImages[tagName] = images;
return images;
$scope.isAnotherImageTag = function(image, tagName) {
if (!image || !tagName) { return false; }
return image.tags.indexOf(tagName) < 0 && $scope.repo.tags[tagName];
};
$scope.askDeleteTag = function(tagName) {
@ -475,6 +439,39 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$('#confirmdeleteTagModal').modal('show');
};
$scope.createOrMoveTag = function(image, tagName, opt_invalid) {
if (opt_invalid) { return; }
$scope.creatingTag = true;
var params = {
'repository': $scope.repo.namespace + '/' + $scope.repo.name,
'tag': tagName
};
var data = {
'image': image.id
};
ApiService.changeTagImage(data, params).then(function(resp) {
$scope.creatingTag = false;
loadViewInfo();
$('#addTagModal').modal('hide');
}, function(resp) {
$('#addTagModal').modal('hide');
bootbox.dialog({
"message": resp.data ? resp.data : 'Could not create or move tag',
"title": "Cannot create or move tag",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.deleteTag = function(tagName) {
if (!$scope.repo.can_admin) { return; }
$('#confirmdeleteTagModal').modal('hide');
@ -555,20 +552,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$scope.getFirstTextLine = getFirstTextLine;
$scope.getImageListingClasses = function(image, tagName) {
var classes = '';
if (image.ancestors.length > 1) {
classes += 'child ';
}
var currentTag = $scope.repo.tags[tagName];
if (image.dbid == currentTag.image.dbid) {
classes += 'tag-image ';
}
return classes;
};
$scope.getTagCount = function(repo) {
if (!repo) { return 0; }
var count = 0;

View file

@ -115,12 +115,20 @@ ImageHistoryTree.prototype.setupOverscroll_ = function() {
$(that).trigger({
'type': 'hideTagMenu'
});
$(that).trigger({
'type': 'hideImageMenu'
});
});
overscroll.on('scroll', function() {
$(that).trigger({
'type': 'hideTagMenu'
});
$(that).trigger({
'type': 'hideImageMenu'
});
});
};
@ -634,7 +642,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
.text(function(d) { return d.name; })
.on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
.on('mouseout', tip.hide)
.on("contextmenu", function(d, e) {
d3.event.preventDefault();
if (d.image) {
$(that).trigger({
'type': 'showImageMenu',
'image': d.image.id,
'clientX': d3.event.clientX,
'clientY': d3.event.clientY
});
}
});
nodeEnter.selectAll("tags")
.append("svg:text")
@ -685,9 +705,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
if (d.virtual) {
return 'virtual';
}
if (!currentImage) {
return '';
}
if (!currentImage) {
return '';
}
return d.image.id == currentImage.id ? 'current' : '';
});
@ -702,15 +722,16 @@ ImageHistoryTree.prototype.update_ = function(source) {
return '';
}
var html = '';
var html = '<div style="width: ' + DEPTH_HEIGHT + 'px">';
for (var i = 0; i < d.tags.length; ++i) {
var tag = d.tags[i];
var kind = 'default';
if (tag == currentTag) {
kind = 'success';
}
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"">' + tag + '</span>';
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>';
}
html += '</div>';
return html;
});