Add frontend and API support for deleting tags. Model support is needed.
This commit is contained in:
parent
ee80f43375
commit
9da93c7caf
8 changed files with 195 additions and 6 deletions
|
@ -740,6 +740,9 @@ def list_repository_tags(namespace_name, repository_name):
|
||||||
return with_image.where(Repository.name == repository_name,
|
return with_image.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_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):
|
def get_tag_image(namespace_name, repository_name, tag_name):
|
||||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||||
|
|
|
@ -1158,6 +1158,24 @@ def get_image_changes(namespace, repository, image_id):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>/tag/<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/<path:repository>/tag/<tag>/images',
|
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
|
|
@ -123,6 +123,7 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='push_repo')
|
LogEntryKind.create(name='push_repo')
|
||||||
LogEntryKind.create(name='pull_repo')
|
LogEntryKind.create(name='pull_repo')
|
||||||
LogEntryKind.create(name='delete_repo')
|
LogEntryKind.create(name='delete_repo')
|
||||||
|
LogEntryKind.create(name='delete_tag')
|
||||||
LogEntryKind.create(name='add_repo_permission')
|
LogEntryKind.create(name='add_repo_permission')
|
||||||
LogEntryKind.create(name='change_repo_permission')
|
LogEntryKind.create(name='change_repo_permission')
|
||||||
LogEntryKind.create(name='delete_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,
|
model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today,
|
||||||
metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'})
|
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__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
||||||
initialize_database()
|
initialize_database()
|
||||||
|
|
|
@ -1148,6 +1148,34 @@ p.editable:hover i {
|
||||||
font-size: 1.15em;
|
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 {
|
.right-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
|
@ -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) {
|
$scope.setTag = function(tagName, opt_updateURL) {
|
||||||
var repo = $scope.repo;
|
var repo = $scope.repo;
|
||||||
var proposedTag = repo.tags[tagName];
|
var proposedTag = repo.tags[tagName];
|
||||||
|
@ -229,6 +261,40 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
||||||
return count;
|
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() {
|
var getDefaultTag = function() {
|
||||||
if ($scope.repo === undefined) {
|
if ($scope.repo === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -343,6 +409,14 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
||||||
$scope.$apply(function() { $scope.setImage(e.image); });
|
$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;
|
return resp.images;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
* Updates the dimensions of the tree.
|
||||||
*/
|
*/
|
||||||
|
@ -88,7 +107,7 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
||||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||||
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
|
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
|
||||||
|
|
||||||
$('#' + container).overscroll();
|
this.setupOverscroll_();
|
||||||
|
|
||||||
// Update the tree.
|
// Update the tree.
|
||||||
var rootSvg = this.rootSvg_;
|
var rootSvg = this.rootSvg_;
|
||||||
|
@ -183,8 +202,7 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
this.root_.y0 = 0;
|
this.root_.y0 = 0;
|
||||||
|
|
||||||
this.setTag_(this.currentTag_);
|
this.setTag_(this.currentTag_);
|
||||||
|
this.setupOverscroll_();
|
||||||
$('#' + container).overscroll();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -642,7 +660,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (tag == currentTag) {
|
if (tag == currentTag) {
|
||||||
kind = 'success';
|
kind = 'success';
|
||||||
}
|
}
|
||||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"">' + tag + '</span>';
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
});
|
});
|
||||||
|
@ -654,6 +672,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (tag) {
|
if (tag) {
|
||||||
that.changeTag_(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.
|
// Ensure the tags are visible.
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<div id="tagContextMenu" class="dropdown clearfix" tabindex="-1">
|
||||||
|
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
|
||||||
|
<li><a tabindex="-1" href="javascript:void(0)" ng-click="askDeleteTag(currentMenuTag)">Delete Tag</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
|
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
|
||||||
<div class="container repo">
|
<div class="container repo">
|
||||||
<!-- Repo Header -->
|
<!-- Repo Header -->
|
||||||
|
@ -73,7 +79,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
||||||
<!-- Tree View container -->
|
<!-- Tree View container -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading tag-heading">
|
||||||
<!-- Tag dropdown -->
|
<!-- Tag dropdown -->
|
||||||
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
|
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
|
||||||
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
||||||
|
@ -85,6 +91,9 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<span class="right-title">Tags</span>
|
<span class="right-title">Tags</span>
|
||||||
|
<span class="tag-controls" ng-show="repo.can_admin">
|
||||||
|
<a href="javascript:void(0)" ng-click="askDeleteTag(currentTag.name)">Delete Tag</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image history tree -->
|
<!-- Image history tree -->
|
||||||
|
@ -180,3 +189,25 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="confirmdeleteTagModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Delete tag <span class="label label-default tag">{{ tagToDelete }}</span>?</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you sure you want to delete tag <span class="label label-default tag">{{ tagToDelete }}</span>?
|
||||||
|
<br><br>
|
||||||
|
Doing so will delete any images no attached to another tag.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="deleteTag(tagToDelete)">Delete Tag</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
Binary file not shown.
Reference in a new issue