From 9da93c7caf2c12822d23225acec98aedd8ace198 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 6 Jan 2014 15:20:58 -0500 Subject: [PATCH] Add frontend and API support for deleting tags. Model support is needed. --- data/model.py | 3 ++ endpoints/api.py | 18 ++++++++ initdb.py | 4 ++ static/css/quay.css | 28 +++++++++++++ static/js/controllers.js | 74 +++++++++++++++++++++++++++++++++ static/js/graphing.js | 41 +++++++++++++++--- static/partials/view-repo.html | 33 ++++++++++++++- test/data/test.db | Bin 339968 -> 339968 bytes 8 files changed, 195 insertions(+), 6 deletions(-) diff --git a/data/model.py b/data/model.py index 9182950be..7c4f80fb5 100644 --- a/data/model.py +++ b/data/model.py @@ -740,6 +740,9 @@ def list_repository_tags(namespace_name, repository_name): return with_image.where(Repository.name == repository_name, Repository.namespace == namespace_name) +def delete_tag_and_images(namespace_name, repository_name, tag_name): + # TODO: Implement this. + pass def get_tag_image(namespace_name, repository_name, tag_name): joined = Image.select().join(RepositoryTag).join(Repository) diff --git a/endpoints/api.py b/endpoints/api.py index b954befb1..d1c5f8d2d 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1158,6 +1158,24 @@ def get_image_changes(namespace, repository, image_id): abort(403) +@app.route('/api/repository//tag/', + methods=['DELETE']) +@parse_repository_name +def delete_full_tag(namespace, repository, tag): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + model.delete_tag_and_images(namespace, repository, tag) + + username = current_user.db_user().username + log_action('delete_tag', namespace, + {'username': username, 'repo': repository, 'tag': tag}, + repo=model.get_repository(namespace, repository)) + + return make_response('Deleted', 204) + + abort(403) # Permission denied + + @app.route('/api/repository//tag//images', methods=['GET']) @parse_repository_name diff --git a/initdb.py b/initdb.py index 5dc75e52f..8df4e7f00 100644 --- a/initdb.py +++ b/initdb.py @@ -123,6 +123,7 @@ def initialize_database(): LogEntryKind.create(name='push_repo') LogEntryKind.create(name='pull_repo') LogEntryKind.create(name='delete_repo') + LogEntryKind.create(name='delete_tag') LogEntryKind.create(name='add_repo_permission') LogEntryKind.create(name='change_repo_permission') LogEntryKind.create(name='delete_repo_permission') @@ -279,6 +280,9 @@ def populate_database(): model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today, metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'}) + model.log_action('delete_tag', org.username, performer=new_user_2, repository=org_repo, timestamp=today, + metadata={'username': new_user_2.username, 'repo': 'orgrepo', 'tag': 'sometag'}) + if __name__ == '__main__': logging.basicConfig(**app.config['LOGGING_CONFIG']) initialize_database() diff --git a/static/css/quay.css b/static/css/quay.css index 8cbb3bb25..7adb2cfd7 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1148,6 +1148,34 @@ p.editable:hover i { font-size: 1.15em; } +#tagContextMenu { + display: none; + position: absolute; + z-index: 100000; + outline: none; +} + +#tagContextMenu ul { + display: block; + position: static; + margin-bottom: 5px; +} + +.tag-controls { + display: inline-block; + margin-right: 20px; + margin-top: 2px; + opacity: 0; + float: right; + -webkit-transition: opacity 200ms ease-in-out; + -moz-transition: opacity 200ms ease-in-out; + transition: opacity 200ms ease-in-out; +} + +.tag-heading:hover .tag-controls { + opacity: 1; +} + .right-title { display: inline-block; float: right; diff --git a/static/js/controllers.js b/static/js/controllers.js index f66f052dc..c58dd564a 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -194,6 +194,38 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo } }; + $scope.askDeleteTag = function(tagName) { + if (!$scope.repo.can_admin) { return; } + + $scope.tagToDelete = tagName; + $('#confirmdeleteTagModal').modal('show'); + }; + + $scope.deleteTag = function(tagName) { + if (!$scope.repo.can_admin) { return; } + $('#confirmdeleteTagModal').modal('hide'); + + var params = { + 'repository': namespace + '/' + name, + 'tag': tagName + }; + + ApiService.deleteFullTag(null, params).then(function() { + loadViewInfo(); + }, function(resp) { + bootbox.dialog({ + "message": resp.data ? resp.data : 'Could not delete tag', + "title": "Cannot delete tag", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + $scope.setTag = function(tagName, opt_updateURL) { var repo = $scope.repo; var proposedTag = repo.tags[tagName]; @@ -229,6 +261,40 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo return count; }; + $scope.hideTagMenu = function(tagName, clientX, clientY) { + $scope.currentMenuTag = null; + + var tagMenu = $("#tagContextMenu"); + tagMenu.hide(); + }; + + $scope.showTagMenu = function(tagName, clientX, clientY) { + if (!$scope.repo.can_admin) { return; } + + $scope.currentMenuTag = tagName; + + var tagMenu = $("#tagContextMenu"); + tagMenu.css({ + display: "block", + left: clientX, + top: clientY + }); + + tagMenu.on("blur", function() { + setTimeout(function() { + tagMenu.hide(); + }, 100); // Needed to allow clicking on menu items. + }); + + tagMenu.on("click", "a", function() { + setTimeout(function() { + tagMenu.hide(); + }, 100); // Needed to allow clicking on menu items. + }); + + tagMenu[0].focus(); + }; + var getDefaultTag = function() { if ($scope.repo === undefined) { return undefined; @@ -343,6 +409,14 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo $scope.$apply(function() { $scope.setImage(e.image); }); }); + $($scope.tree).bind('showTagMenu', function(e) { + $scope.$apply(function() { $scope.showTagMenu(e.tag, e.clientX, e.clientY); }); + }); + + $($scope.tree).bind('hideTagMenu', function(e) { + $scope.$apply(function() { $scope.hideTagMenu(); }); + }); + return resp.images; }); }; diff --git a/static/js/graphing.js b/static/js/graphing.js index fbf5f2162..b7466da6a 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -69,6 +69,25 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) { }; +ImageHistoryTree.prototype.setupOverscroll_ = function() { + var container = this.container_; + var that = this; + var overscroll = $('#' + container).overscroll(); + + overscroll.on('overscroll:dragstart', function() { + $(that).trigger({ + 'type': 'hideTagMenu' + }); + }); + + overscroll.on('scroll', function() { + $(that).trigger({ + 'type': 'hideTagMenu' + }); + }); +}; + + /** * Updates the dimensions of the tree. */ @@ -88,8 +107,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() { var boundingBox = document.getElementById(container).getBoundingClientRect(); document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px'; - $('#' + container).overscroll(); - + this.setupOverscroll_(); + // Update the tree. var rootSvg = this.rootSvg_; var tree = this.tree_; @@ -183,8 +202,7 @@ ImageHistoryTree.prototype.draw = function(container) { this.root_.y0 = 0; this.setTag_(this.currentTag_); - - $('#' + container).overscroll(); + this.setupOverscroll_(); }; @@ -642,7 +660,7 @@ ImageHistoryTree.prototype.update_ = function(source) { if (tag == currentTag) { kind = 'success'; } - html += '' + tag + ''; + html += '' + tag + ''; } return html; }); @@ -654,6 +672,19 @@ ImageHistoryTree.prototype.update_ = function(source) { if (tag) { that.changeTag_(tag); } + }) + .on("contextmenu", function(d, e) { + d3.event.preventDefault(); + + var tag = this.getAttribute('data-tag'); + if (tag) { + $(that).trigger({ + 'type': 'showTagMenu', + 'tag': tag, + 'clientX': d3.event.clientX, + 'clientY': d3.event.clientY + }); + } }); // Ensure the tags are visible. diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index ebc1ea5b2..7d119d4fa 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -1,3 +1,9 @@ + +
@@ -73,7 +79,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}
-
+
Tags + + Delete Tag +
@@ -180,3 +189,25 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}
+ + + + diff --git a/test/data/test.db b/test/data/test.db index c2ef5d6528ab19aa099da05eaf5e87dfae0471c3..41dd4cb0be5ce20621ce683203bc2d999063cdcb 100644 GIT binary patch delta 3599 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 ChXG6g