Merge pull request #2416 from coreos-inc/new-tags-ui
Updated Tags and Tag History UI
This commit is contained in:
commit
9297373bd6
45 changed files with 1250 additions and 639 deletions
|
@ -82,6 +82,16 @@ def filter_tags_have_repository_event(query, event):
|
||||||
.where(RepositoryNotification.event == event)
|
.where(RepositoryNotification.event == event)
|
||||||
.order_by(RepositoryTag.lifetime_start_ts.desc()))
|
.order_by(RepositoryTag.lifetime_start_ts.desc()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_tag_manifests(tags):
|
||||||
|
""" Returns a map from tag ID to its associated manifest, if any. """
|
||||||
|
if not tags:
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
manifests = TagManifest.select().where(TagManifest.tag << [t.id for t in tags])
|
||||||
|
return {manifest.tag_id:manifest for manifest in manifests}
|
||||||
|
|
||||||
|
|
||||||
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
||||||
include_storage=False):
|
include_storage=False):
|
||||||
to_select = (RepositoryTag, Image)
|
to_select = (RepositoryTag, Image)
|
||||||
|
@ -296,6 +306,7 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None):
|
||||||
query = (RepositoryTag
|
query = (RepositoryTag
|
||||||
.select(RepositoryTag, Image)
|
.select(RepositoryTag, Image)
|
||||||
.join(Image)
|
.join(Image)
|
||||||
|
.switch(RepositoryTag)
|
||||||
.where(RepositoryTag.repository == repo_obj)
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
.where(RepositoryTag.hidden == False)
|
.where(RepositoryTag.hidden == False)
|
||||||
.order_by(RepositoryTag.lifetime_start_ts.desc(), RepositoryTag.name)
|
.order_by(RepositoryTag.lifetime_start_ts.desc(), RepositoryTag.name)
|
||||||
|
@ -306,35 +317,76 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None):
|
||||||
query = query.where(RepositoryTag.name == specific_tag)
|
query = query.where(RepositoryTag.name == specific_tag)
|
||||||
|
|
||||||
tags = list(query)
|
tags = list(query)
|
||||||
return tags[0:size], len(tags) > size
|
if not tags:
|
||||||
|
return [], {}, False
|
||||||
|
|
||||||
|
manifest_map = get_tag_manifests(tags)
|
||||||
|
return tags[0:size], manifest_map, len(tags) > size
|
||||||
|
|
||||||
|
|
||||||
def revert_tag(repo_obj, tag_name, docker_image_id):
|
def restore_tag_to_manifest(repo_obj, tag_name, manifest_digest):
|
||||||
""" Reverts a tag to a specific image ID. """
|
""" Restores a tag to a specific manifest digest. """
|
||||||
# Verify that the image ID already existed under this repository under the
|
with db_transaction():
|
||||||
# tag.
|
# Verify that the manifest digest already existed under this repository under the
|
||||||
try:
|
# tag.
|
||||||
(RepositoryTag
|
try:
|
||||||
.select()
|
manifest = (TagManifest
|
||||||
.join(Image)
|
.select(TagManifest, RepositoryTag, Image)
|
||||||
.where(RepositoryTag.repository == repo_obj)
|
.join(RepositoryTag)
|
||||||
.where(RepositoryTag.name == tag_name)
|
.join(Image)
|
||||||
.where(Image.docker_image_id == docker_image_id)
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
.get())
|
.where(RepositoryTag.name == tag_name)
|
||||||
except RepositoryTag.DoesNotExist:
|
.where(TagManifest.digest == manifest_digest)
|
||||||
raise DataModelException('Cannot revert to unknown or invalid image')
|
.get())
|
||||||
|
except TagManifest.DoesNotExist:
|
||||||
|
raise DataModelException('Cannot restore to unknown or invalid digest')
|
||||||
|
|
||||||
return create_or_update_tag(repo_obj.namespace_user.username, repo_obj.name, tag_name,
|
# Lookup the existing image, if any.
|
||||||
docker_image_id, reversion=True)
|
try:
|
||||||
|
existing_image = get_repo_tag_image(repo_obj, tag_name)
|
||||||
|
except DataModelException:
|
||||||
|
existing_image = None
|
||||||
|
|
||||||
|
docker_image_id = manifest.tag.image.docker_image_id
|
||||||
|
store_tag_manifest(repo_obj.namespace_user.username, repo_obj.name, tag_name, docker_image_id,
|
||||||
|
manifest_digest, manifest.json_data, reversion=True)
|
||||||
|
return existing_image
|
||||||
|
|
||||||
|
|
||||||
|
def restore_tag_to_image(repo_obj, tag_name, docker_image_id):
|
||||||
|
""" Restores a tag to a specific image ID. """
|
||||||
|
with db_transaction():
|
||||||
|
# Verify that the image ID already existed under this repository under the
|
||||||
|
# tag.
|
||||||
|
try:
|
||||||
|
(RepositoryTag
|
||||||
|
.select()
|
||||||
|
.join(Image)
|
||||||
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
|
.where(RepositoryTag.name == tag_name)
|
||||||
|
.where(Image.docker_image_id == docker_image_id)
|
||||||
|
.get())
|
||||||
|
except RepositoryTag.DoesNotExist:
|
||||||
|
raise DataModelException('Cannot restore to unknown or invalid image')
|
||||||
|
|
||||||
|
# Lookup the existing image, if any.
|
||||||
|
try:
|
||||||
|
existing_image = get_repo_tag_image(repo_obj, tag_name)
|
||||||
|
except DataModelException:
|
||||||
|
existing_image = None
|
||||||
|
|
||||||
|
create_or_update_tag(repo_obj.namespace_user.username, repo_obj.name, tag_name,
|
||||||
|
docker_image_id, reversion=True)
|
||||||
|
return existing_image
|
||||||
|
|
||||||
|
|
||||||
def store_tag_manifest(namespace, repo_name, tag_name, docker_image_id, manifest_digest,
|
def store_tag_manifest(namespace, repo_name, tag_name, docker_image_id, manifest_digest,
|
||||||
manifest_data):
|
manifest_data, reversion=False):
|
||||||
""" Stores a tag manifest for a specific tag name in the database. Returns the TagManifest
|
""" Stores a tag manifest for a specific tag name in the database. Returns the TagManifest
|
||||||
object, as well as a boolean indicating whether the TagManifest was created.
|
object, as well as a boolean indicating whether the TagManifest was created.
|
||||||
"""
|
"""
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
tag = create_or_update_tag(namespace, repo_name, tag_name, docker_image_id)
|
tag = create_or_update_tag(namespace, repo_name, tag_name, docker_image_id, reversion=reversion)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manifest = TagManifest.get(digest=manifest_digest)
|
manifest = TagManifest.get(digest=manifest_digest)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from endpoints.api import (ApiResource, resource, method_metadata, nickname, tru
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PARAM_REGEX = re.compile(r'<([\w]+:)?([\w]+)>')
|
PARAM_REGEX = re.compile(r'<([^:>]+:)*([\w]+)>')
|
||||||
|
|
||||||
|
|
||||||
TYPE_CONVERTER = {
|
TYPE_CONVERTER = {
|
||||||
|
|
|
@ -49,9 +49,9 @@ class RepositoryManifestLabels(RepositoryParamResource):
|
||||||
'description': 'The value for the label',
|
'description': 'The value for the label',
|
||||||
},
|
},
|
||||||
'media_type': {
|
'media_type': {
|
||||||
'type': ['string'],
|
'type': ['string', 'null'],
|
||||||
'description': 'The media type for this label',
|
'description': 'The media type for this label',
|
||||||
'enum': ALLOWED_LABEL_MEDIA_TYPES,
|
'enum': ALLOWED_LABEL_MEDIA_TYPES + [None],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -259,7 +259,7 @@ class Repository(RepositoryParamResource):
|
||||||
"""Fetch the specified repository."""
|
"""Fetch the specified repository."""
|
||||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||||
|
|
||||||
def tag_view(tag):
|
def tag_view(tag, manifest):
|
||||||
tag_info = {
|
tag_info = {
|
||||||
'name': tag.name,
|
'name': tag.name,
|
||||||
'image_id': tag.image.docker_image_id,
|
'image_id': tag.image.docker_image_id,
|
||||||
|
@ -270,13 +270,18 @@ class Repository(RepositoryParamResource):
|
||||||
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
||||||
tag_info['last_modified'] = last_modified
|
tag_info['last_modified'] = last_modified
|
||||||
|
|
||||||
|
if manifest is not None:
|
||||||
|
tag_info['manifest_digest'] = manifest.digest
|
||||||
|
|
||||||
return tag_info
|
return tag_info
|
||||||
|
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace, repository)
|
||||||
stats = None
|
stats = None
|
||||||
if repo:
|
if repo:
|
||||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
||||||
tag_dict = {tag.name: tag_view(tag) for tag in tags}
|
manifests = model.tag.get_tag_manifests(tags)
|
||||||
|
|
||||||
|
tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags}
|
||||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||||
|
|
||||||
|
|
|
@ -41,19 +41,22 @@ class ListRepositoryTags(RepositoryParamResource):
|
||||||
if tag.lifetime_end_ts > 0:
|
if tag.lifetime_end_ts > 0:
|
||||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
tag_info['end_ts'] = tag.lifetime_end_ts
|
||||||
|
|
||||||
|
if tag.id in manifest_map:
|
||||||
|
tag_info['manifest_digest'] = manifest_map[tag.id].digest
|
||||||
|
|
||||||
return tag_info
|
return tag_info
|
||||||
|
|
||||||
specific_tag = parsed_args.get('specificTag') or None
|
specific_tag = parsed_args.get('specificTag') or None
|
||||||
|
|
||||||
page = max(1, parsed_args.get('page', 1))
|
page = max(1, parsed_args.get('page', 1))
|
||||||
limit = min(100, max(1, parsed_args.get('limit', 50)))
|
limit = min(100, max(1, parsed_args.get('limit', 50)))
|
||||||
tags, has_additional = model.tag.list_repository_tag_history(repo, page=page, size=limit,
|
tags, manifest_map, more = model.tag.list_repository_tag_history(repo, page=page, size=limit,
|
||||||
specific_tag=specific_tag)
|
specific_tag=specific_tag)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'tags': [tag_view(tag) for tag in tags],
|
'tags': [tag_view(tag) for tag in tags],
|
||||||
'page': page,
|
'page': page,
|
||||||
'has_additional': has_additional,
|
'has_additional': more,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,15 +180,15 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/revert')
|
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/restore')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('tag', 'The name of the tag')
|
@path_param('tag', 'The name of the tag')
|
||||||
class RevertTag(RepositoryParamResource):
|
class RestoreTag(RepositoryParamResource):
|
||||||
""" Resource for reverting a repository tag back to a previous image. """
|
""" Resource for restoring a repository tag back to a previous image. """
|
||||||
schemas = {
|
schemas = {
|
||||||
'RevertTag': {
|
'RestoreTag': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'Reverts a tag to a specific image',
|
'description': 'Restores a tag to a specific image',
|
||||||
'required': [
|
'required': [
|
||||||
'image',
|
'image',
|
||||||
],
|
],
|
||||||
|
@ -194,32 +197,45 @@ class RevertTag(RepositoryParamResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'Image identifier to which the tag should point',
|
'description': 'Image identifier to which the tag should point',
|
||||||
},
|
},
|
||||||
|
'manifest_digest': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'If specified, the manifest digest that should be used',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@nickname('revertTag')
|
@nickname('restoreTag')
|
||||||
@validate_json_request('RevertTag')
|
@validate_json_request('RestoreTag')
|
||||||
def post(self, namespace, repository, tag):
|
def post(self, namespace, repository, tag):
|
||||||
""" Reverts a repository tag back to a previous image in the repository. """
|
""" Restores a repository tag back to a previous image in the repository. """
|
||||||
try:
|
repo = model.repository.get_repository(namespace, repository)
|
||||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
|
||||||
except model.DataModelException:
|
|
||||||
raise NotFound()
|
|
||||||
|
|
||||||
# Revert the tag back to the previous image.
|
# Restore the tag back to the previous image.
|
||||||
image_id = request.get_json()['image']
|
image_id = request.get_json()['image']
|
||||||
model.tag.revert_tag(tag_image.repository, tag, image_id)
|
manifest_digest = request.get_json().get('manifest_digest', None)
|
||||||
|
if manifest_digest is not None:
|
||||||
|
existing_image = model.tag.restore_tag_to_manifest(repo, tag, manifest_digest)
|
||||||
|
else:
|
||||||
|
existing_image = model.tag.restore_tag_to_image(repo, tag, image_id)
|
||||||
|
|
||||||
# Log the reversion.
|
# Log the reversion/restoration.
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
|
log_data = {
|
||||||
|
'username': username,
|
||||||
|
'repo': repository,
|
||||||
|
'tag': tag,
|
||||||
|
'image': image_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_image is not None:
|
||||||
|
log_data['original_image'] = existing_image.docker_image_id
|
||||||
|
|
||||||
log_action('revert_tag', namespace,
|
log_action('revert_tag', namespace,
|
||||||
{'username': username, 'repo': repository, 'tag': tag,
|
log_data, repo=model.repository.get_repository(namespace, repository))
|
||||||
'image': image_id, 'original_image': tag_image.docker_image_id},
|
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
'original_image_id': tag_image.docker_image_id
|
'original_image_id': existing_image.docker_image_id if existing_image else None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ EXTERNAL_JS = [
|
||||||
'cdn.ravenjs.com/3.1.0/angular/raven.min.js',
|
'cdn.ravenjs.com/3.1.0/angular/raven.min.js',
|
||||||
'cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js',
|
'cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js',
|
||||||
'cdnjs.cloudflare.com/ajax/libs/angular-recaptcha/3.2.1/angular-recaptcha.min.js',
|
'cdnjs.cloudflare.com/ajax/libs/angular-recaptcha/3.2.1/angular-recaptcha.min.js',
|
||||||
|
'cdnjs.cloudflare.com/ajax/libs/ng-tags-input/3.1.1/ng-tags-input.min.js',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_CSS = [
|
EXTERNAL_CSS = [
|
||||||
|
@ -28,6 +29,7 @@ EXTERNAL_CSS = [
|
||||||
's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css',
|
's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css',
|
||||||
'cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/css/bootstrap-datetimepicker.min.css',
|
'cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/css/bootstrap-datetimepicker.min.css',
|
||||||
'cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.css',
|
'cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.css',
|
||||||
|
'cdnjs.cloudflare.com/ajax/libs/ng-tags-input/3.1.1/ng-tags-input.min.css',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_FONTS = [
|
EXTERNAL_FONTS = [
|
||||||
|
|
|
@ -510,6 +510,7 @@ def populate_database(minimal=False, with_storage=False):
|
||||||
first_label = model.label.create_manifest_label(tag_manifest, 'foo', 'bar', 'manifest')
|
first_label = model.label.create_manifest_label(tag_manifest, 'foo', 'bar', 'manifest')
|
||||||
model.label.create_manifest_label(tag_manifest, 'foo', 'baz', 'api')
|
model.label.create_manifest_label(tag_manifest, 'foo', 'baz', 'api')
|
||||||
model.label.create_manifest_label(tag_manifest, 'anotherlabel', '1234', 'internal')
|
model.label.create_manifest_label(tag_manifest, 'anotherlabel', '1234', 'internal')
|
||||||
|
model.label.create_manifest_label(tag_manifest, 'jsonlabel', '{"hey": "there"}', 'internal')
|
||||||
|
|
||||||
label_metadata = {
|
label_metadata = {
|
||||||
'key': 'foo',
|
'key': 'foo',
|
||||||
|
|
|
@ -110,15 +110,29 @@
|
||||||
.repo-panel-tags-element .security-scan-col .has-vulns.Defcon1 .highest-vuln {
|
.repo-panel-tags-element .security-scan-col .has-vulns.Defcon1 .highest-vuln {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.repo-panel-tags-element .security-scan-col .has-vulns {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .security-scan-col .has-vulns .donut-chart {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .security-scan-col .has-vulns a {
|
||||||
|
color: ablack;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .other-vulns {
|
.repo-panel-tags-element .other-vulns {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arepo-panel-tags-element .tag-span {
|
.repo-panel-tags-element tr.expanded-view td {
|
||||||
overflow: hidden;
|
border-bottom: 0px;
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
max-width: 250px;
|
|
||||||
display: inline-block;
|
.repo-panel-tags-element .labels-col {
|
||||||
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
|
|
5
static/css/directives/ui/donut-chart.css
Normal file
5
static/css/directives/ui/donut-chart.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.donut-chart-element svg {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
|
@ -1,4 +1,29 @@
|
||||||
|
.image-link {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.image-link a {
|
.image-link a {
|
||||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-link .id-label {
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: black !important;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-link .id-label.cas {
|
||||||
|
background-color: #e8f1f6;
|
||||||
}
|
}
|
40
static/css/directives/ui/label-input.css
Normal file
40
static/css/directives/ui/label-input.css
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.label-input-element .tags {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-input-element .tags.focused {
|
||||||
|
border-color: #66afe9;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px rgba(102,175,233,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-input-element .tags .tag-item {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #eee;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-input-element tags-input .tags .input {
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-input-element .tags .tag-item.selected {
|
||||||
|
background: #31b0d5;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-input-element .tags .tag-item button {
|
||||||
|
background: transparent;
|
||||||
|
color: #000;
|
||||||
|
opacity: .4;
|
||||||
|
}
|
9
static/css/directives/ui/label-list.css
Normal file
9
static/css/directives/ui/label-list.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.label-list-element .label-view {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-list-element .empty-list {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
25
static/css/directives/ui/label-view.css
Normal file
25
static/css/directives/ui/label-view.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.label-view-element {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background-color: #eee;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-view-element .kind {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-right: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-view-element .value {
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
8
static/css/directives/ui/manifest-label-list.css
Normal file
8
static/css/directives/ui/manifest-label-list.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.manifest-label-list-element {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-label-list-element .none {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
|
@ -1,7 +1,73 @@
|
||||||
.repo-tag-history-element .history-list {
|
.repo-tag-history-element .history-list {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-left: 2px solid #eee;
|
}
|
||||||
margin-right: 150px;
|
|
||||||
|
.repo-tag-history-element .co-table thead {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table tbody td {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.revert-col {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.revert-col .tag-span {
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.icon-col {
|
||||||
|
border-bottom: 0px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.history-col {
|
||||||
|
width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.datetime-col {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.icon-col:after {
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 15px;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #ddd;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.icon-col .history-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .co-table td.icon-col .datetime-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 13px;
|
||||||
|
left: 9px;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-row {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry {
|
.repo-tag-history-element .history-entry {
|
||||||
|
@ -12,16 +78,21 @@
|
||||||
transition: all 350ms ease-in-out;
|
transition: all 350ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-text {
|
.repo-tag-history-element .history-entry .history-description,
|
||||||
transition: transform 350ms ease-in-out, opacity 350ms ease-in-out;
|
.repo-tag-history-element .history-entry .history-datetime,
|
||||||
|
.repo-tag-history-element .history-entry .history-revert {
|
||||||
|
transition: height 350ms ease-in-out, opacity 350ms ease-in-out;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 40px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .co-filter-box {
|
.repo-tag-history-element .co-filter-box {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-datetime-small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.repo-tag-history-element .history-list {
|
.repo-tag-history-element .history-list {
|
||||||
|
@ -36,13 +107,43 @@
|
||||||
float: none;
|
float: none;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .history-description {
|
||||||
|
overflow: visible;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-row {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-datetime-small {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #aaa;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.filtered-mismatch {
|
.repo-tag-history-element .history-entry.filtered-mismatch {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.filtered-mismatch .history-text {
|
.repo-tag-history-element .history-entry.filtered-mismatch .history-datetime {
|
||||||
|
height: 18px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch .history-revert {
|
||||||
|
height: 18px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.filtered-mismatch .history-description {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -64,19 +165,12 @@
|
||||||
content: "\f073";
|
content: "\f073";
|
||||||
font-family: FontAwesome;
|
font-family: FontAwesome;
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
top: 1px;
|
|
||||||
left: -9px;
|
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-icon {
|
.repo-tag-history-element .history-entry .history-icon {
|
||||||
position: absolute;
|
|
||||||
left: -17px;
|
|
||||||
top: -4px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -100,6 +194,11 @@
|
||||||
font-family: FontAwesome;
|
font-family: FontAwesome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.recreate .history-icon:before {
|
||||||
|
content: "\f10d";
|
||||||
|
font-family: core-icons;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.revert .history-icon:before {
|
.repo-tag-history-element .history-entry.revert .history-icon:before {
|
||||||
content: "\f0e2";
|
content: "\f0e2";
|
||||||
font-family: FontAwesome;
|
font-family: FontAwesome;
|
||||||
|
@ -122,6 +221,10 @@
|
||||||
background-color: #98df8a;
|
background-color: #98df8a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.current.recreate .history-icon {
|
||||||
|
background-color: #ba8adf;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.current.delete .history-icon {
|
.repo-tag-history-element .history-entry.current.delete .history-icon {
|
||||||
background-color: #ff9896;
|
background-color: #ff9896;
|
||||||
}
|
}
|
||||||
|
@ -135,20 +238,25 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
color: black;
|
color: black;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry .tag-span span {
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .tag-span.checked {
|
.repo-tag-history-element .history-entry .tag-span.checked {
|
||||||
background: #F6FCFF;
|
background: #F6FCFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .tag-span:before {
|
.repo-tag-history-element .history-entry .image-link {
|
||||||
content: "\f02b";
|
margin-left: 6px;
|
||||||
font-family: FontAwesome;
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-description {
|
.repo-tag-history-element .history-entry .history-description {
|
||||||
|
@ -156,6 +264,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-datetime {
|
.repo-tag-history-element .history-entry .history-datetime {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #ccc;
|
|
||||||
}
|
}
|
|
@ -20,4 +20,9 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-operations-dialog .label-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
1
static/directives/donut-chart.html
Normal file
1
static/directives/donut-chart.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<span class="donut-chart-element" ng-style="{'line-height': size + 'px'}"></span>
|
|
@ -1,2 +1,17 @@
|
||||||
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
|
<span>
|
||||||
class="image-link-element" bindonce>{{ imageId.substr(0, 12) }}</a>
|
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
|
||||||
|
class="image-link-element" bindonce>
|
||||||
|
<span class="id-label" ng-if="!hasSHA256(manifestDigest)"
|
||||||
|
data-title="The Docker V1 ID for this image. This ID is not content addressable nor is it stable across pulls."
|
||||||
|
data-container="body"
|
||||||
|
bs-tooltip>V1ID</span>
|
||||||
|
|
||||||
|
<span class="id-label cas" ng-if="hasSHA256(manifestDigest)"
|
||||||
|
data-title="The content-addressable SHA256 hash of this tag."
|
||||||
|
data-container="body"
|
||||||
|
bs-tooltip>SHA256</span>
|
||||||
|
|
||||||
|
<span ng-if="!hasSHA256(manifestDigest)">{{ imageId.substr(0, 12) }}</span>
|
||||||
|
<span ng-if="hasSHA256(manifestDigest)">{{ getShortDigest(manifestDigest) }}</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
12
static/directives/label-input.html
Normal file
12
static/directives/label-input.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="label-input-element">
|
||||||
|
<tags-input class="quay-labels"
|
||||||
|
ng-model="tags"
|
||||||
|
display-property="keyValue"
|
||||||
|
placeholder="git-sha=123456ab"
|
||||||
|
add-on-paste="true"
|
||||||
|
add-on-comma="false"
|
||||||
|
spellcheck="false"
|
||||||
|
replace-spaces-with-dashes="false"
|
||||||
|
allowed-tags-pattern="^[0-9A-Za-z/\-_.]+=.+$"
|
||||||
|
enable-editing-last-tag="true"></tags-input>
|
||||||
|
</div>
|
10
static/directives/label-list.html
Normal file
10
static/directives/label-list.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<div class="label-list-element">
|
||||||
|
<div class="label-view"
|
||||||
|
ng-repeat="label in labels"
|
||||||
|
expand="{{expand}}"
|
||||||
|
label="label">
|
||||||
|
</div>
|
||||||
|
<div class="empty-list" ng-if="!labels.length">
|
||||||
|
No labels found
|
||||||
|
</div>
|
||||||
|
</div>
|
8
static/directives/label-view.html
Normal file
8
static/directives/label-view.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<a class="label-view-element" ng-click="viewLabelValue()">
|
||||||
|
<span class="kind">{{ getKind(label) }}</span>
|
||||||
|
<span class="label-value">
|
||||||
|
<span class="key">{{ label.key }}</span>
|
||||||
|
<span class="equals">=</span>
|
||||||
|
<span class="value">{{ label.value }}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
11
static/directives/manifest-label-list.html
Normal file
11
static/directives/manifest-label-list.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="manifest-label-list-element">
|
||||||
|
<div class="cor-loader-inline" ng-if="repository && manifestDigest && !labels && !loadError"></div>
|
||||||
|
<div class="none" ng-if="repository && !manifestDigest && !loadError">
|
||||||
|
This tag does not have an associated manifest
|
||||||
|
</div>
|
||||||
|
<div class="none" ng-if="repository && manifestDigest && loadError">
|
||||||
|
Could not load labels for this manifest
|
||||||
|
</div>
|
||||||
|
<div class="label-list" labels="labels"
|
||||||
|
ng-if="repository && manifestDigest && labels && !loadError"></div>
|
||||||
|
</div>
|
|
@ -11,41 +11,116 @@
|
||||||
<div class="empty-secondary-msg">There has not been any recent tag activity on this repository.</div>
|
<div class="empty-secondary-msg">There has not been any recent tag activity on this repository.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-entry" ng-repeat="entry in historyEntries"
|
<table class="co-table" ng-if="historyEntries.length">
|
||||||
ng-class="getEntryClasses(entry, filter)">
|
<thead class="hidden-xs">
|
||||||
<div class="history-date-break" ng-if="entry.date_break">
|
<td class="icon-col"></td>
|
||||||
{{ entry.date | amDateFormat:'dddd, MMMM Do YYYY' }}
|
<td class="history-col">Tag Change</td>
|
||||||
</div>
|
<td class="datetime-col hidden-sm hidden-xs">Date/Time</td>
|
||||||
<div ng-if="!entry.date_break">
|
<td class="revert-col hidden-sm hidden-xs"><span ng-if="repository.can_write">Restore</span></td>
|
||||||
<div class="history-icon-container"><div class="history-icon"></div></div>
|
</thead>
|
||||||
<div class="history-text">
|
<tbody>
|
||||||
<div class="history-description">
|
<tr style="height: 20px;"><td colspan="4"></td></tr>
|
||||||
<span class="tag-span"
|
<tr ng-repeat="entry in historyEntries" class="history-entry" ng-class="getEntryClasses(entry, filter)">
|
||||||
ng-click="showHistory(true, entry.tag_name)">{{ entry.tag_name }}</span>
|
<td ng-if="entry.date_break" class="icon-col">
|
||||||
<span ng-switch on="entry.action">
|
<i class="fa fa-calendar datetime-icon"></i>
|
||||||
<span ng-switch-when="create">
|
</td>
|
||||||
was created pointing to image <span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
|
||||||
|
<td class="history-row" ng-if="entry.date_break" colspan="2">
|
||||||
|
{{ entry.date | amDateFormat:'dddd, MMMM Do YYYY' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td ng-if="!entry.date_break" class="icon-col">
|
||||||
|
<div class="history-icon" data-title="{{ isCurrent(entry) ? 'Current view of this tag' : 'The value of this tag has changed since this action' }}" data-container="body" bs-tooltip></div>
|
||||||
|
</td>
|
||||||
|
<td ng-if="!entry.date_break" class="history-col">
|
||||||
|
<div class="history-description">
|
||||||
|
<span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span>
|
||||||
|
<span ng-switch on="entry.action">
|
||||||
|
<span ng-switch-when="recreate">
|
||||||
|
was recreated pointing to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="create">
|
||||||
|
was created pointing to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="delete">
|
||||||
|
was deleted
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="move">
|
||||||
|
was moved to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
from
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.old_docker_image_id"
|
||||||
|
manifest-digest="entry.old_manifest_digest"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="revert">
|
||||||
|
was reverted to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
from
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.old_docker_image_id"
|
||||||
|
manifest-digest="entry.old_manifest_digest"></span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-switch-when="delete">
|
</div>
|
||||||
was deleted
|
<div class="history-datetime-small">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
||||||
|
</td>
|
||||||
|
<td ng-if="!entry.date_break" class="datetime-col hidden-sm hidden-xs">
|
||||||
|
<div class="history-datetime">{{ entry.time | amDateFormat:'MMM Do YYYY, h:mm:ss a' }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="revert-col hidden-xs hidden-sm">
|
||||||
|
<div ng-if="!entry.date_break && repository.can_write" class="history-revert">
|
||||||
|
<span ng-switch on="entry.action">
|
||||||
|
<a ng-switch-when="create" ng-click="askRestoreTag(entry, true)" ng-if="!isCurrent(entry)">
|
||||||
|
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
</a>
|
||||||
|
<a ng-switch-when="recreate" ng-click="askRestoreTag(entry, true)" ng-if="!isCurrent(entry)">
|
||||||
|
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
</a>
|
||||||
|
<a ng-switch-when="delete" ng-click="askRestoreTag(entry, true)">
|
||||||
|
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.docker_image_id"
|
||||||
|
manifest-digest="entry.manifest_digest"></span>
|
||||||
|
</a>
|
||||||
|
<a ng-switch-when="move" ng-click="askRestoreTag(entry, false)" ng-if="!isCurrent(entry)">
|
||||||
|
Restore <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.old_docker_image_id"
|
||||||
|
manifest-digest="entry.old_manifest_digest"></span>
|
||||||
|
</a>
|
||||||
|
<a ng-switch-when="revert" ng-click="askRestoreTag(entry, false)" ng-if="!isCurrent(entry)">
|
||||||
|
Restore <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
|
||||||
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="entry.old_docker_image_id"
|
||||||
|
manifest-digest="entry.old_manifest_digest"></span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span ng-switch-when="move">
|
</div>
|
||||||
was moved to image
|
</td>
|
||||||
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
</tr>
|
||||||
from image
|
</tbody>
|
||||||
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
</table>
|
||||||
</span>
|
|
||||||
<span ng-switch-when="revert">
|
|
||||||
was reverted to image
|
|
||||||
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
|
||||||
from image
|
|
||||||
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tag-operations-dialog" repository="repository"
|
||||||
|
image-loader="imageLoader"
|
||||||
|
action-handler="tagActionHandler"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
<div class="repo-panel-changes-element">
|
|
||||||
<div class="cor-loader" ng-show="loading"></div>
|
|
||||||
<div ng-show="!loading">
|
|
||||||
<h3 class="tab-header">
|
|
||||||
Visualize Tags:
|
|
||||||
<span class="multiselect-dropdown" items="tagNames" selected-items="selectedTagsSlice"
|
|
||||||
item-name="tag" item-checked="updateState()">
|
|
||||||
<span class="tag-span">{{ item }}</span>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- No Tags Selected -->
|
|
||||||
<div class="empty" ng-if="!selectedTagsSlice.length">
|
|
||||||
<div class="empty-primary-msg">No tags selected to view</div>
|
|
||||||
<div class="empty-secondary-msg">
|
|
||||||
Please select one or more tags above.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags Selected -->
|
|
||||||
<div ng-show="selectedTagsSlice.length > 0">
|
|
||||||
<!-- Tree View container -->
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<!-- Image history tree -->
|
|
||||||
<div id="image-history-container" onresize="tree.notifyResized()"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Side Panel -->
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="side-panel-title" ng-if="currentTag">
|
|
||||||
<i class="fa fa-tag"></i>{{ currentTag }}
|
|
||||||
</div>
|
|
||||||
<div class="side-panel-title" ng-if="currentImage">
|
|
||||||
<i class="fa fa-archive"></i>{{ currentImage.substr(0, 12) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="side-panel">
|
|
||||||
<!-- Tag Info -->
|
|
||||||
<div class="tag-info-sidebar"
|
|
||||||
tracker="tracker"
|
|
||||||
tag="currentTag"
|
|
||||||
image-selected="setImage(image)"
|
|
||||||
delete-tag-requested="tagActionHandler.askDeleteTag(tag)"
|
|
||||||
ng-if="currentTag">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image Info -->
|
|
||||||
<div class="image-info-sidebar"
|
|
||||||
tracker="tracker"
|
|
||||||
image="currentImage"
|
|
||||||
image-loader="imageLoader"
|
|
||||||
tag-selected="setTag(tag)"
|
|
||||||
add-tag-requested="tagActionHandler.askAddTag(image)"
|
|
||||||
ng-if="currentImage">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tag-operations-dialog" repository="repository" image-loader="imageLoader"
|
|
||||||
action-handler="tagActionHandler" tag-changed="handleTagChanged(data)"></div>
|
|
|
@ -1,129 +1,132 @@
|
||||||
<div class="repo-panel-tags-element">
|
<div class="repo-panel-tags-element">
|
||||||
<div class="tab-header-controls">
|
<div class="tab-header-controls">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button class="btn" ng-class="!showingHistory ? 'btn-primary active' : 'btn-default'" ng-click="showHistory(false)">
|
<button class="btn" ng-class="!expandedView ? 'btn-primary active' : 'btn-default'"
|
||||||
<i class="fa fa-tags"></i>Current Tags
|
ng-click="setExpanded(false)">
|
||||||
|
Compact
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" ng-class="showingHistory ? 'btn-info active' : 'btn-default'" ng-click="showHistory(true)">
|
<button class="btn" ng-class="expandedView ? 'btn-info active' : 'btn-default'"
|
||||||
<i class="fa fa-history"></i>History
|
ng-click="setExpanded(true)">
|
||||||
|
Expanded
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="tab-header"><span class="hidden-xs">Repository </span>Tags</h3>
|
<h3 class="tab-header"><span class="hidden-xs">Repository </span>Tags</h3>
|
||||||
|
<div class="co-alert co-alert-danger" ng-if="hasDefcon1">
|
||||||
|
One or more of your tags has an <strong>extremely critical</strong> vulnerability which should be addressed immediately:
|
||||||
|
<a href="{{ vuln.Link }}" ng-repeat="(key, vuln) in defcon1" style="margin-left: 10px;" ng-safenewtab>
|
||||||
|
{{ vuln.Name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- History view -->
|
<div class="co-check-bar">
|
||||||
<div class="repo-tag-history" repository="repository" filter="options.historyFilter"
|
<span class="cor-checkable-menu" controller="checkedTags">
|
||||||
is-enabled="showingHistory" ng-show="showingHistory"></div>
|
<div class="cor-checkable-menu-item" item-filter="allTagFilter(item)">
|
||||||
|
<i class="fa fa-check-square-o"></i>All Tags
|
||||||
|
</div>
|
||||||
|
<div class="cor-checkable-menu-item" item-filter="noTagFilter(item)">
|
||||||
|
<i class="fa fa-square-o"></i>No Tags
|
||||||
|
</div>
|
||||||
|
<div class="cor-checkable-menu-item" item-filter="commitTagFilter(item)">
|
||||||
|
<i class="fa fa-git"></i>Commit SHAs
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Normal View -->
|
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
|
||||||
<div ng-show="!showingHistory">
|
ng-repeat="it in imageTrackEntries">
|
||||||
<div class="co-alert co-alert-danger" ng-if="hasDefcon1">
|
<i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }}
|
||||||
One or more of your tags has an <strong>extremely critical</strong> vulnerability which should be addressed immediately:
|
</div>
|
||||||
<a href="{{ vuln.Link }}" ng-repeat="(key, vuln) in defcon1" style="margin-left: 10px;" ng-safenewtab>
|
</span>
|
||||||
{{ vuln.Name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="co-check-bar">
|
<span class="co-checked-actions" ng-if="checkedTags.checked.length">
|
||||||
<span class="cor-checkable-menu" controller="checkedTags">
|
<div class="dropdown">
|
||||||
<div class="cor-checkable-menu-item" item-filter="allTagFilter(item)">
|
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||||
<i class="fa fa-check-square-o"></i>All Tags
|
<i class="fa fa-cog"></i>
|
||||||
</div>
|
Actions
|
||||||
<div class="cor-checkable-menu-item" item-filter="noTagFilter(item)">
|
<span class="caret"></span>
|
||||||
<i class="fa fa-square-o"></i>No Tags
|
</button>
|
||||||
</div>
|
<ul class="dropdown-menu" role="menu">
|
||||||
<div class="cor-checkable-menu-item" item-filter="commitTagFilter(item)">
|
<li>
|
||||||
<i class="fa fa-git"></i>Commit SHAs
|
<a ng-click="showHistory(checkedTags.checked)">
|
||||||
</div>
|
<i class="fa fa-history"></i><span class="text">View Tags History</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li ng-if="repository.can_write">
|
||||||
|
<a ng-click="askDeleteMultipleTags(checkedTags.checked)">
|
||||||
|
<i class="fa fa-times"></i><span class="text">Delete Tags</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="cor-checkable-menu-item" item-filter="imageIDFilter(it.image_id, item)"
|
<span class="co-filter-box">
|
||||||
ng-repeat="it in imageTrackEntries">
|
<span class="page-controls" total-count="tags.length" current-page="options.page" page-size="tagsPerPage"></span>
|
||||||
<i class="fa fa-circle-o" ng-style="{'color': it.color}"></i> {{ it.image_id.substr(0, 12) }}
|
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Tags...">
|
||||||
</div>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<span class="co-checked-actions" ng-if="checkedTags.checked.length">
|
<div class="co-alert co-alert-info" ng-if="allTagsSelected && !fullPageSelected">
|
||||||
<a class="btn btn-default" ng-click="setTab('changes')">
|
All <strong>{{ tags.length }}</strong> visible tags are selected.
|
||||||
<i class="fa fa-code-fork"></i><span class="text">Visualize</span>
|
<a ng-click="clearSelectedTags()">Clear selection</a>.
|
||||||
</a>
|
</div>
|
||||||
<a class="btn btn-default"
|
|
||||||
ng-click="showHistory(true, getTagNames(checkedTags.checked))"
|
|
||||||
ng-if="repository.can_write">
|
|
||||||
<i class="fa fa-history"></i><span class="text">View History</span>
|
|
||||||
</a>
|
|
||||||
<button class="btn btn-primary"
|
|
||||||
ng-click="askDeleteMultipleTags(checkedTags.checked)"
|
|
||||||
ng-if="repository.can_write">
|
|
||||||
<i class="fa fa-times"></i><span class="text">Delete</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="co-filter-box">
|
<div class="co-alert co-alert-info" ng-if="fullPageSelected">
|
||||||
<span class="page-controls" total-count="tags.length" current-page="options.page" page-size="tagsPerPage"></span>
|
All <strong>{{ tagsPerPage }}</strong> tags on this page are selected.
|
||||||
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Tags...">
|
<a ng-click="selectAllTags()">Select all {{ tags.length }} tags currently visible</a>.
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="co-alert co-alert-info" ng-if="allTagsSelected && !fullPageSelected">
|
<div class="cor-loader" ng-show="!isEnabled"></div>
|
||||||
All <strong>{{ tags.length }}</strong> visible tags are selected.
|
|
||||||
<a ng-click="clearSelectedTags()">Clear selection</a>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="co-alert co-alert-info" ng-if="fullPageSelected">
|
<table class="co-table co-fixed-table" id="tagsTable" ng-if="isEnabled" style="margin-top: 20px;">
|
||||||
All <strong>{{ tagsPerPage }}</strong> tags on this page are selected.
|
<thead>
|
||||||
<a ng-click="selectAllTags()">Select all {{ tags.length }} tags currently visible</a>.
|
<td class="checkbox-col"></td>
|
||||||
</div>
|
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
|
||||||
|
<a ng-click="orderBy('name')">Tag</a>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs"
|
||||||
|
ng-class="tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)"
|
||||||
|
style="width: 140px;">
|
||||||
|
<a ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs"
|
||||||
|
ng-class="tablePredicateClass('security_scanned', options.predicate, options.reverse)"
|
||||||
|
style="width: 180px;"
|
||||||
|
quay-require="['SECURITY_SCANNER']">
|
||||||
|
Security Scan
|
||||||
|
</td>
|
||||||
|
<td class="hidden-sm hidden-xs"
|
||||||
|
ng-class="tablePredicateClass('size', options.predicate, options.reverse)"
|
||||||
|
style="width: 80px;">
|
||||||
|
<a ng-click="orderBy('size')" data-title="The compressed size of the tag's image" data-container="body" bs-tooltip>Size</a>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs hidden-sm"
|
||||||
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
|
style="width: 140px;">
|
||||||
|
<a ng-click="orderBy('image_id')">Image</a>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
||||||
|
ng-if="imageTracks.length <= maxTrackCount"></td>
|
||||||
|
<td class="hidden-xs hidden-sm" ng-if="imageTracks.length > maxTrackCount"
|
||||||
|
style="width: 20px; position: relative;">
|
||||||
|
<span class="image-track-dot" style="border-color: #ccc; top: 4px;"></span>
|
||||||
|
</td>
|
||||||
|
<td class="options-col"></td>
|
||||||
|
<td class="options-col"></td>
|
||||||
|
<td class="hidden-xs hidden-sm" style="width: 4px"></td>
|
||||||
|
</thead>
|
||||||
|
|
||||||
<div class="cor-loader" ng-show="!isEnabled"></div>
|
<tbody class="co-checkable-row"
|
||||||
|
ng-repeat="tag in tags | slice:(tagsPerPage * options.page):(tagsPerPage * (options.page + 1))"
|
||||||
<table class="co-table co-fixed-table" id="tagsTable" ng-if="isEnabled" style="margin-top: 20px;">
|
ng-class="checkedTags.isChecked(tag, checkedTags.checked) ? 'checked' : ''"
|
||||||
<thead>
|
bindonce>
|
||||||
<td class="checkbox-col"></td>
|
<tr ng-class="expandedView ? 'expanded-view': ''">
|
||||||
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
|
|
||||||
<a ng-click="orderBy('name')">Tag</a>
|
|
||||||
</td>
|
|
||||||
<td class="hidden-xs"
|
|
||||||
ng-class="tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)"
|
|
||||||
style="width: 140px;">
|
|
||||||
<a ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
|
||||||
</td>
|
|
||||||
<td class="hidden-xs"
|
|
||||||
ng-class="tablePredicateClass('security_scanned', options.predicate, options.reverse)"
|
|
||||||
style="width: 270px;"
|
|
||||||
quay-require="['SECURITY_SCANNER']">
|
|
||||||
Security Scan
|
|
||||||
</td>
|
|
||||||
<td class="hidden-sm hidden-xs"
|
|
||||||
ng-class="tablePredicateClass('size', options.predicate, options.reverse)"
|
|
||||||
style="width: 80px;">
|
|
||||||
<a ng-click="orderBy('size')" data-title="The compressed size of the tag's image" data-container="body" bs-tooltip>Size</a>
|
|
||||||
</td>
|
|
||||||
<td class="hidden-xs hidden-sm"
|
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
|
||||||
style="width: 120px;">
|
|
||||||
<a ng-click="orderBy('image_id')">Image</a>
|
|
||||||
</td>
|
|
||||||
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
|
||||||
ng-if="imageTracks.length <= maxTrackCount"></td>
|
|
||||||
<td class="hidden-xs hidden-sm" ng-if="imageTracks.length > maxTrackCount"
|
|
||||||
style="width: 20px; position: relative;">
|
|
||||||
<span class="image-track-dot" style="border-color: #ccc; top: 4px;"></span>
|
|
||||||
</td>
|
|
||||||
<td class="options-col"></td>
|
|
||||||
<td class="options-col"></td>
|
|
||||||
<td class="options-col"></td>
|
|
||||||
<td class="hidden-xs hidden-sm" style="width: 4px"></td>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tr class="co-checkable-row"
|
|
||||||
ng-repeat="tag in tags | slice:(tagsPerPage * options.page):(tagsPerPage * (options.page + 1))"
|
|
||||||
ng-class="checkedTags.isChecked(tag, checkedTags.checked) ? 'checked' : ''"
|
|
||||||
bindonce>
|
|
||||||
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
||||||
<td class="co-flowing-col"><span class="tag-span"><i class="fa fa-tag"></i><span bo-text="tag.name"></span></span></td>
|
<td class="co-flowing-col"><span class="tag-span"><span bo-text="tag.name"></span></span></td>
|
||||||
<td class="hidden-xs">
|
<td class="hidden-xs">
|
||||||
<span am-time-ago="tag.last_modified" bo-if="tag.last_modified"></span>
|
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
||||||
|
<span am-time-ago="tag.last_modified"></span>
|
||||||
|
</span>
|
||||||
<span bo-if="!tag.last_modified">Unknown</span>
|
<span bo-if="!tag.last_modified">Unknown</span>
|
||||||
</td>
|
</td>
|
||||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-xs">
|
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-xs">
|
||||||
|
@ -141,7 +144,7 @@
|
||||||
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
||||||
bs-tooltip>
|
bs-tooltip>
|
||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="fa fa-ellipsis-h"></i>
|
||||||
Queued for scan
|
Queued
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Scan Failed -->
|
<!-- Scan Failed -->
|
||||||
|
@ -149,7 +152,7 @@
|
||||||
data-title="The image for this tag could not be scanned for vulnerabilities"
|
data-title="The image for this tag could not be scanned for vulnerabilities"
|
||||||
bs-tooltip>
|
bs-tooltip>
|
||||||
<i class="fa fa-question-circle"></i>
|
<i class="fa fa-question-circle"></i>
|
||||||
Unable to scan image
|
Unable to scan
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- No Features -->
|
<!-- No Features -->
|
||||||
|
@ -178,29 +181,27 @@
|
||||||
<span ng-if="getTagVulnerabilities(tag).status == 'scanned' && getTagVulnerabilities(tag).hasFeatures && getTagVulnerabilities(tag).hasVulnerabilities"
|
<span ng-if="getTagVulnerabilities(tag).status == 'scanned' && getTagVulnerabilities(tag).hasFeatures && getTagVulnerabilities(tag).hasVulnerabilities"
|
||||||
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
|
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
|
||||||
class="has-vulns" bindonce>
|
class="has-vulns" bindonce>
|
||||||
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities"
|
|
||||||
data-title="The image for this tag has {{ getTagVulnerabilities(tag).highestVulnerability.Count }} {{ getTagVulnerabilities(tag).highestVulnerability.Priority }} level vulnerabilities"
|
|
||||||
bs-tooltip>
|
|
||||||
<span class="highest-vuln">
|
|
||||||
<span class="vulnerability-priority-view" priority="getTagVulnerabilities(tag).highestVulnerability.Priority">
|
|
||||||
{{ getTagVulnerabilities(tag).highestVulnerability.Count }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span ng-if="getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count > 0"
|
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities"
|
||||||
class="other-vulns">
|
data-title="This tag has {{ getTagVulnerabilities(tag).vulnerabilities.length }} vulnerabilities across {{ getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount }} packages"
|
||||||
+ {{ getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count }} others
|
bs-tooltip>
|
||||||
</span>
|
<!-- Donut -->
|
||||||
</a>
|
<span class="donut-chart" width="24" data="getTagVulnerabilities(tag).featuresInfo.severityBreakdown"></span>
|
||||||
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities" style="display: inline-block; margin-left: 6px;">
|
|
||||||
More Info
|
<!-- Messaging -->
|
||||||
|
<span ng-if="getTagVulnerabilities(tag).featuresInfo.fixableFeatureCount == 0">
|
||||||
|
{{ getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount }} vulnerable package<span ng-if="getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount != 1">s</span>
|
||||||
|
</span>
|
||||||
|
<span ng-if="getTagVulnerabilities(tag).featuresInfo.fixableFeatureCount > 0">
|
||||||
|
{{ getTagVulnerabilities(tag).featuresInfo.fixableFeatureCount }} fixable package<span ng-if="getTagVulnerabilities(tag).featuresInfo.fixableFeatureCount != 1">s</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
||||||
<td class="hidden-xs hidden-sm image-id-col">
|
<td class="hidden-xs hidden-sm image-id-col">
|
||||||
<span class="image-link" repository="repository" image-id="tag.image_id"></span>
|
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm image-track"
|
<td class="hidden-xs hidden-sm image-track"
|
||||||
ng-if="imageTracks.length > maxTrackCount" bindonce>
|
ng-if="imageTracks.length > maxTrackCount" bindonce>
|
||||||
|
@ -225,34 +226,16 @@
|
||||||
ng-click="fetchTagActionHandler.askFetchTag(tag)">
|
ng-click="fetchTagActionHandler.askFetchTag(tag)">
|
||||||
</i>
|
</i>
|
||||||
</td>
|
</td>
|
||||||
<td class="options-col">
|
|
||||||
<div class="dropdown" style="text-align: left;">
|
|
||||||
<i class="fa fa-history dropdown-toggle" data-toggle="dropdown" data-title="Tag History"
|
|
||||||
ng-click="loadTagHistory(tag)"
|
|
||||||
bs-tooltip></i>
|
|
||||||
<ul class="dropdown-menu pull-right">
|
|
||||||
<li ng-if="!tagHistory[tag.name]"><div class="cor-loader"></div></li>
|
|
||||||
<li class="tag-image-history-item" ng-repeat="entry in tagHistory[tag.name]" bindonce>
|
|
||||||
<a ng-click="askRevertTag(tag, entry.docker_image_id)">
|
|
||||||
<div class="image-id">
|
|
||||||
<i class="fa fa-circle-o"
|
|
||||||
bo-style="{'color': imageMap[entry.docker_image_id].color || '#eee'}"></i>
|
|
||||||
{{ entry.docker_image_id.substr(0, 12) }}
|
|
||||||
</div>
|
|
||||||
<div class="image-apply-time">
|
|
||||||
{{ entry.start_ts * 1000 | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="options-col">
|
<td class="options-col">
|
||||||
<span bo-if="repository.can_write">
|
<span bo-if="repository.can_write">
|
||||||
<span class="cor-options-menu">
|
<span class="cor-options-menu">
|
||||||
<span class="cor-option" option-click="askAddTag(tag)">
|
<span class="cor-option" option-click="askAddTag(tag)">
|
||||||
<i class="fa fa-plus"></i> Add New Tag
|
<i class="fa fa-plus"></i> Add New Tag
|
||||||
</span>
|
</span>
|
||||||
|
<span class="cor-option" option-click="showLabelEditor(tag)"
|
||||||
|
ng-if="tag.manifest_digest">
|
||||||
|
<i class="fa fa-tags"></i> Edit Labels
|
||||||
|
</span>
|
||||||
<span class="cor-option" option-click="askDeleteTag(tag.name)">
|
<span class="cor-option" option-click="askDeleteTag(tag.name)">
|
||||||
<i class="fa fa-times"></i> Delete Tag
|
<i class="fa fa-times"></i> Delete Tag
|
||||||
</span>
|
</span>
|
||||||
|
@ -260,23 +243,40 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="options-col hidden-xs hidden-sm"><!-- Whitespace col --></td>
|
<td class="options-col hidden-xs hidden-sm"><!-- Whitespace col --></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
<tr ng-if="expandedView">
|
||||||
|
<td class="checkbox-col"></td>
|
||||||
|
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0)}}">
|
||||||
|
<div class="manifest-label-list" repository="repository"
|
||||||
|
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||||
|
</td>
|
||||||
|
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
||||||
|
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
||||||
|
<span ng-repeat="entry in it.entries">
|
||||||
|
<span class="image-track-line"
|
||||||
|
bo-class="trackLineExpandedClass($parent.$parent.$parent.$index, entry)"
|
||||||
|
bo-style="{'borderColor': entry.color}"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td colspan="2"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class="empty" ng-if="allTags.length && !tags.length">
|
<div class="empty" ng-if="allTags.length && !tags.length">
|
||||||
<div class="empty-primary-msg">No matching tags found.</div>
|
<div class="empty-primary-msg">No matching tags found.</div>
|
||||||
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
|
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="empty" ng-if="!allTags.length">
|
<div class="empty" ng-if="!allTags.length">
|
||||||
<div class="empty-primary-msg">This repository is empty.</div>
|
<div class="empty-primary-msg">This repository is empty.</div>
|
||||||
<div class="empty-secondary-msg">Push a tag or initiate a build to populate this repository.</div>
|
<div class="empty-secondary-msg">Push a tag or initiate a build to populate this repository.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-operations-dialog" repository="repository"
|
<div class="tag-operations-dialog" repository="repository"
|
||||||
image-loader="imageLoader"
|
image-loader="imageLoader"
|
||||||
action-handler="tagActionHandler"></div>
|
action-handler="tagActionHandler"
|
||||||
|
labels-changed="handleLabelsChanged(manifest_digest)"></div>
|
||||||
|
|
||||||
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler"></div>
|
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler"></div>
|
||||||
|
|
|
@ -18,6 +18,27 @@
|
||||||
</div><!-- /.modal-dialog -->
|
</div><!-- /.modal-dialog -->
|
||||||
</div><!-- /.modal -->
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
<!-- Edit Labels Dialog -->
|
||||||
|
<div class="cor-confirm-dialog"
|
||||||
|
dialog-context="editLabelsInfo"
|
||||||
|
dialog-action="editLabels(info, callback)"
|
||||||
|
dialog-title="Edit Manifest Labels"
|
||||||
|
dialog-action-title="Edit Labels">
|
||||||
|
|
||||||
|
<div class="cor-loader" ng-if="editLabelsInfo.loading"></div>
|
||||||
|
<div ng-if="!editLabelsInfo.loading && editLabelsInfo.labels">
|
||||||
|
<div><strong>Read-only labels:</strong></div>
|
||||||
|
<div class="label-section">
|
||||||
|
<div class="label-list" labels="editLabelsInfo.readonly_labels"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div><strong>Mutable labels:</strong></div>
|
||||||
|
<div class="label-section">
|
||||||
|
<div class="label-input" labels="editLabelsInfo.mutable_labels"
|
||||||
|
updated-labels="editLabelsInfo.updated_labels"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add Tag Dialog -->
|
<!-- Add Tag Dialog -->
|
||||||
<div class="modal fade" id="createOrMoveTagModal">
|
<div class="modal fade" id="createOrMoveTagModal">
|
||||||
|
@ -106,19 +127,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recert Tag Confirm -->
|
<!-- Restore Tag Confirm -->
|
||||||
<div class="cor-confirm-dialog"
|
<div class="cor-confirm-dialog"
|
||||||
dialog-context="revertTagInfo"
|
dialog-context="restoreTagInfo"
|
||||||
dialog-action="revertTag(info.tag, info.image_id, callback)"
|
dialog-action="restoreTag(info.tag, info.image_id, info.manifest_digest, callback)"
|
||||||
dialog-title="Revert Tag"
|
dialog-title="Restore Tag"
|
||||||
dialog-action-title="Revert Tag">
|
dialog-action-title="Restore Tag">
|
||||||
|
|
||||||
<div class="co-alert co-alert-warning">
|
<div class="co-alert co-alert-warning">
|
||||||
This will change the image to which the tag points.
|
This will change the image to which the tag points.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Are you sure you want to revert tag
|
Are you sure you want to restore tag
|
||||||
<span class="label label-default tag">{{ revertTagInfo.tag.name }}</span> to image
|
<span class="label label-default tag">{{ restoreTagInfo.tag.name }}</span> to image
|
||||||
<span class="image-id">{{ revertTagInfo.image_id.substr(0, 12) }}?</span>
|
<span class="image-link" repository="repository"
|
||||||
|
image-id="restoreTagInfo.image_id"
|
||||||
|
manifest-digest="restoreTagInfo.manifest_digest"></span>?
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,218 +0,0 @@
|
||||||
/**
|
|
||||||
* An element which displays the changes visualization panel for a repository view.
|
|
||||||
*/
|
|
||||||
angular.module('quay').directive('repoPanelChanges', function () {
|
|
||||||
var RepositoryImageTracker = function(repository, imageLoader) {
|
|
||||||
this.repository = repository;
|
|
||||||
this.imageLoader = imageLoader;
|
|
||||||
|
|
||||||
// Build a map of image ID -> image.
|
|
||||||
var images = imageLoader.images;
|
|
||||||
var imageIDMap = {};
|
|
||||||
|
|
||||||
images.forEach(function(image) {
|
|
||||||
imageIDMap[image.id] = image;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imageMap_ = imageIDMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.imageLink = function(image) {
|
|
||||||
return '/repository/' + this.repository.namespace + '/' +
|
|
||||||
this.repository.name + '/image/' + image;
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.getImageForTag = function(tag) {
|
|
||||||
var tagData = this.lookupTag(tag);
|
|
||||||
if (!tagData) { return null; }
|
|
||||||
|
|
||||||
return this.imageMap_[tagData.image_id];
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.lookupTag = function(tag) {
|
|
||||||
return this.repository.tags[tag];
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.lookupImage = function(image) {
|
|
||||||
return this.imageMap_[image];
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.forAllTagImages = function(tag, callback) {
|
|
||||||
var tagData = this.lookupTag(tag);
|
|
||||||
if (!tagData) { return; }
|
|
||||||
|
|
||||||
var tagImage = this.imageMap_[tagData.image_id];
|
|
||||||
if (!tagImage) { return; }
|
|
||||||
|
|
||||||
// Callback the tag's image itself.
|
|
||||||
callback(tagImage);
|
|
||||||
|
|
||||||
// Callback any parent images.
|
|
||||||
if (!tagImage.ancestors) { return; }
|
|
||||||
|
|
||||||
var ancestors = tagImage.ancestors.split('/');
|
|
||||||
for (var i = 0; i < ancestors.length; ++i) {
|
|
||||||
var image = this.imageMap_[ancestors[i]];
|
|
||||||
if (image) {
|
|
||||||
callback(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.getTotalSize = function(tag) {
|
|
||||||
var size = 0;
|
|
||||||
this.forAllTagImages(tag, function(image) {
|
|
||||||
size += image.size;
|
|
||||||
});
|
|
||||||
return size;
|
|
||||||
};
|
|
||||||
|
|
||||||
RepositoryImageTracker.prototype.getImagesForTagBySize = function(tag) {
|
|
||||||
var images = [];
|
|
||||||
this.forAllTagImages(tag, function(image) {
|
|
||||||
images.push(image);
|
|
||||||
});
|
|
||||||
|
|
||||||
images.sort(function(a, b) {
|
|
||||||
return b.size - a.size;
|
|
||||||
});
|
|
||||||
|
|
||||||
return images;
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
var directiveDefinitionObject = {
|
|
||||||
priority: 0,
|
|
||||||
templateUrl: '/static/directives/repo-view/repo-panel-changes.html',
|
|
||||||
replace: false,
|
|
||||||
transclude: false,
|
|
||||||
restrict: 'C',
|
|
||||||
scope: {
|
|
||||||
'repository': '=repository',
|
|
||||||
'selectedTags': '=selectedTags',
|
|
||||||
|
|
||||||
'imagesResource': '=imagesResource',
|
|
||||||
'imageLoader': '=imageLoader',
|
|
||||||
|
|
||||||
'isEnabled': '=isEnabled'
|
|
||||||
},
|
|
||||||
controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) {
|
|
||||||
$scope.tagNames = [];
|
|
||||||
$scope.loading = true;
|
|
||||||
|
|
||||||
$scope.$watch('selectedTags', function(selectedTags) {
|
|
||||||
if (!selectedTags) { return; }
|
|
||||||
$scope.selectedTagsSlice = selectedTags.slice(0, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
var update = function() {
|
|
||||||
if (!$scope.repository || !$scope.isEnabled) { return; }
|
|
||||||
|
|
||||||
$scope.tagNames = Object.keys($scope.repository.tags);
|
|
||||||
$scope.currentImage = null;
|
|
||||||
$scope.currentTag = null;
|
|
||||||
|
|
||||||
$scope.loading = true;
|
|
||||||
$scope.imageLoader.loadImages($scope.selectedTagsSlice, function() {
|
|
||||||
$scope.loading = false;
|
|
||||||
updateImages();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var updateImages = function() {
|
|
||||||
if (!$scope.repository || !$scope.imageLoader || !$scope.isEnabled) { return; }
|
|
||||||
|
|
||||||
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.imageLoader);
|
|
||||||
if ($scope.selectedTagsSlice && $scope.selectedTagsSlice.length) {
|
|
||||||
refreshTree();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.$watch('selectedTagsSlice', update)
|
|
||||||
$scope.$watch('repository', update);
|
|
||||||
$scope.$watch('isEnabled', update);
|
|
||||||
|
|
||||||
$scope.updateState = function() {
|
|
||||||
update();
|
|
||||||
};
|
|
||||||
|
|
||||||
var refreshTree = function() {
|
|
||||||
if (!$scope.repository || !$scope.imageLoader || !$scope.isEnabled) { return; }
|
|
||||||
if ($scope.selectedTagsSlice.length < 1) { return; }
|
|
||||||
|
|
||||||
$('#image-history-container').empty();
|
|
||||||
|
|
||||||
var getTagsForImage = function(image) {
|
|
||||||
return $scope.imageLoader.getTagsForImage(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
var tree = new ImageHistoryTree(
|
|
||||||
$scope.repository.namespace,
|
|
||||||
$scope.repository.name,
|
|
||||||
$scope.imageLoader.images,
|
|
||||||
getTagsForImage,
|
|
||||||
UtilService.getFirstMarkdownLineAsText,
|
|
||||||
$scope.getTimeSince,
|
|
||||||
ImageMetadataService.getEscapedFormattedCommand,
|
|
||||||
function(tag) {
|
|
||||||
return $.inArray(tag, $scope.selectedTagsSlice) >= 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.tree = tree.draw('image-history-container');
|
|
||||||
if ($scope.tree) {
|
|
||||||
// Give enough time for the UI to be drawn before we resize the tree.
|
|
||||||
$timeout(function() {
|
|
||||||
$scope.tree.notifyResized();
|
|
||||||
$scope.setTag($scope.selectedTagsSlice[0]);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Listen for changes to the selected tag and image in the tree.
|
|
||||||
$($scope.tree).bind('tagChanged', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.setTag(e.tag); });
|
|
||||||
});
|
|
||||||
|
|
||||||
$($scope.tree).bind('imageChanged', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.setImage(e.image.id); });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.setImage = function(image_id) {
|
|
||||||
$scope.currentTag = null;
|
|
||||||
$scope.currentImage = image_id;
|
|
||||||
$scope.tree.setImage(image_id);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.setTag = function(tag) {
|
|
||||||
$scope.currentTag = tag;
|
|
||||||
$scope.currentImage = null;
|
|
||||||
$scope.tree.setTag(tag);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.parseDate = function(dateString) {
|
|
||||||
return Date.parse(dateString);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getTimeSince = function(createdTime) {
|
|
||||||
return moment($scope.parseDate(createdTime)).fromNow();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.handleTagChanged = function(data) {
|
|
||||||
data.removed.map(function(tag) {
|
|
||||||
$scope.currentImage = null;
|
|
||||||
$scope.currentTag = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
data.added.map(function(tag) {
|
|
||||||
$scope.selectedTags.push(tag);
|
|
||||||
$scope.currentTag = tag;
|
|
||||||
});
|
|
||||||
|
|
||||||
update();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return directiveDefinitionObject;
|
|
||||||
});
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'selectedTags': '=selectedTags',
|
'selectedTags': '=selectedTags',
|
||||||
|
'historyFilter': '=historyFilter',
|
||||||
'imagesResource': '=imagesResource',
|
'imagesResource': '=imagesResource',
|
||||||
'imageLoader': '=imageLoader',
|
'imageLoader': '=imageLoader',
|
||||||
|
|
||||||
|
@ -31,11 +32,12 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.iterationState = {};
|
$scope.iterationState = {};
|
||||||
$scope.tagHistory = {};
|
|
||||||
$scope.tagActionHandler = null;
|
$scope.tagActionHandler = null;
|
||||||
$scope.showingHistory = false;
|
|
||||||
$scope.tagsPerPage = 25;
|
$scope.tagsPerPage = 25;
|
||||||
|
|
||||||
|
$scope.expandedView = false;
|
||||||
|
$scope.labelCache = {};
|
||||||
|
|
||||||
$scope.imageVulnerabilities = {};
|
$scope.imageVulnerabilities = {};
|
||||||
$scope.defcon1 = {};
|
$scope.defcon1 = {};
|
||||||
$scope.hasDefcon1 = false;
|
$scope.hasDefcon1 = false;
|
||||||
|
@ -213,7 +215,8 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
highest = {
|
highest = {
|
||||||
'Priority': vuln.Severity,
|
'Priority': vuln.Severity,
|
||||||
'Count': 1,
|
'Count': 1,
|
||||||
'index': VulnerabilityService.LEVELS[vuln.Severity].index
|
'index': VulnerabilityService.LEVELS[vuln.Severity].index,
|
||||||
|
'Color': VulnerabilityService.LEVELS[vuln.Severity].color
|
||||||
}
|
}
|
||||||
} else if (VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
|
} else if (VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
|
||||||
highest['Count']++;
|
highest['Count']++;
|
||||||
|
@ -226,6 +229,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
imageData.hasVulnerabilities = !!vulnerabilities.length;
|
imageData.hasVulnerabilities = !!vulnerabilities.length;
|
||||||
imageData.vulnerabilities = vulnerabilities;
|
imageData.vulnerabilities = vulnerabilities;
|
||||||
imageData.highestVulnerability = highest;
|
imageData.highestVulnerability = highest;
|
||||||
|
imageData.featuresInfo = VulnerabilityService.buildFeaturesInfo(null, resp);
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
imageData.loading = false;
|
imageData.loading = false;
|
||||||
|
@ -261,13 +265,24 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.checkedTags.setChecked($scope.tags);
|
$scope.checkedTags.setChecked($scope.tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.showHistory = function(value, opt_tagname) {
|
$scope.trackLineExpandedClass = function(index, track_info) {
|
||||||
$scope.options.historyFilter = opt_tagname ? opt_tagname : '';
|
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
|
||||||
$scope.showingHistory = value;
|
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
|
||||||
};
|
index += $scope.options.page * $scope.tagsPerPage;
|
||||||
|
|
||||||
$scope.toggleHistory = function() {
|
if (index < startIndex) {
|
||||||
$scope.showHistory(!$scope.showingHistory);
|
return 'before';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > endIndex) {
|
||||||
|
return 'after';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= startIndex && index < endIndex) {
|
||||||
|
return 'middle';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.trackLineClass = function(index, track_info) {
|
$scope.trackLineClass = function(index, track_info) {
|
||||||
|
@ -321,6 +336,11 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.tagActionHandler.askAddTag(tag.image_id);
|
$scope.tagActionHandler.askAddTag(tag.image_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showLabelEditor = function(tag) {
|
||||||
|
if (!tag.manifest_digest) { return; }
|
||||||
|
$scope.tagActionHandler.showLabelEditor(tag.manifest_digest);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.orderBy = function(predicate) {
|
$scope.orderBy = function(predicate) {
|
||||||
if (predicate == $scope.options.predicate) {
|
if (predicate == $scope.options.predicate) {
|
||||||
$scope.options.reverse = !$scope.options.reverse;
|
$scope.options.reverse = !$scope.options.reverse;
|
||||||
|
@ -358,6 +378,19 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showHistory = function(checked) {
|
||||||
|
if (!checked.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.historyFilter = $scope.getTagNames(checked);
|
||||||
|
$scope.setTab('history');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setExpanded = function(expanded) {
|
||||||
|
$scope.expandedView = expanded;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getTagNames = function(checked) {
|
$scope.getTagNames = function(checked) {
|
||||||
var names = checked.map(function(tag) {
|
var names = checked.map(function(tag) {
|
||||||
return tag.name;
|
return tag.name;
|
||||||
|
@ -366,24 +399,8 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
return names.join(',');
|
return names.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadTagHistory = function(tag) {
|
$scope.handleLabelsChanged = function(manifest_digest) {
|
||||||
delete $scope.tagHistory[tag.name];
|
delete $scope.labelCache[manifest_digest];
|
||||||
|
|
||||||
var params = {
|
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
|
||||||
'specificTag': tag.name,
|
|
||||||
'limit': 5
|
|
||||||
};
|
|
||||||
|
|
||||||
ApiService.listRepoTags(null, params).then(function(resp) {
|
|
||||||
$scope.tagHistory[tag.name] = resp.tags;
|
|
||||||
}, ApiService.errorDisplay('Could not load tag history'));
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.askRevertTag = function(tag, image_id) {
|
|
||||||
if ($scope.repository.can_write) {
|
|
||||||
$scope.tagActionHandler.askRevertTag(tag, image_id);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
71
static/js/directives/ui/donut-chart.js
Normal file
71
static/js/directives/ui/donut-chart.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* An element which displays a donut chart of data.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('donutChart', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/donut-chart.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'width': '@width',
|
||||||
|
'data': '=data',
|
||||||
|
},
|
||||||
|
controller: function($scope, $element) {
|
||||||
|
$scope.created = false;
|
||||||
|
|
||||||
|
// Based on: http://bl.ocks.org/erichoco/6694616
|
||||||
|
var chart = d3.select($element.find('.donut-chart-element')[0]);
|
||||||
|
|
||||||
|
var renderChart = function() {
|
||||||
|
if (!$scope.data || !$scope.data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $chart = $element.find('.donut-chart-element');
|
||||||
|
$chart.empty();
|
||||||
|
|
||||||
|
var width = $scope.width * 1;
|
||||||
|
var chart_m = width / 2 * 0.14;
|
||||||
|
var chart_r = width / 2 * 0.85;
|
||||||
|
|
||||||
|
var topG = chart.append('svg:svg')
|
||||||
|
.attr('width', (chart_r + chart_m) * 2)
|
||||||
|
.attr('height', (chart_r + chart_m) * 2)
|
||||||
|
.append('svg:g')
|
||||||
|
.attr('class', 'donut')
|
||||||
|
.attr('transform', 'translate(' + (chart_r + chart_m) + ',' +
|
||||||
|
(chart_r + chart_m) + ')');
|
||||||
|
|
||||||
|
|
||||||
|
var arc = d3.svg.arc()
|
||||||
|
.innerRadius(chart_r * 0.6)
|
||||||
|
.outerRadius(function(d, i) {
|
||||||
|
return i == $scope.data.length - 1 ? chart_r * 1.2 : chart_r * 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
var pie = d3.layout.pie()
|
||||||
|
.sort(null)
|
||||||
|
.value(function(d) {
|
||||||
|
return d.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
var reversed = $scope.data.slice(0).reverse();
|
||||||
|
var g = topG.selectAll(".arc")
|
||||||
|
.data(pie(reversed))
|
||||||
|
.enter().append("g")
|
||||||
|
.attr("class", "arc");
|
||||||
|
|
||||||
|
g.append("path")
|
||||||
|
.attr("d", arc)
|
||||||
|
.style('stroke', '#fff')
|
||||||
|
.style("fill", function(d) { return d.data.color; });
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('data', renderChart);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
|
@ -10,9 +10,17 @@ angular.module('quay').directive('imageLink', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'imageId': '=imageId'
|
'imageId': '=imageId',
|
||||||
|
'manifestDigest': '=?manifestDigest'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
|
$scope.hasSHA256 = function(digest) {
|
||||||
|
return digest && digest.indexOf('sha256:') == 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getShortDigest = function(digest) {
|
||||||
|
return digest.substr('sha256:'.length).substr(0, 12);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
43
static/js/directives/ui/label-input.js
Normal file
43
static/js/directives/ui/label-input.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* An element which allows for editing labels.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('labelInput', function () {
|
||||||
|
return {
|
||||||
|
templateUrl: '/static/directives/label-input.html',
|
||||||
|
restrict: 'C',
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
'labels': '<labels',
|
||||||
|
'updatedLabels': '=?updatedLabels',
|
||||||
|
},
|
||||||
|
controller: function($scope) {
|
||||||
|
$scope.tags = [];
|
||||||
|
|
||||||
|
$scope.$watch('tags', function(tags) {
|
||||||
|
if (!tags) { return; }
|
||||||
|
$scope.updatedLabels = tags.filter(function(tag) {
|
||||||
|
parts = tag['keyValue'].split('=', 2);
|
||||||
|
return tag['label'] ? tag['label'] : {
|
||||||
|
'key': parts[0],
|
||||||
|
'value': parts[1],
|
||||||
|
'is_new': true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('labels', function(labels) {
|
||||||
|
$scope.filteredLabels = labels.filter(function(label) {
|
||||||
|
return label['source_type'] == 'api';
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.tags = $scope.filteredLabels.map(function(label) {
|
||||||
|
return {
|
||||||
|
'id': label['id'],
|
||||||
|
'keyValue': label['key'] + '=' + label['value'],
|
||||||
|
'label': label
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
15
static/js/directives/ui/label-list.js
Normal file
15
static/js/directives/ui/label-list.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* An element which displays labels.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('labelList', function () {
|
||||||
|
return {
|
||||||
|
templateUrl: '/static/directives/label-list.html',
|
||||||
|
restrict: 'C',
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
expand: '@expand',
|
||||||
|
labels: '<labels'
|
||||||
|
},
|
||||||
|
controller: function($scope) {}
|
||||||
|
};
|
||||||
|
});
|
32
static/js/directives/ui/label-view.js
Normal file
32
static/js/directives/ui/label-view.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* An element which displays a single label.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('labelView', function () {
|
||||||
|
return {
|
||||||
|
templateUrl: '/static/directives/label-view.html',
|
||||||
|
restrict: 'C',
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
expand: '@expand',
|
||||||
|
label: '<label'
|
||||||
|
},
|
||||||
|
controller: function($scope, $sanitize) {
|
||||||
|
$scope.getKind = function(label) {
|
||||||
|
switch (label.media_type) {
|
||||||
|
case 'application/json':
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.viewLabelValue = function() {
|
||||||
|
bootbox.alert({
|
||||||
|
size: "small",
|
||||||
|
title: $scope.label.key,
|
||||||
|
message: '<pre>' + $sanitize($scope.label.value) + '</pre>'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -125,7 +125,13 @@ angular.module('quay').directive('logsView', function () {
|
||||||
return 'Remove permission for token {token} from repository {namespace}/{repo}';
|
return 'Remove permission for token {token} from repository {namespace}/{repo}';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
|
'revert_tag': function(metadata) {
|
||||||
|
if (metadata.original_image) {
|
||||||
|
return 'Tag {tag} restored to image {image} from image {original_image}';
|
||||||
|
} else {
|
||||||
|
return 'Tag {tag} recreated pointing to image {image}';
|
||||||
|
}
|
||||||
|
},
|
||||||
'delete_tag': 'Tag {tag} deleted in repository {namespace}/{repo} by user {username}',
|
'delete_tag': 'Tag {tag} deleted in repository {namespace}/{repo} by user {username}',
|
||||||
'create_tag': 'Tag {tag} created in repository {namespace}/{repo} on image {image} by user {username}',
|
'create_tag': 'Tag {tag} created in repository {namespace}/{repo} on image {image} by user {username}',
|
||||||
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {namespace}/{repo} by user {username}',
|
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {namespace}/{repo} by user {username}',
|
||||||
|
@ -266,7 +272,7 @@ angular.module('quay').directive('logsView', function () {
|
||||||
'delete_tag': 'Delete Tag',
|
'delete_tag': 'Delete Tag',
|
||||||
'create_tag': 'Create Tag',
|
'create_tag': 'Create Tag',
|
||||||
'move_tag': 'Move Tag',
|
'move_tag': 'Move Tag',
|
||||||
'revert_tag':' Revert Tag',
|
'revert_tag':'Restore 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',
|
||||||
|
|
60
static/js/directives/ui/manifest-label-list.js
Normal file
60
static/js/directives/ui/manifest-label-list.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* An element which displays the labels on a repository manifest.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('manifestLabelList', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/manifest-label-list.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: true,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'repository': '=repository',
|
||||||
|
'manifestDigest': '=manifestDigest',
|
||||||
|
'cache': '=cache'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ApiService) {
|
||||||
|
$scope.labels = null;
|
||||||
|
|
||||||
|
var loadLabels = function() {
|
||||||
|
if (!$scope.repository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.manifestDigest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.cache[$scope.manifestDigest]) {
|
||||||
|
$scope.labels = $scope.cache[$scope.manifestDigest];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.labels = null;
|
||||||
|
$scope.loadError = false;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'manifestref': $scope.manifestDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listManifestLabels(null, params).then(function(resp) {
|
||||||
|
$scope.labels = resp['labels'];
|
||||||
|
$scope.cache[$scope.manifestDigest] = resp['labels'];
|
||||||
|
}, function() {
|
||||||
|
$scope.loadError = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('cache', function(cache) {
|
||||||
|
if (cache && $scope.manifestDigest && $scope.labels && !cache[$scope.manifestDigest]) {
|
||||||
|
loadLabels();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('repository', loadLabels);
|
||||||
|
$scope.$watch('manifestDigest', loadLabels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
|
@ -11,7 +11,8 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'filter': '=filter',
|
'filter': '=filter',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled',
|
||||||
|
'imageLoader': '=imageLoader'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService) {
|
controller: function($scope, $element, ApiService) {
|
||||||
$scope.tagHistoryData = null;
|
$scope.tagHistoryData = null;
|
||||||
|
@ -43,6 +44,7 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
tags.forEach(function(tag) {
|
tags.forEach(function(tag) {
|
||||||
var tagName = tag.name;
|
var tagName = tag.name;
|
||||||
var dockerImageId = tag.docker_image_id;
|
var dockerImageId = tag.docker_image_id;
|
||||||
|
var manifestDigest = tag.manifest_digest;
|
||||||
|
|
||||||
if (!tagEntries[tagName]) {
|
if (!tagEntries[tagName]) {
|
||||||
tagEntries[tagName] = [];
|
tagEntries[tagName] = [];
|
||||||
|
@ -53,8 +55,10 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
tagEntries[entry.tag_name].splice(tagEntries[entry.tag_name].indexOf(entry), 1);
|
tagEntries[entry.tag_name].splice(tagEntries[entry.tag_name].indexOf(entry), 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
var addEntry = function(action, time, opt_docker_id, opt_old_docker_id) {
|
var addEntry = function(action, time, opt_docker_id, opt_old_docker_id,
|
||||||
|
opt_manifest_digest, opt_old_manifest_digest) {
|
||||||
var entry = {
|
var entry = {
|
||||||
|
'tag': tag,
|
||||||
'tag_name': tagName,
|
'tag_name': tagName,
|
||||||
'action': action,
|
'action': action,
|
||||||
'start_ts': tag.start_ts,
|
'start_ts': tag.start_ts,
|
||||||
|
@ -62,7 +66,9 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
'reversion': tag.reversion,
|
'reversion': tag.reversion,
|
||||||
'time': time * 1000, // JS expects ms, not s since epoch.
|
'time': time * 1000, // JS expects ms, not s since epoch.
|
||||||
'docker_image_id': opt_docker_id || dockerImageId,
|
'docker_image_id': opt_docker_id || dockerImageId,
|
||||||
'old_docker_image_id': opt_old_docker_id || ''
|
'old_docker_image_id': opt_old_docker_id || '',
|
||||||
|
'manifest_digest': opt_manifest_digest || manifestDigest,
|
||||||
|
'old_manifest_digest': opt_old_manifest_digest || null
|
||||||
};
|
};
|
||||||
|
|
||||||
tagEntries[tagName].push(entry);
|
tagEntries[tagName].push(entry);
|
||||||
|
@ -79,7 +85,8 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
if (futureEntry.start_ts - tag.end_ts <= MOVE_THRESHOLD) {
|
if (futureEntry.start_ts - tag.end_ts <= MOVE_THRESHOLD) {
|
||||||
removeEntry(futureEntry);
|
removeEntry(futureEntry);
|
||||||
addEntry(futureEntry.reversion ? 'revert': 'move', tag.end_ts,
|
addEntry(futureEntry.reversion ? 'revert': 'move', tag.end_ts,
|
||||||
futureEntry.docker_image_id, dockerImageId);
|
futureEntry.docker_image_id, dockerImageId, futureEntry.manifest_digest,
|
||||||
|
manifestDigest);
|
||||||
} else {
|
} else {
|
||||||
addEntry('delete', tag.end_ts)
|
addEntry('delete', tag.end_ts)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +94,7 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
|
|
||||||
// If the tag has a start time, it was created.
|
// If the tag has a start time, it was created.
|
||||||
if (tag.start_ts) {
|
if (tag.start_ts) {
|
||||||
addEntry('create', tag.start_ts);
|
addEntry(tag.reversion ? 'recreate' : 'create', tag.start_ts);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -130,6 +137,18 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
$scope.historyEntryMap = tagEntries;
|
$scope.historyEntryMap = tagEntries;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.isCurrent = function(entry) {
|
||||||
|
return $scope.historyEntryMap[entry.tag_name][0] == entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.askRestoreTag = function(entity, use_current_id) {
|
||||||
|
if ($scope.repository.can_write) {
|
||||||
|
var docker_id = use_current_id ? entity.docker_image_id : entity.old_docker_image_id;
|
||||||
|
var digest = use_current_id ? entity.manifest_digest : entity.old_manifest_digest;
|
||||||
|
$scope.tagActionHandler.askRestoreTag(entity.tag, docker_id, digest);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getEntryClasses = function(entry, historyFilter) {
|
$scope.getEntryClasses = function(entry, historyFilter) {
|
||||||
if (!entry.action) { return ''; }
|
if (!entry.action) { return ''; }
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'actionHandler': '=actionHandler',
|
'actionHandler': '=actionHandler',
|
||||||
'imageLoader': '=imageLoader',
|
'imageLoader': '=imageLoader',
|
||||||
'tagChanged': '&tagChanged'
|
'tagChanged': '&tagChanged',
|
||||||
|
'labelsChanged': '&labelsChanged'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $timeout, ApiService) {
|
controller: function($scope, $element, $timeout, ApiService) {
|
||||||
$scope.addingTag = false;
|
$scope.addingTag = false;
|
||||||
|
@ -115,7 +116,7 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.revertTag = function(tag, image_id, callback) {
|
$scope.restoreTag = function(tag, image_id, opt_manifest_digest, callback) {
|
||||||
if (!$scope.repository.can_write) { return; }
|
if (!$scope.repository.can_write) { return; }
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
|
@ -127,13 +128,97 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
'image': image_id
|
'image': image_id
|
||||||
};
|
};
|
||||||
|
|
||||||
var errorHandler = ApiService.errorDisplay('Cannot revert tag', callback);
|
if (opt_manifest_digest) {
|
||||||
ApiService.revertTag(data, params).then(function() {
|
data['manifest_digest'] = opt_manifest_digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot restore tag', callback);
|
||||||
|
ApiService.restoreTag(data, params).then(function() {
|
||||||
callback(true);
|
callback(true);
|
||||||
markChanged([], [tag]);
|
markChanged([], [tag]);
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.editLabels = function(info, callback) {
|
||||||
|
var actions = [];
|
||||||
|
var existingMutableLabels = {};
|
||||||
|
|
||||||
|
// Build the set of adds and deletes.
|
||||||
|
info['updated_labels'].forEach(function(label) {
|
||||||
|
if (label['id']) {
|
||||||
|
existingMutableLabels[label['id']] = true;
|
||||||
|
} else {
|
||||||
|
actions.push({
|
||||||
|
'action': 'add',
|
||||||
|
'label': label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
info['mutable_labels'].forEach(function(label) {
|
||||||
|
if (!existingMutableLabels[label['id']]) {
|
||||||
|
actions.push({
|
||||||
|
'action': 'delete',
|
||||||
|
'label': label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute the add and delete label actions.
|
||||||
|
var currentIndex = 0;
|
||||||
|
|
||||||
|
var performAction = function() {
|
||||||
|
if (currentIndex >= actions.length) {
|
||||||
|
$scope.labelsChanged({'manifest_digest': info['manifest_digest']});
|
||||||
|
callback(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentAction = actions[currentIndex];
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Could not update labels', callback);
|
||||||
|
switch (currentAction.action) {
|
||||||
|
case 'add':
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'manifestref': info['manifest_digest']
|
||||||
|
};
|
||||||
|
|
||||||
|
var pieces = currentAction['label']['keyValue'].split('=', 2);
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'key': pieces[0],
|
||||||
|
'value': pieces[1],
|
||||||
|
'media_type': null // Have backend infer the media type
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.addManifestLabel(data, params).then(performAction, errorHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'manifestref': info['manifest_digest'],
|
||||||
|
'labelid': currentAction['label']['id']
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteManifestLabel(null, params).then(performAction, errorHandler);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performAction();
|
||||||
|
};
|
||||||
|
|
||||||
|
var filterLabels = function(labels, readOnly) {
|
||||||
|
if (!labels) { return []; }
|
||||||
|
|
||||||
|
return labels.filter(function(label) {
|
||||||
|
return (label['source_type'] != 'api') == readOnly;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.actionHandler = {
|
$scope.actionHandler = {
|
||||||
'askDeleteTag': function(tag) {
|
'askDeleteTag': function(tag) {
|
||||||
$scope.deleteTagInfo = {
|
$scope.deleteTagInfo = {
|
||||||
|
@ -155,18 +240,42 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
$element.find('#createOrMoveTagModal').modal('show');
|
$element.find('#createOrMoveTagModal').modal('show');
|
||||||
},
|
},
|
||||||
|
|
||||||
'askRevertTag': function(tag, image_id) {
|
'showLabelEditor': function(manifest_digest) {
|
||||||
|
$scope.editLabelsInfo = {
|
||||||
|
'manifest_digest': manifest_digest,
|
||||||
|
'loading': true
|
||||||
|
};
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'manifestref': manifest_digest
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listManifestLabels(null, params).then(function(resp) {
|
||||||
|
var labels = resp['labels'];
|
||||||
|
|
||||||
|
$scope.editLabelsInfo['readonly_labels'] = filterLabels(labels, true);
|
||||||
|
$scope.editLabelsInfo['mutable_labels'] = filterLabels(labels, false);
|
||||||
|
|
||||||
|
$scope.editLabelsInfo['labels'] = labels;
|
||||||
|
$scope.editLabelsInfo['loading'] = false;
|
||||||
|
|
||||||
|
}, ApiService.errorDisplay('Could not load manifest labels'));
|
||||||
|
},
|
||||||
|
|
||||||
|
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
|
||||||
if (tag.image_id == image_id) {
|
if (tag.image_id == image_id) {
|
||||||
bootbox.alert('This is the current image for the tag');
|
bootbox.alert('This is the current image for the tag');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.revertTagInfo = {
|
$scope.restoreTagInfo = {
|
||||||
'tag': tag,
|
'tag': tag,
|
||||||
'image_id': image_id
|
'image_id': image_id,
|
||||||
|
'manifest_digest': opt_manifest_digest
|
||||||
};
|
};
|
||||||
|
|
||||||
$element.find('#revertTagModal').modal('show');
|
$element.find('#restoreTagModal').modal('show');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,14 @@
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
$scope.buildsShown = 0;
|
$scope.buildsShown = 0;
|
||||||
$scope.settingsShown = 0;
|
$scope.settingsShown = 0;
|
||||||
|
$scope.historyShown = 0;
|
||||||
|
|
||||||
$scope.viewScope = {
|
$scope.viewScope = {
|
||||||
'selectedTags': [],
|
'selectedTags': [],
|
||||||
'repository': null,
|
'repository': null,
|
||||||
'imageLoader': imageLoader,
|
'imageLoader': imageLoader,
|
||||||
'builds': null,
|
'builds': null,
|
||||||
'changesVisible': false
|
'historyFilter': ''
|
||||||
};
|
};
|
||||||
|
|
||||||
var buildPollChannel = null;
|
var buildPollChannel = null;
|
||||||
|
@ -133,6 +134,10 @@
|
||||||
$scope.buildsShown++;
|
$scope.buildsShown++;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showHistory = function() {
|
||||||
|
$scope.historyShown++;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showSettings = function() {
|
$scope.showSettings = function() {
|
||||||
$scope.settingsShown++;
|
$scope.settingsShown++;
|
||||||
};
|
};
|
||||||
|
@ -147,10 +152,6 @@
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.handleChangesState = function(value) {
|
|
||||||
$scope.viewScope.changesVisible = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getImages = function(callback) {
|
$scope.getImages = function(callback) {
|
||||||
loadImages(callback);
|
loadImages(callback);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ var quayDependencies: any[] = [
|
||||||
'core-ui',
|
'core-ui',
|
||||||
'core-config-setup',
|
'core-config-setup',
|
||||||
'infinite-scroll',
|
'infinite-scroll',
|
||||||
|
'ngTagsInput',
|
||||||
'react'
|
'react'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -45,4 +45,4 @@ angular
|
||||||
.constant('NAME_PATTERNS', NAME_PATTERNS)
|
.constant('NAME_PATTERNS', NAME_PATTERNS)
|
||||||
.constant('INJECTED_CONFIG', INJECTED_CONFIG)
|
.constant('INJECTED_CONFIG', INJECTED_CONFIG)
|
||||||
.constant('INJECTED_FEATURES', INJECTED_FEATURES)
|
.constant('INJECTED_FEATURES', INJECTED_FEATURES)
|
||||||
.constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS);
|
.constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS);
|
|
@ -279,6 +279,8 @@ angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService',
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'features': features,
|
'features': features,
|
||||||
|
'brokenFeaturesCount': features.length - totalCount,
|
||||||
|
'fixableFeatureCount': features.filter(function(f) { return f.fixableScore > 0 }).length,
|
||||||
'severityBreakdown': severityBreakdown,
|
'severityBreakdown': severityBreakdown,
|
||||||
'highestFixableScore': highestFixableScore
|
'highestFixableScore': highestFixableScore
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ URI.js - MIT (https://github.com/medialize/URI.js)
|
||||||
angular-hotkeys - MIT (https://github.com/chieffancypants/angular-hotkeys/blob/master/LICENSE)
|
angular-hotkeys - MIT (https://github.com/chieffancypants/angular-hotkeys/blob/master/LICENSE)
|
||||||
angular-debounce - MIT (https://github.com/shahata/angular-debounce/blob/master/LICENSE)
|
angular-debounce - MIT (https://github.com/shahata/angular-debounce/blob/master/LICENSE)
|
||||||
infinite-scroll - MIT (https://github.com/sroze/ngInfiniteScroll/blob/master/LICENSE)
|
infinite-scroll - MIT (https://github.com/sroze/ngInfiniteScroll/blob/master/LICENSE)
|
||||||
|
ngTagsInput - MIT (https://github.com/mbenford/ngTagsInput/blob/master/LICENSE)
|
||||||
|
|
||||||
Issues:
|
Issues:
|
||||||
>>>>> jquery.spotlight - GPLv3 (https://github.com/jameshalsall/jQuery-Spotlight)
|
>>>>> jquery.spotlight - GPLv3 (https://github.com/jameshalsall/jQuery-Spotlight)
|
|
@ -47,18 +47,17 @@
|
||||||
<i class="fa fa-tags"></i>
|
<i class="fa fa-tags"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span class="cor-tab" tab-title="Tag History" tab-target="#history" id="tagHistoryTab"
|
||||||
|
tab-init="showHistory()">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="cor-tab" tab-title="Builds" tab-target="#builds" id="buildsTab"
|
<span class="cor-tab" tab-title="Builds" tab-target="#builds" id="buildsTab"
|
||||||
tab-init="showBuilds()"
|
tab-init="showBuilds()"
|
||||||
quay-show="viewScope.repository.can_write && Features.BUILD_SUPPORT">
|
quay-show="viewScope.repository.can_write && Features.BUILD_SUPPORT">
|
||||||
<i class="fa fa-tasks"></i>
|
<i class="fa fa-tasks"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="cor-tab" tab-title="Visualize" tab-target="#changes"
|
|
||||||
tab-shown="handleChangesState(true)"
|
|
||||||
tab-hidden="handleChangesState(false)">
|
|
||||||
<i class="fa fa-code-fork"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Admin Only Tabs -->
|
<!-- Admin Only Tabs -->
|
||||||
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
|
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
|
||||||
ng-show="viewScope.repository.can_admin">
|
ng-show="viewScope.repository.can_admin">
|
||||||
|
@ -87,9 +86,20 @@
|
||||||
repository="viewScope.repository"
|
repository="viewScope.repository"
|
||||||
image-loader="viewScope.imageLoader"
|
image-loader="viewScope.imageLoader"
|
||||||
selected-tags="viewScope.selectedTags"
|
selected-tags="viewScope.selectedTags"
|
||||||
|
history-filter="viewScope.historyFilter"
|
||||||
is-enabled="tagsShown"></div>
|
is-enabled="tagsShown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tag History -->
|
||||||
|
<div id="history" class="tab-pane">
|
||||||
|
<h3 class="tab-header">Tag History</h3>
|
||||||
|
<div class="repo-tag-history"
|
||||||
|
repository="viewScope.repository"
|
||||||
|
filter="viewScope.historyFilter"
|
||||||
|
image-loader="viewScope.imageLoader"
|
||||||
|
is-enabled="historyShown"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Builds -->
|
<!-- Builds -->
|
||||||
<div id="builds" class="tab-pane">
|
<div id="builds" class="tab-pane">
|
||||||
<div class="repo-panel-builds"
|
<div class="repo-panel-builds"
|
||||||
|
@ -98,15 +108,6 @@
|
||||||
is-enabled="buildsShown"></div>
|
is-enabled="buildsShown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Changes -->
|
|
||||||
<div id="changes" class="tab-pane">
|
|
||||||
<div class="repo-panel-changes"
|
|
||||||
repository="viewScope.repository"
|
|
||||||
image-loader="viewScope.imageLoader"
|
|
||||||
selected-tags="viewScope.selectedTags"
|
|
||||||
is-enabled="viewScope.changesVisible"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Usage Logs -->
|
<!-- Usage Logs -->
|
||||||
<div id="logs" class="tab-pane" ng-if="viewScope.repository.can_admin">
|
<div id="logs" class="tab-pane" ng-if="viewScope.repository.can_admin">
|
||||||
<div class="logs-view" repository="viewScope.repository" makevisible="logsShown"></div>
|
<div class="logs-view" repository="viewScope.repository" makevisible="logsShown"></div>
|
||||||
|
|
|
@ -12,7 +12,7 @@ from endpoints.api import api_bp, api
|
||||||
|
|
||||||
from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
|
from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
|
||||||
TeamPermissions)
|
TeamPermissions)
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RestoreTag
|
||||||
from endpoints.api.search import EntitySearch
|
from endpoints.api.search import EntitySearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||||
|
@ -2539,10 +2539,10 @@ class TestRepositoryImage5avqBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('GET', 404, 'devtable', None)
|
self._run_test('GET', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rPublicPublicrepo(ApiTestCase):
|
class TestRestoreTagHp8rPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="public/publicrepo")
|
self._set_url(RestoreTag, tag="HP8R", repository="public/publicrepo")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2557,10 +2557,10 @@ class TestRevertTagHp8rPublicPublicrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rDevtableShared(ApiTestCase):
|
class TestRestoreTagHp8rDevtableShared(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="devtable/shared")
|
self._set_url(RestoreTag, tag="HP8R", repository="devtable/shared")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2572,13 +2572,13 @@ class TestRevertTagHp8rDevtableShared(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 400, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
class TestRestoreTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="buynlarge/orgrepo")
|
self._set_url(RestoreTag, tag="HP8R", repository="buynlarge/orgrepo")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2590,7 +2590,7 @@ class TestRevertTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 400, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ from util.secscan.fake import fake_security_scanner
|
||||||
|
|
||||||
from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
|
from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
|
||||||
TeamPermissions, InviteTeamMember)
|
TeamPermissions, InviteTeamMember)
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RestoreTag, ListRepositoryTags
|
||||||
from endpoints.api.search import EntitySearch, ConductSearch
|
from endpoints.api.search import EntitySearch, ConductSearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource
|
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource
|
||||||
|
@ -2872,24 +2872,32 @@ class TestGetImageChanges(ApiTestCase):
|
||||||
# image_id=image_id))
|
# image_id=image_id))
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTag(ApiTestCase):
|
class TestRestoreTag(ApiTestCase):
|
||||||
def test_reverttag_invalidtag(self):
|
def test_restoretag_invalidtag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
self.postResponse(RevertTag,
|
self.postResponse(RestoreTag,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='invalidtag'),
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='invalidtag'),
|
||||||
data=dict(image='invalid_image'),
|
data=dict(image='invalid_image'),
|
||||||
expected_code=404)
|
expected_code=400)
|
||||||
|
|
||||||
def test_reverttag_invalidimage(self):
|
def test_restoretag_invalidimage(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
self.postResponse(RevertTag,
|
self.postResponse(RestoreTag,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
||||||
data=dict(image='invalid_image'),
|
data=dict(image='invalid_image'),
|
||||||
expected_code=400)
|
expected_code=400)
|
||||||
|
|
||||||
def test_reverttag(self):
|
def test_restoretag_invalidmanifest(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
self.postResponse(RestoreTag,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
||||||
|
data=dict(manifest_digest='invalid_digest'),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
def test_restoretag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
json = self.getJsonResponse(ListRepositoryTags,
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
@ -2901,8 +2909,8 @@ class TestRevertTag(ApiTestCase):
|
||||||
|
|
||||||
previous_image_id = json['tags'][1]['docker_image_id']
|
previous_image_id = json['tags'][1]['docker_image_id']
|
||||||
|
|
||||||
self.postJsonResponse(RevertTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
self.postJsonResponse(RestoreTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
tag='latest'),
|
tag='latest'),
|
||||||
data=dict(image=previous_image_id))
|
data=dict(image=previous_image_id))
|
||||||
|
|
||||||
json = self.getJsonResponse(ListRepositoryTags,
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
@ -2912,6 +2920,29 @@ class TestRevertTag(ApiTestCase):
|
||||||
self.assertFalse('end_ts' in json['tags'][0])
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
self.assertEquals(previous_image_id, json['tags'][0]['docker_image_id'])
|
self.assertEquals(previous_image_id, json['tags'][0]['docker_image_id'])
|
||||||
|
|
||||||
|
def test_restoretag_to_digest(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'))
|
||||||
|
|
||||||
|
self.assertEquals(2, len(json['tags']))
|
||||||
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
|
|
||||||
|
previous_manifest = json['tags'][1]['manifest_digest']
|
||||||
|
|
||||||
|
self.postJsonResponse(RestoreTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'),
|
||||||
|
data=dict(image='foo', manifest_digest=previous_manifest))
|
||||||
|
|
||||||
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'))
|
||||||
|
self.assertEquals(3, len(json['tags']))
|
||||||
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
|
self.assertEquals(previous_manifest, json['tags'][0]['manifest_digest'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestListAndDeleteTag(ApiTestCase):
|
class TestListAndDeleteTag(ApiTestCase):
|
||||||
|
@ -3030,6 +3061,13 @@ class TestListAndDeleteTag(ApiTestCase):
|
||||||
|
|
||||||
self.assertEquals(prod_images, json['images'])
|
self.assertEquals(prod_images, json['images'])
|
||||||
|
|
||||||
|
def test_listtag_digest(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', page=1,
|
||||||
|
limit=1))
|
||||||
|
self.assertTrue('manifest_digest' in json['tags'][0])
|
||||||
|
|
||||||
def test_listtagpagination(self):
|
def test_listtagpagination(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
|
Reference in a new issue