Merge branch 'tagyourit'
This commit is contained in:
commit
d8efb399b0
12 changed files with 641 additions and 172 deletions
|
@ -1,5 +1,7 @@
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_admin,
|
from flask import request
|
||||||
RepositoryParamResource, log_action, NotFound)
|
|
||||||
|
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||||
|
RepositoryParamResource, log_action, NotFound, validate_json_request)
|
||||||
from endpoints.api.image import image_view
|
from endpoints.api.image import image_view
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -8,8 +10,54 @@ from auth.auth_context import get_authenticated_user
|
||||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||||
class RepositoryTag(RepositoryParamResource):
|
class RepositoryTag(RepositoryParamResource):
|
||||||
""" Resource for managing repository tags. """
|
""" Resource for managing repository tags. """
|
||||||
|
schemas = {
|
||||||
|
'MoveTag': {
|
||||||
|
'id': 'MoveTag',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of to which image a new or existing tag should point',
|
||||||
|
'required': [
|
||||||
|
'image',
|
||||||
|
],
|
||||||
|
'properties': {
|
||||||
|
'image': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Image identifier to which the tag should point',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_write
|
||||||
|
@nickname('changeTagImage')
|
||||||
|
@validate_json_request('MoveTag')
|
||||||
|
def put(self, namespace, repository, tag):
|
||||||
|
""" Change which image a tag points to or create a new tag."""
|
||||||
|
image_id = request.get_json()['image']
|
||||||
|
image = model.get_repo_image(namespace, repository, image_id)
|
||||||
|
if not image:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
original_image_id = None
|
||||||
|
try:
|
||||||
|
original_tag_image = model.get_tag_image(namespace, repository, tag)
|
||||||
|
if original_tag_image:
|
||||||
|
original_image_id = original_tag_image.docker_image_id
|
||||||
|
except model.DataModelException:
|
||||||
|
# This is a new tag.
|
||||||
|
pass
|
||||||
|
|
||||||
|
model.create_or_update_tag(namespace, repository, tag, image_id)
|
||||||
|
model.garbage_collect_repository(namespace, repository)
|
||||||
|
|
||||||
|
username = get_authenticated_user().username
|
||||||
|
log_action('move_tag' if original_image_id else 'create_tag', namespace,
|
||||||
|
{ 'username': username, 'repo': repository, 'tag': tag,
|
||||||
|
'image': image_id, 'original_image': original_image_id },
|
||||||
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
return 'Updated', 201
|
||||||
|
|
||||||
|
@require_repo_write
|
||||||
@nickname('deleteFullTag')
|
@nickname('deleteFullTag')
|
||||||
def delete(self, namespace, repository, tag):
|
def delete(self, namespace, repository, tag):
|
||||||
""" Delete the specified repository tag. """
|
""" Delete the specified repository tag. """
|
||||||
|
|
|
@ -196,6 +196,8 @@ 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='create_tag')
|
||||||
|
LogEntryKind.create(name='move_tag')
|
||||||
LogEntryKind.create(name='delete_tag')
|
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')
|
||||||
|
|
|
@ -995,6 +995,24 @@ i.toggle-icon:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visible-xl {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible-xl-inline {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.visible-xl {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible-xl-inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plans-list .plan-box .description {
|
.plans-list .plan-box .description {
|
||||||
color: white;
|
color: white;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
@ -1528,22 +1546,22 @@ p.editable:hover i {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings {
|
.tag-specific-images-view .image-listings {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing {
|
.tag-specific-images-view .image-listings .image-listing {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-id {
|
.tag-specific-images-view .image-listings .image-listing .image-listing-id {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-line {
|
.tag-specific-images-view .image-listings .image-listing .image-listing-line {
|
||||||
border-left: 2px solid steelblue;
|
border-left: 2px solid steelblue;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1554,15 +1572,15 @@ p.editable:hover i {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-line {
|
.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-line {
|
||||||
top: 8px;
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing.child .image-listing-line {
|
.tag-specific-images-view .image-listings .image-listing.child .image-listing-line {
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-circle {
|
.tag-specific-images-view .image-listings .image-listing .image-listing-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|
||||||
|
@ -1575,14 +1593,55 @@ p.editable:hover i {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-circle {
|
.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-circle {
|
||||||
background: steelblue;
|
background: steelblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .more-changes {
|
.tag-specific-images-view .more-changes {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo.container-fluid {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.repo.container-fluid {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.repo.container-fluid {
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo.container-fluid .col-md-4 {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo.container-fluid .col-md-8 {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.repo .current-context {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 200px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .current-context-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.repo .header {
|
.repo .header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
|
@ -1798,6 +1857,77 @@ p.editable:hover i {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo .image-comment {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section .tag {
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.repo .image-section .section-icon {
|
||||||
|
float: left;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: 14px;
|
||||||
|
color: #bbb;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section .section-info {
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 6px;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
|
vertical-align: middle;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section .section-info-with-dropdown {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section .dropdown {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 2px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .image-section .dropdown-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-list {
|
.repo-list {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
@ -2106,19 +2236,11 @@ p.editable:hover i {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .small-changes-container:before {
|
|
||||||
content: "File Changes: ";
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
float: left;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo .formatted-command {
|
.repo .formatted-command {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .formatted-command.trimmed {
|
.repo .formatted-command.trimmed {
|
||||||
|
@ -2127,16 +2249,22 @@ p.editable:hover i {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .changes-count-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo .change-count {
|
.repo .change-count {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo .change-count b {
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .changes-container .well {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.repo .changes-container i.fa-plus-square {
|
.repo .changes-container i.fa-plus-square {
|
||||||
color: rgb(73, 209, 73);
|
color: rgb(73, 209, 73);
|
||||||
}
|
}
|
||||||
|
@ -2154,7 +2282,7 @@ p.editable:hover i {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .change-count i {
|
.repo .change-count i {
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2166,6 +2294,7 @@ p.editable:hover i {
|
||||||
|
|
||||||
.repo .more-changes {
|
.repo .more-changes {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo #collapseChanges .well {
|
.repo #collapseChanges .well {
|
||||||
|
@ -2406,10 +2535,13 @@ p.editable:hover i {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags .tag, #confirmdeleteTagModal .tag {
|
.tags .tag, .tag-specific-images-view .tag {
|
||||||
|
display: inline-block;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-tags {
|
.tooltip-tags {
|
||||||
|
|
17
static/directives/tag-specific-images-view.html
Normal file
17
static/directives/tag-specific-images-view.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="tag-specific-images-view-element" ng-show="tagSpecificImages.length">
|
||||||
|
<div ng-transclude></div>
|
||||||
|
<div class="image-listings">
|
||||||
|
<div class="image-listing" ng-repeat="image in tagSpecificImages | limitTo:5"
|
||||||
|
ng-class="getImageListingClasses(image)">
|
||||||
|
<span class="image-listing-circle"></span>
|
||||||
|
<span class="image-listing-line"></span>
|
||||||
|
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="getFirstTextLine(image.comment)"
|
||||||
|
data-html="true">
|
||||||
|
{{ image.id.substr(0, 12) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="more-changes" ng-show="tagSpecificImages.length > 5">
|
||||||
|
And {{ tagSpecificImages.length - 5 }} more...
|
||||||
|
</div>
|
||||||
|
</div>
|
124
static/js/app.js
124
static/js/app.js
|
@ -425,6 +425,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'role': 'th-large',
|
'role': 'th-large',
|
||||||
'original_role': 'th-large',
|
'original_role': 'th-large',
|
||||||
'application_name': 'cloud',
|
'application_name': 'cloud',
|
||||||
|
'image': 'archive',
|
||||||
|
'original_image': 'archive',
|
||||||
'client_id': 'chain'
|
'client_id': 'chain'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -436,6 +438,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
for (var key in metadata) {
|
for (var key in metadata) {
|
||||||
if (metadata.hasOwnProperty(key)) {
|
if (metadata.hasOwnProperty(key)) {
|
||||||
var value = metadata[key] != null ? metadata[key].toString() : '(Unknown)';
|
var value = metadata[key] != null ? metadata[key].toString() : '(Unknown)';
|
||||||
|
if (key.indexOf('image') >= 0) {
|
||||||
|
value = value.substr(0, 12);
|
||||||
|
}
|
||||||
var markedDown = getMarkedDown(value);
|
var markedDown = getMarkedDown(value);
|
||||||
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
||||||
|
|
||||||
|
@ -2133,6 +2138,8 @@ quayApp.directive('logsView', function () {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
'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}',
|
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
||||||
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
||||||
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
||||||
|
@ -2212,6 +2219,8 @@ quayApp.directive('logsView', function () {
|
||||||
'set_repo_description': 'Change repository description',
|
'set_repo_description': 'Change repository description',
|
||||||
'build_dockerfile': 'Build image from Dockerfile',
|
'build_dockerfile': 'Build image from Dockerfile',
|
||||||
'delete_tag': 'Delete Tag',
|
'delete_tag': 'Delete Tag',
|
||||||
|
'create_tag': 'Create Tag',
|
||||||
|
'move_tag': 'Move Tag',
|
||||||
'org_create_team': 'Create team',
|
'org_create_team': 'Create team',
|
||||||
'org_delete_team': 'Delete team',
|
'org_delete_team': 'Delete team',
|
||||||
'org_add_team_member': 'Add team member',
|
'org_add_team_member': 'Add team member',
|
||||||
|
@ -4522,6 +4531,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.
|
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
|
||||||
quayApp.directive('ngBlur', function() {
|
quayApp.directive('ngBlur', function() {
|
||||||
return function( scope, elem, attrs ) {
|
return function( scope, elem, attrs ) {
|
||||||
|
|
|
@ -398,9 +398,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
$scope.getMoreCount = function(changes) {
|
$scope.getMoreCount = function(changes) {
|
||||||
if (!changes) { return 0; }
|
if (!changes) { return 0; }
|
||||||
var addedDisplayed = Math.min(5, changes.added.length);
|
var addedDisplayed = Math.min(2, changes.added.length);
|
||||||
var removedDisplayed = Math.min(5, changes.removed.length);
|
var removedDisplayed = Math.min(2, changes.removed.length);
|
||||||
var changedDisplayed = Math.min(5, changes.changed.length);
|
var changedDisplayed = Math.min(2, changes.changed.length);
|
||||||
|
|
||||||
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
||||||
addedDisplayed - removedDisplayed - changedDisplayed;
|
addedDisplayed - removedDisplayed - changedDisplayed;
|
||||||
|
@ -431,55 +431,19 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.tagSpecificImages = function(tagName) {
|
$scope.showAddTag = function(image) {
|
||||||
if (!tagName) { return []; }
|
$scope.toTagImage = image;
|
||||||
|
$('#addTagModal').modal('show');
|
||||||
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.
|
$scope.isOwnedTag = function(image, tagName) {
|
||||||
var toDelete = getIdsForTag(tag);
|
if (!image || !tagName) { return false; }
|
||||||
for (var currentTagName in $scope.repo.tags) {
|
return image.tags.indexOf(tagName) >= 0;
|
||||||
var currentTag = $scope.repo.tags[currentTagName];
|
};
|
||||||
if (currentTag != tag) {
|
|
||||||
for (var dbid in getIdsForTag(currentTag)) {
|
|
||||||
delete toDelete[dbid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matching list of images.
|
$scope.isAnotherImageTag = function(image, tagName) {
|
||||||
var images = [];
|
if (!image || !tagName) { return false; }
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
return image.tags.indexOf(tagName) < 0 && $scope.repo.tags[tagName];
|
||||||
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.askDeleteTag = function(tagName) {
|
$scope.askDeleteTag = function(tagName) {
|
||||||
|
@ -489,6 +453,39 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
$('#confirmdeleteTagModal').modal('show');
|
$('#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) {
|
$scope.deleteTag = function(tagName) {
|
||||||
if (!$scope.repo.can_admin) { return; }
|
if (!$scope.repo.can_admin) { return; }
|
||||||
$('#confirmdeleteTagModal').modal('hide');
|
$('#confirmdeleteTagModal').modal('hide');
|
||||||
|
@ -569,20 +566,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
$scope.getFirstTextLine = getFirstTextLine;
|
$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) {
|
$scope.getTagCount = function(repo) {
|
||||||
if (!repo) { return 0; }
|
if (!repo) { return 0; }
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
|
@ -115,12 +115,20 @@ ImageHistoryTree.prototype.setupOverscroll_ = function() {
|
||||||
$(that).trigger({
|
$(that).trigger({
|
||||||
'type': 'hideTagMenu'
|
'type': 'hideTagMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideImageMenu'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
overscroll.on('scroll', function() {
|
overscroll.on('scroll', function() {
|
||||||
$(that).trigger({
|
$(that).trigger({
|
||||||
'type': 'hideTagMenu'
|
'type': 'hideTagMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideImageMenu'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -664,7 +672,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (d.collapsed) { that.expandCollapsed_(d); }
|
if (d.collapsed) { that.expandCollapsed_(d); }
|
||||||
})
|
})
|
||||||
.on('mouseover', tip.show)
|
.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")
|
nodeEnter.selectAll("tags")
|
||||||
.append("svg:text")
|
.append("svg:text")
|
||||||
|
@ -732,15 +752,16 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = '';
|
var html = '<div style="width: ' + DEPTH_HEIGHT + 'px">';
|
||||||
for (var i = 0; i < d.tags.length; ++i) {
|
for (var i = 0; i < d.tags.length; ++i) {
|
||||||
var tag = d.tags[i];
|
var tag = d.tags[i];
|
||||||
var kind = 'default';
|
var kind = 'default';
|
||||||
if (tag == currentTag) {
|
if (tag == currentTag) {
|
||||||
kind = 'success';
|
kind = 'success';
|
||||||
}
|
}
|
||||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '">' + tag + '</span>';
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
</div>
|
</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 repo-view">
|
<div class="container-fluid repo repo-view">
|
||||||
<!-- Repo Header -->
|
<!-- Repo Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -127,9 +127,13 @@
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<!-- Dropdown -->
|
<!-- Dropdown -->
|
||||||
<div id="side-panel-dropdown" class="tag-dropdown dropdown" data-placement="top">
|
<div id="side-panel-dropdown" class="tag-dropdown dropdown" data-placement="top">
|
||||||
<i class="fa fa-tag" ng-show="currentTag"></i>
|
<i class="fa fa-tag current-context-icon" ng-show="currentTag"></i>
|
||||||
<i class="fa fa-archive" ng-show="!currentTag"></i>
|
<i class="fa fa-archive current-context-icon" ng-show="!currentTag"></i>
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
<span class="current-context">
|
||||||
|
{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}}
|
||||||
|
</span>
|
||||||
|
<b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li ng-repeat="tag in repo.tags">
|
<li ng-repeat="tag in repo.tags">
|
||||||
<a href="javascript:void(0)" ng-click="setTag(tag.name, true)">
|
<a href="javascript:void(0)" ng-click="setTag(tag.name, true)">
|
||||||
|
@ -186,66 +190,110 @@
|
||||||
|
|
||||||
<!-- Current Image -->
|
<!-- Current Image -->
|
||||||
<div id="current-image" ng-show="currentImage && !currentTag">
|
<div id="current-image" ng-show="currentImage && !currentTag">
|
||||||
<div ng-show="currentImage.comment">
|
<div class="image-comment" ng-if="currentImage.comment">
|
||||||
<blockquote style="margin-top: 10px;">
|
<blockquote style="margin-top: 10px;">
|
||||||
<span class="markdown-view" content="currentImage.comment"></span>
|
<span class="markdown-view" content="currentImage.comment"></span>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="dl-normal">
|
<div class="image-section">
|
||||||
<dt>Created</dt>
|
<i class="fa fa-code section-icon" bs-tooltip="tooltip.title" data-title="Full Image ID"></i>
|
||||||
<dd am-time-ago="parseDate(currentImage.created)"></dd>
|
<span class="section-info">
|
||||||
<dt>Image ID</dt>
|
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a>
|
||||||
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
|
</span>
|
||||||
<dt>Compressed Image Size</dt>
|
</div>
|
||||||
<dd><span class="context-tooltip"
|
|
||||||
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
<div class="image-section">
|
||||||
bs-tooltip="tooltip.title" data-container="body">{{ currentImage.size | bytes }}</span>
|
<i class="fa fa-tag section-icon" bs-tooltip="tooltip.title" data-title="Current Tags"></i>
|
||||||
</dd>
|
<span class="section-info section-info-with-dropdown">
|
||||||
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
|
<a class="label tag label-default" ng-repeat="tag in currentImage.tags"
|
||||||
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
|
href="/repository/{{ repo.namespace }}/{{ repo.name }}?tag={{ tag }}">
|
||||||
<pre class="formatted-command trimmed"
|
{{ tag }}
|
||||||
|
</a>
|
||||||
|
<span style="color: #ccc;" ng-if="!currentImage.tags.length">(No Tags)</span>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="dropdown" data-placement="top" ng-if="repo.can_write || currentImage.tags">
|
||||||
|
<a href="javascript:void(0)" class="dropdown-button" data-toggle="dropdown" bs-tooltip="tooltip.title" data-title="Manage Tags"
|
||||||
|
data-container="body">
|
||||||
|
<b class="caret"></b>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu pull-right">
|
||||||
|
<li ng-repeat="tag in currentImage.tags">
|
||||||
|
<a href="javascript:void(0)" ng-click="setTag(tag, true)">
|
||||||
|
<i class="fa fa-tag"></i>{{tag}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider" role="presentation" ng-if="repo.can_write && currentImage.tags"></li>
|
||||||
|
<li>
|
||||||
|
<a href="javascript:void(0)" ng-click="showAddTag(currentImage)" ng-if="repo.can_write">
|
||||||
|
<i class="fa fa-plus"></i>Add New Tag
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section" ng-if="currentImage.command && currentImage.command.length">
|
||||||
|
<i class="fa fa-terminal section-icon" bs-tooltip="tooltip.title" data-title="Image Command"></i>
|
||||||
|
<span class="section-info">
|
||||||
|
<span class="formatted-command trimmed"
|
||||||
data-html="true"
|
data-html="true"
|
||||||
bs-tooltip="" data-title="{{ getTooltipCommand(currentImage) }}"
|
bs-tooltip="" data-title="{{ getTooltipCommand(currentImage) }}"
|
||||||
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
|
data-placement="top">{{ getFormattedCommand(currentImage) }}</span>
|
||||||
</dd>
|
</span>
|
||||||
</dl>
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<i class="fa fa-calendar section-icon" bs-tooltip="tooltip.title" data-title="Created"></i>
|
||||||
|
<span class="section-info">
|
||||||
|
<dd am-time-ago="parseDate(currentImage.created)"></dd>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-section">
|
||||||
|
<i class="fa fa-cloud-upload section-icon" bs-tooltip="tooltip.title"
|
||||||
|
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"></i>
|
||||||
|
<span class="section-info">{{ currentImage.size | bytes }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Image changes loading -->
|
<!-- Image changes loading -->
|
||||||
<div class="resource-view" resource="currentImageChangeResource">
|
<div class="resource-view" resource="currentImageChangeResource">
|
||||||
<div class="changes-container small-changes-container"
|
<div class="changes-container small-changes-container section-info"
|
||||||
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
||||||
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
<div class="changes-count-container image-section">
|
||||||
|
<i class="fa fa-code-fork section-icon" bs-tooltip="tooltip.title" data-title="File Changes"></i>
|
||||||
|
<div style="float: right; display: inline-block">
|
||||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" data-title="Files Added"
|
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" data-title="Files Added"
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
|
||||||
<i class="fa fa-plus-square"></i>
|
<i class="fa fa-plus-square"></i>
|
||||||
<b>{{currentImageChanges.added.length}}</b>
|
<b>{{currentImageChanges.added.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" data-title="Files Removed"
|
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" data-title="Files Removed"
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
|
||||||
<i class="fa fa-minus-square"></i>
|
<i class="fa fa-minus-square"></i>
|
||||||
<b>{{currentImageChanges.removed.length}}</b>
|
<b>{{currentImageChanges.removed.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" data-title="Files Changed"
|
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" data-title="Files Changed"
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
bs-tooltip="tooltip.title" data-placement="top" data-container="body">
|
||||||
<i class="fa fa-pencil-square"></i>
|
<i class="fa fa-pencil-square"></i>
|
||||||
<b>{{currentImageChanges.changed.length}}</b>
|
<b>{{currentImageChanges.changed.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="collapseChanges" style="padding-top: 24px;">
|
||||||
<div id="collapseChanges" class="panel-collapse collapse in">
|
|
||||||
<div class="well well-sm">
|
<div class="well well-sm">
|
||||||
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:2">
|
||||||
<i class="fa fa-plus-square"></i>
|
<i class="fa fa-plus-square"></i>
|
||||||
<span data-title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:2">
|
||||||
<i class="fa fa-minus-square"></i>
|
<i class="fa fa-minus-square"></i>
|
||||||
<span data-title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:2">
|
||||||
<i class="fa fa-pencil-square"></i>
|
<i class="fa fa-pencil-square"></i>
|
||||||
<span data-title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
||||||
|
@ -270,6 +318,44 @@
|
||||||
build-started="handleBuildStarted(build)">
|
build-started="handleBuildStarted(build)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="addTagModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!creatingTag">×</button>
|
||||||
|
<h4 class="modal-title">{{ isAnotherImageTag(toTagImage, tagToCreate) ? 'Move' : 'Add' }} Tag to Image {{ toTagImage.id.substr(0, 12) }}</h4>
|
||||||
|
</div>
|
||||||
|
<form name="addTagForm" ng-submit="createOrMoveTag(toTagImage, tagToCreate, addTagForm.$invalid); addTagForm.$setPristine(); tagToCreate=''">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" class="form-control" id="tagName" placeholder="Enter tag name"
|
||||||
|
ng-model="tagToCreate" ng-pattern="/^([a-z0-9_]){3,30}$/" required
|
||||||
|
ng-disabled="creatingTag">
|
||||||
|
<div style="margin: 10px; margin-top: 20px;" ng-show="isOwnedTag(toTagImage, tagToCreate)">
|
||||||
|
Note: <span class="label tag label-default">{{ tagToCreate }}</span> is already applied to this image.
|
||||||
|
</div>
|
||||||
|
<div style="margin: 10px; margin-top: 20px;" ng-show="isAnotherImageTag(toTagImage, tagToCreate)">
|
||||||
|
Note: <span class="label tag label-default">{{ tagToCreate }}</span> is already applied to another image. This will <b>move</b> the tag.
|
||||||
|
</div>
|
||||||
|
<div class="tag-specific-images-view" tag="tagToCreate" repository="repo" images="images"
|
||||||
|
style="margin: 10px; margin-top: 20px; margin-bottom: -10px;" ng-show="isAnotherImageTag(toTagImage, tagToCreate)">
|
||||||
|
This will also delete any unattach images and delete the following images:
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary"
|
||||||
|
ng-disabled="!tagToCreate || addTagForm.$invalid || isOwnedTag(toTagImage, tagToCreate)"
|
||||||
|
ng-class="isAnotherImageTag(toTagImage, tagToCreate) ? 'btn-warning' : 'btn-primary'" ng-show="!creatingTag">
|
||||||
|
{{ isAnotherImageTag(toTagImage, tagToCreate) ? 'Move Tag' : 'Create Tag' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" data-dismiss="modal" ng-show="!creatingTag">Cancel</button>
|
||||||
|
<div class="quay-spinner" ng-show="creatingTag"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="confirmdeleteTagModal">
|
<div class="modal fade" id="confirmdeleteTagModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
@ -288,23 +374,8 @@
|
||||||
{{ tagToDelete }}
|
{{ tagToDelete }}
|
||||||
</span>?
|
</span>?
|
||||||
|
|
||||||
<div ng-show="tagSpecificImages(tagToDelete).length" style="margin-top: 20px">
|
<div class="tag-specific-images-view" tag="tagToDelete" repository="repo" images="images" style="margin-top: 20px">
|
||||||
The following images and any other images not referenced by a tag will be deleted:
|
The following images and any other images not referenced by a tag will be deleted:
|
||||||
<div class="image-listings">
|
|
||||||
<div class="image-listing" ng-repeat="image in tagSpecificImages(tagToDelete) | limitTo:5"
|
|
||||||
ng-class="getImageListingClasses(image, tagToDelete)">
|
|
||||||
<!--<i class="fa fa-archive"></i>-->
|
|
||||||
<span class="image-listing-circle"></span>
|
|
||||||
<span class="image-listing-line"></span>
|
|
||||||
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="{{ getFirstTextLine(image.comment) }}"
|
|
||||||
data-html="true">
|
|
||||||
{{ image.id.substr(0, 12) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="more-changes" ng-show="tagSpecificImages(tagToDelete).length > 5">
|
|
||||||
And {{ tagSpecificImages(tagToDelete).length - 5 }} more...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
Binary file not shown.
|
@ -71,6 +71,7 @@ UPDATE_REPO_DETAILS = {
|
||||||
'description': 'A new description',
|
'description': 'A new description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IndexTestSpec(object):
|
class IndexTestSpec(object):
|
||||||
def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403,
|
def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403,
|
||||||
read_code=200, admin_code=200):
|
read_code=200, admin_code=200):
|
||||||
|
|
|
@ -2155,6 +2155,18 @@ class TestRepositoryTagHp8rPublicPublicrepo(ApiTestCase):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RepositoryTag, tag="HP8R", repository="public/publicrepo")
|
self._set_url(RepositoryTag, tag="HP8R", repository="public/publicrepo")
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 403, 'freshuser', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 403, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 401, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
@ -2173,6 +2185,18 @@ class TestRepositoryTagHp8rDevtableShared(ApiTestCase):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RepositoryTag, tag="HP8R", repository="devtable/shared")
|
self._set_url(RepositoryTag, tag="HP8R", repository="devtable/shared")
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 403, 'freshuser', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 404, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 401, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
@ -2191,6 +2215,18 @@ class TestRepositoryTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RepositoryTag, tag="HP8R", repository="buynlarge/orgrepo")
|
self._set_url(RepositoryTag, tag="HP8R", repository="buynlarge/orgrepo")
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 403, 'freshuser', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 404, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 401, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,13 @@ class ApiTestCase(unittest.TestCase):
|
||||||
self.assertEquals(rv.status_code, expected_code)
|
self.assertEquals(rv.status_code, expected_code)
|
||||||
return rv.data
|
return rv.data
|
||||||
|
|
||||||
|
def putResponse(self, resource_name, params={}, data={}, expected_code=200):
|
||||||
|
rv = self.app.put(self.url_for(resource_name, params),
|
||||||
|
data=py_json.dumps(data),
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
self.assertEquals(rv.status_code, expected_code)
|
||||||
|
return rv.data
|
||||||
|
|
||||||
def deleteResponse(self, resource_name, params={}, expected_code=204):
|
def deleteResponse(self, resource_name, params={}, expected_code=204):
|
||||||
rv = self.app.delete(self.url_for(resource_name, params))
|
rv = self.app.delete(self.url_for(resource_name, params))
|
||||||
self.assertEquals(rv.status_code, expected_code)
|
self.assertEquals(rv.status_code, expected_code)
|
||||||
|
@ -1126,7 +1133,7 @@ class TestGetImageChanges(ApiTestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestListAndDeleteTag(ApiTestCase):
|
class TestListAndDeleteTag(ApiTestCase):
|
||||||
def test_listtagimagesanddeletetag(self):
|
def test_listdeletecreateandmovetag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# List the images for prod.
|
# List the images for prod.
|
||||||
|
@ -1159,6 +1166,33 @@ class TestListAndDeleteTag(ApiTestCase):
|
||||||
|
|
||||||
self.assertEquals(staging_images, json['images'])
|
self.assertEquals(staging_images, json['images'])
|
||||||
|
|
||||||
|
# Add a new tag to the staging image.
|
||||||
|
self.putResponse(RepositoryTag,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='sometag'),
|
||||||
|
data=dict(image=staging_images[0]['id']),
|
||||||
|
expected_code=201)
|
||||||
|
|
||||||
|
# Make sure the tag is present.
|
||||||
|
json = self.getJsonResponse(RepositoryTagImages,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='sometag'))
|
||||||
|
|
||||||
|
sometag_images = json['images']
|
||||||
|
self.assertEquals(sometag_images, staging_images)
|
||||||
|
|
||||||
|
# Move the tag.
|
||||||
|
self.putResponse(RepositoryTag,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='sometag'),
|
||||||
|
data=dict(image=staging_images[-1]['id']),
|
||||||
|
expected_code=201)
|
||||||
|
|
||||||
|
# Make sure the tag has moved.
|
||||||
|
json = self.getJsonResponse(RepositoryTagImages,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='sometag'))
|
||||||
|
|
||||||
|
sometag_new_images = json['images']
|
||||||
|
self.assertEquals(1, len(sometag_new_images))
|
||||||
|
self.assertEquals(staging_images[-1], sometag_new_images[0])
|
||||||
|
|
||||||
|
|
||||||
def test_deletesubtag(self):
|
def test_deletesubtag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
Reference in a new issue