Never load the full repo image list
Always make smaller queries per tag to ensure we scale better Fixes #754
This commit is contained in:
parent
43720b27e7
commit
4f41f79fa8
15 changed files with 268 additions and 254 deletions
|
@ -4,7 +4,7 @@ from flask import request, abort
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||||
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||||
path_param, parse_args, query_param)
|
path_param, parse_args, query_param, truthy_bool)
|
||||||
from endpoints.api.image import image_view
|
from endpoints.api.image import image_view
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -135,7 +135,10 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
""" Resource for listing the images in a specific repository tag. """
|
""" Resource for listing the images in a specific repository tag. """
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@nickname('listTagImages')
|
@nickname('listTagImages')
|
||||||
def get(self, namespace, repository, tag):
|
@parse_args
|
||||||
|
@query_param('owned', 'If specified, only images wholely owned by this tag are returned.',
|
||||||
|
type=truthy_bool, default=False)
|
||||||
|
def get(self, args, namespace, repository, tag):
|
||||||
""" List the images for the specified repository tag. """
|
""" List the images for the specified repository tag. """
|
||||||
try:
|
try:
|
||||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
||||||
|
@ -144,15 +147,37 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
|
|
||||||
parent_images = model.image.get_parent_images(namespace, repository, tag_image)
|
parent_images = model.image.get_parent_images(namespace, repository, tag_image)
|
||||||
image_map = {}
|
image_map = {}
|
||||||
|
|
||||||
|
image_map[str(tag_image.id)] = tag_image
|
||||||
|
|
||||||
for image in parent_images:
|
for image in parent_images:
|
||||||
image_map[str(image.id)] = image
|
image_map[str(image.id)] = image
|
||||||
|
|
||||||
|
image_map_all = dict(image_map)
|
||||||
|
|
||||||
parents = list(parent_images)
|
parents = list(parent_images)
|
||||||
parents.reverse()
|
parents.reverse()
|
||||||
all_images = [tag_image] + parents
|
all_images = [tag_image] + parents
|
||||||
|
|
||||||
|
# Filter the images returned to those not found in the ancestry of any of the other tags in
|
||||||
|
# the repository.
|
||||||
|
if args['owned']:
|
||||||
|
all_tags = model.tag.list_repository_tags(namespace, repository)
|
||||||
|
for current_tag in all_tags:
|
||||||
|
if current_tag.name == tag:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove the tag's image ID.
|
||||||
|
tag_image_id = str(current_tag.image_id)
|
||||||
|
image_map.pop(tag_image_id, None)
|
||||||
|
|
||||||
|
# Remove any ancestors:
|
||||||
|
for ancestor_id in current_tag.image.ancestors.split('/'):
|
||||||
|
image_map.pop(ancestor_id, None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'images': [image_view(image, image_map) for image in all_images]
|
'images': [image_view(image, image_map_all) for image in all_images
|
||||||
|
if not args['owned'] or (str(image.id) in image_map)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,14 @@
|
||||||
<div class="image-section">
|
<div class="image-section">
|
||||||
<i class="fa fa-tag section-icon" data-title="Current Tags" bs-tooltip></i>
|
<i class="fa fa-tag section-icon" data-title="Current Tags" bs-tooltip></i>
|
||||||
<span class="section-info section-info-with-dropdown">
|
<span class="section-info section-info-with-dropdown">
|
||||||
<a class="label tag label-default" ng-repeat="tag in imageData.tags"
|
<a class="label tag label-default" ng-repeat="tag in getTags(imageData)"
|
||||||
href="javascript:void(0)" ng-click="tagSelected({'tag': tag})">
|
href="javascript:void(0)" ng-click="tagSelected({'tag': tag})">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</a>
|
</a>
|
||||||
<span style="color: #ccc;" ng-if="!imageData.tags.length">(No Tags)</span>
|
<span style="color: #ccc;" ng-if="!getTags(imageData).length">(No Tags)</span>
|
||||||
|
|
||||||
<div class="dropdown" data-placement="top"
|
<div class="dropdown" data-placement="top"
|
||||||
ng-if="tracker.repository.can_write || imageData.tags">
|
ng-if="tracker.repository.can_write || getTags(imageData)">
|
||||||
<a href="javascript:void(0)" class="dropdown-button" data-toggle="dropdown"
|
<a href="javascript:void(0)" class="dropdown-button" data-toggle="dropdown"
|
||||||
bs-tooltip="tooltip.title" data-title="Manage Tags"
|
bs-tooltip="tooltip.title" data-title="Manage Tags"
|
||||||
data-container="body">
|
data-container="body">
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu pull-right">
|
<ul class="dropdown-menu pull-right">
|
||||||
<li ng-repeat="tag in imageData.tags">
|
<li ng-repeat="tag in getTags(imageData)">
|
||||||
<a href="javascript:void(0)" ng-click="tagSelected({'tag': tag})">
|
<a href="javascript:void(0)" ng-click="tagSelected({'tag': tag})">
|
||||||
<i class="fa fa-tag"></i>{{ tag }}
|
<i class="fa fa-tag"></i>{{ tag }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="repo-panel-changes-element">
|
<div class="repo-panel-changes-element">
|
||||||
<div class="resource-view" resource="imagesResource"
|
<div class="cor-loader" ng-show="loading"></div>
|
||||||
error-message="'Could not load repository images'">
|
<div ng-show="!loading">
|
||||||
<h3 class="tab-header">
|
<h3 class="tab-header">
|
||||||
Visualize Tags:
|
Visualize Tags:
|
||||||
<span class="multiselect-dropdown" items="tagNames" selected-items="selectedTagsSlice"
|
<span class="multiselect-dropdown" items="tagNames" selected-items="selectedTagsSlice"
|
||||||
|
@ -19,49 +19,46 @@
|
||||||
|
|
||||||
<!-- Tags Selected -->
|
<!-- Tags Selected -->
|
||||||
<div ng-show="selectedTagsSlice.length > 0">
|
<div ng-show="selectedTagsSlice.length > 0">
|
||||||
<div id="image-history row" class="resource-view" resource="imagesResource"
|
<!-- Tree View container -->
|
||||||
error-message="'Cannot load repository images'">
|
<div class="col-md-8">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<!-- Image history tree -->
|
||||||
|
<div id="image-history-container" onresize="tree.notifyResized()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tree View container -->
|
<!-- Side Panel -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="side-panel-title" ng-if="currentTag">
|
||||||
<!-- Image history tree -->
|
<i class="fa fa-tag"></i>{{ currentTag }}
|
||||||
<div id="image-history-container" onresize="tree.notifyResized()"></div>
|
</div>
|
||||||
</div>
|
<div class="side-panel-title" ng-if="currentImage">
|
||||||
|
<i class="fa fa-archive"></i>{{ currentImage.substr(0, 12) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Side Panel -->
|
<div class="side-panel">
|
||||||
<div class="col-md-4">
|
<!-- Tag Info -->
|
||||||
<div class="side-panel-title" ng-if="currentTag">
|
<div class="tag-info-sidebar"
|
||||||
<i class="fa fa-tag"></i>{{ currentTag }}
|
tracker="tracker"
|
||||||
</div>
|
tag="currentTag"
|
||||||
<div class="side-panel-title" ng-if="currentImage">
|
image-selected="setImage(image)"
|
||||||
<i class="fa fa-archive"></i>{{ currentImage.substr(0, 12) }}
|
delete-tag-requested="tagActionHandler.askDeleteTag(tag)"
|
||||||
|
ng-if="currentTag">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-panel">
|
<!-- Image Info -->
|
||||||
<!-- Tag Info -->
|
<div class="image-info-sidebar"
|
||||||
<div class="tag-info-sidebar"
|
tracker="tracker"
|
||||||
tracker="tracker"
|
image="currentImage"
|
||||||
tag="currentTag"
|
image-loader="imageLoader"
|
||||||
image-selected="setImage(image)"
|
tag-selected="setTag(tag)"
|
||||||
delete-tag-requested="tagActionHandler.askDeleteTag(tag)"
|
add-tag-requested="tagActionHandler.askAddTag(image)"
|
||||||
ng-if="currentTag">
|
ng-if="currentImage">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image Info -->
|
|
||||||
<div class="image-info-sidebar"
|
|
||||||
tracker="tracker"
|
|
||||||
image="currentImage"
|
|
||||||
tag-selected="setTag(tag)"
|
|
||||||
add-tag-requested="tagActionHandler.askAddTag(image)"
|
|
||||||
ng-if="currentImage">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-operations-dialog" repository="repository" images="images"
|
<div class="tag-operations-dialog" repository="repository" image-loader="imageLoader"
|
||||||
action-handler="tagActionHandler" tag-changed="handleTagChanged(data)"></div>
|
action-handler="tagActionHandler" tag-changed="handleTagChanged(data)"></div>
|
|
@ -172,7 +172,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-operations-dialog" repository="repository"
|
<div class="tag-operations-dialog" repository="repository"
|
||||||
get-images="getImages({'callback': callback})"
|
image-loader="imageLoader"
|
||||||
action-handler="tagActionHandler"></div>
|
action-handler="tagActionHandler"></div>
|
||||||
|
|
||||||
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler"></div>
|
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler"></div>
|
|
@ -49,11 +49,11 @@
|
||||||
|
|
||||||
<div class="tag-specific-images-view"
|
<div class="tag-specific-images-view"
|
||||||
tag="tagToCreate"
|
tag="tagToCreate"
|
||||||
repository="repo"
|
repository="repository"
|
||||||
images="imagesInternal"
|
|
||||||
image-cutoff="toTagImage"
|
image-cutoff="toTagImage"
|
||||||
style="margin: 10px; margin-top: 20px; margin-bottom: -10px;"
|
style="margin: 10px; margin-top: 20px; margin-bottom: -10px;"
|
||||||
ng-show="isAnotherImageTag(toTagImage, tagToCreate)">
|
ng-show="isAnotherImageTag(toTagImage, tagToCreate)"
|
||||||
|
image-loader="imageLoader">
|
||||||
This will also delete any unattached images and delete the following images:
|
This will also delete any unattached images and delete the following images:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
<span class="label label-default tag">{{ deleteTagInfo.tag }}</span>?
|
<span class="label label-default tag">{{ deleteTagInfo.tag }}</span>?
|
||||||
|
|
||||||
<div class="tag-specific-images-view" tag="deleteTagInfo.tag" repository="repository"
|
<div class="tag-specific-images-view" tag="deleteTagInfo.tag" repository="repository"
|
||||||
images="imagesInternal" style="margin-top: 20px">
|
image-loader="imageLoader" style="margin-top: 20px">
|
||||||
The following images and any other images not referenced by a tag will be deleted:
|
The following images and any other images not referenced by a tag will be deleted:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<div class="tag-specific-images-view-element" ng-show="tagSpecificImages.length">
|
<div class="tag-specific-images-view-element" ng-show="loading">
|
||||||
|
<div class="cor-loader-inline"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tag-specific-images-view-element" ng-show="tagSpecificImages.length && !loading">
|
||||||
<div ng-transclude></div>
|
<div ng-transclude></div>
|
||||||
<div class="image-listings">
|
<div class="image-listings">
|
||||||
<div class="image-listing" ng-repeat="image in tagSpecificImages | limitTo:5"
|
<div class="image-listing" ng-repeat="image in tagSpecificImages | limitTo:5"
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
* An element which displays the changes visualization panel for a repository view.
|
* An element which displays the changes visualization panel for a repository view.
|
||||||
*/
|
*/
|
||||||
angular.module('quay').directive('repoPanelChanges', function () {
|
angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
var RepositoryImageTracker = function(repository, images) {
|
var RepositoryImageTracker = function(repository, imageLoader) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.images = images;
|
this.imageLoader = imageLoader;
|
||||||
|
|
||||||
// Build a map of image ID -> image.
|
// Build a map of image ID -> image.
|
||||||
|
var images = imageLoader.images;
|
||||||
var imageIDMap = {};
|
var imageIDMap = {};
|
||||||
this.images.map(function(image) {
|
|
||||||
|
images.forEach(function(image) {
|
||||||
imageIDMap[image.id] = image;
|
imageIDMap[image.id] = image;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,12 +93,13 @@ angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
'selectedTags': '=selectedTags',
|
'selectedTags': '=selectedTags',
|
||||||
|
|
||||||
'imagesResource': '=imagesResource',
|
'imagesResource': '=imagesResource',
|
||||||
'images': '=images',
|
'imageLoader': '=imageLoader',
|
||||||
|
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) {
|
controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) {
|
||||||
$scope.tagNames = [];
|
$scope.tagNames = [];
|
||||||
|
$scope.loading = true;
|
||||||
|
|
||||||
$scope.$watch('selectedTags', function(selectedTags) {
|
$scope.$watch('selectedTags', function(selectedTags) {
|
||||||
if (!selectedTags) { return; }
|
if (!selectedTags) { return; }
|
||||||
|
@ -110,18 +113,17 @@ angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
$scope.currentImage = null;
|
$scope.currentImage = null;
|
||||||
$scope.currentTag = null;
|
$scope.currentTag = null;
|
||||||
|
|
||||||
if ($scope.tracker) {
|
$scope.loading = true;
|
||||||
refreshTree();
|
$scope.imageLoader.loadImages($scope.selectedTagsSlice, function() {
|
||||||
} else {
|
$scope.loading = false;
|
||||||
updateImages();
|
updateImages();
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var updateImages = function() {
|
var updateImages = function() {
|
||||||
if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; }
|
if (!$scope.repository || !$scope.imageLoader || !$scope.isEnabled) { return; }
|
||||||
|
|
||||||
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images);
|
|
||||||
|
|
||||||
|
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.imageLoader);
|
||||||
if ($scope.selectedTagsSlice && $scope.selectedTagsSlice.length) {
|
if ($scope.selectedTagsSlice && $scope.selectedTagsSlice.length) {
|
||||||
refreshTree();
|
refreshTree();
|
||||||
}
|
}
|
||||||
|
@ -131,22 +133,25 @@ angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
$scope.$watch('repository', update);
|
$scope.$watch('repository', update);
|
||||||
$scope.$watch('isEnabled', update);
|
$scope.$watch('isEnabled', update);
|
||||||
|
|
||||||
$scope.$watch('images', updateImages);
|
|
||||||
|
|
||||||
$scope.updateState = function() {
|
$scope.updateState = function() {
|
||||||
update();
|
update();
|
||||||
};
|
};
|
||||||
|
|
||||||
var refreshTree = function() {
|
var refreshTree = function() {
|
||||||
if (!$scope.repository || !$scope.images || !$scope.isEnabled) { return; }
|
if (!$scope.repository || !$scope.imageLoader || !$scope.isEnabled) { return; }
|
||||||
if ($scope.selectedTagsSlice.length < 1) { return; }
|
if ($scope.selectedTagsSlice.length < 1) { return; }
|
||||||
|
|
||||||
$('#image-history-container').empty();
|
$('#image-history-container').empty();
|
||||||
|
|
||||||
|
var getTagsForImage = function(image) {
|
||||||
|
return $scope.imageLoader.getTagsForImage(image);
|
||||||
|
};
|
||||||
|
|
||||||
var tree = new ImageHistoryTree(
|
var tree = new ImageHistoryTree(
|
||||||
$scope.repository.namespace,
|
$scope.repository.namespace,
|
||||||
$scope.repository.name,
|
$scope.repository.name,
|
||||||
$scope.images,
|
$scope.imageLoader.images,
|
||||||
|
getTagsForImage,
|
||||||
UtilService.getFirstMarkdownLineAsText,
|
UtilService.getFirstMarkdownLineAsText,
|
||||||
$scope.getTimeSince,
|
$scope.getTimeSince,
|
||||||
ImageMetadataService.getEscapedFormattedCommand,
|
ImageMetadataService.getEscapedFormattedCommand,
|
||||||
|
@ -194,8 +199,6 @@ angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.handleTagChanged = function(data) {
|
$scope.handleTagChanged = function(data) {
|
||||||
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images);
|
|
||||||
|
|
||||||
data.removed.map(function(tag) {
|
data.removed.map(function(tag) {
|
||||||
$scope.currentImage = null;
|
$scope.currentImage = null;
|
||||||
$scope.currentTag = null;
|
$scope.currentTag = null;
|
||||||
|
@ -206,7 +209,7 @@ angular.module('quay').directive('repoPanelChanges', function () {
|
||||||
$scope.currentTag = tag;
|
$scope.currentTag = tag;
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshTree();
|
update();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'selectedTags': '=selectedTags',
|
'selectedTags': '=selectedTags',
|
||||||
'imagesResource': '=imagesResource',
|
'imagesResource': '=imagesResource',
|
||||||
'images': '=images',
|
'imageLoader': '=imageLoader',
|
||||||
|
|
||||||
'isEnabled': '=isEnabled',
|
'isEnabled': '=isEnabled',
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ angular.module('quay').directive('imageInfoSidebar', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'tracker': '=tracker',
|
'tracker': '=tracker',
|
||||||
'image': '=image',
|
'image': '=image',
|
||||||
|
'imageLoader': '=imageLoader',
|
||||||
|
|
||||||
'tagSelected': '&tagSelected',
|
'tagSelected': '&tagSelected',
|
||||||
'addTagRequested': '&addTagRequested'
|
'addTagRequested': '&addTagRequested'
|
||||||
|
@ -25,6 +26,10 @@ angular.module('quay').directive('imageInfoSidebar', function () {
|
||||||
return Date.parse(dateString);
|
return Date.parse(dateString);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.getTags = function(imageData) {
|
||||||
|
return $scope.imageLoader.getTagsForImage(imageData);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,46 +11,31 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'images': '=images',
|
|
||||||
'actionHandler': '=actionHandler',
|
'actionHandler': '=actionHandler',
|
||||||
|
'imageLoader': '=imageLoader',
|
||||||
'getImages': '&getImages',
|
|
||||||
'tagChanged': '&tagChanged'
|
'tagChanged': '&tagChanged'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $timeout, ApiService) {
|
controller: function($scope, $element, $timeout, ApiService) {
|
||||||
$scope.addingTag = false;
|
$scope.addingTag = false;
|
||||||
$scope.imagesInternal = [];
|
|
||||||
|
|
||||||
$scope.$watch('images', function(images) {
|
|
||||||
if (!images) { return; }
|
|
||||||
$scope.imagesInternal = images;
|
|
||||||
});
|
|
||||||
|
|
||||||
var markChanged = function(added, removed) {
|
var markChanged = function(added, removed) {
|
||||||
// Reload the repository and the images.
|
// Reload the repository.
|
||||||
$scope.repository.get().then(function(resp) {
|
$scope.repository.get().then(function(resp) {
|
||||||
$scope.repository = resp;
|
$scope.repository = resp;
|
||||||
|
$scope.imageLoader.reset()
|
||||||
|
|
||||||
var params = {
|
// Note: We need the timeout here so that Angular can $digest the images change
|
||||||
'repository': resp.namespace + '/' + resp.name
|
// on the parent scope before the tagChanged callback occurs.
|
||||||
};
|
$timeout(function() {
|
||||||
|
$scope.tagChanged({
|
||||||
ApiService.listRepositoryImages(null, params).then(function(resp) {
|
'data': { 'added': added, 'removed': removed }
|
||||||
$scope.images = resp.images;
|
});
|
||||||
|
}, 1);
|
||||||
// Note: We need the timeout here so that Angular can $digest the images change
|
|
||||||
// on the parent scope before the tagChanged callback occurs.
|
|
||||||
$timeout(function() {
|
|
||||||
$scope.tagChanged({
|
|
||||||
'data': { 'added': added, 'removed': removed }
|
|
||||||
});
|
|
||||||
}, 1);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isAnotherImageTag = function(image, tag) {
|
$scope.isAnotherImageTag = function(image, tag) {
|
||||||
if (!$scope.repository || !$scope.imagesInternal) { return; }
|
if (!$scope.repository) { return; }
|
||||||
|
|
||||||
var found = $scope.repository.tags[tag];
|
var found = $scope.repository.tags[tag];
|
||||||
if (found == null) { return false; }
|
if (found == null) { return false; }
|
||||||
|
@ -58,7 +43,7 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isOwnedTag = function(image, tag) {
|
$scope.isOwnedTag = function(image, tag) {
|
||||||
if (!$scope.repository || !$scope.imagesInternal) { return; }
|
if (!$scope.repository) { return; }
|
||||||
|
|
||||||
var found = $scope.repository.tags[tag];
|
var found = $scope.repository.tags[tag];
|
||||||
if (found == null) { return false; }
|
if (found == null) { return false; }
|
||||||
|
@ -149,70 +134,39 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
var lazyLoadImages = function(callback) {
|
|
||||||
if ($scope.imagesInternal.length) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isLoading = true;
|
|
||||||
$timeout(function() {
|
|
||||||
if (isLoading) {
|
|
||||||
$('#loadingImagesModal').modal({});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
var cb = function(images) {
|
|
||||||
isLoading = false;
|
|
||||||
$('#loadingImagesModal').modal('hide');
|
|
||||||
$scope.imagesInternal = images;
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getImages({'callback': cb});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.actionHandler = {
|
$scope.actionHandler = {
|
||||||
'askDeleteTag': function(tag) {
|
'askDeleteTag': function(tag) {
|
||||||
lazyLoadImages(function() {
|
$scope.deleteTagInfo = {
|
||||||
$scope.deleteTagInfo = {
|
'tag': tag
|
||||||
'tag': tag
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'askDeleteMultipleTags': function(tags) {
|
'askDeleteMultipleTags': function(tags) {
|
||||||
lazyLoadImages(function() {
|
$scope.deleteMultipleTagsInfo = {
|
||||||
$scope.deleteMultipleTagsInfo = {
|
'tags': tags
|
||||||
'tags': tags
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'askAddTag': function(image) {
|
'askAddTag': function(image) {
|
||||||
lazyLoadImages(function() {
|
$scope.tagToCreate = '';
|
||||||
$scope.tagToCreate = '';
|
$scope.toTagImage = image;
|
||||||
$scope.toTagImage = image;
|
$scope.addingTag = false;
|
||||||
$scope.addingTag = false;
|
$scope.addTagForm.$setPristine();
|
||||||
$scope.addTagForm.$setPristine();
|
$element.find('#createOrMoveTagModal').modal('show');
|
||||||
$element.find('#createOrMoveTagModal').modal('show');
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'askRevertTag': function(tag, image_id) {
|
'askRevertTag': function(tag, image_id) {
|
||||||
lazyLoadImages(function() {
|
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.revertTagInfo = {
|
||||||
'tag': tag,
|
'tag': tag,
|
||||||
'image_id': image_id
|
'image_id': image_id
|
||||||
};
|
};
|
||||||
|
|
||||||
$element.find('#revertTagModal').modal('show');
|
$element.find('#revertTagModal').modal('show');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,12 @@ angular.module('quay').directive('tagSpecificImagesView', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'tag': '=tag',
|
'tag': '=tag',
|
||||||
'images': '=images',
|
'imageLoader': '=imageLoader',
|
||||||
'imageCutoff': '=imageCutoff'
|
'imageCutoff': '=imageCutoff'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, UtilService) {
|
controller: function($scope, $element, UtilService) {
|
||||||
$scope.getFirstTextLine = UtilService.getFirstMarkdownLineAsText;
|
$scope.getFirstTextLine = UtilService.getFirstMarkdownLineAsText;
|
||||||
|
$scope.loading = false;
|
||||||
$scope.hasImages = false;
|
|
||||||
$scope.tagSpecificImages = [];
|
$scope.tagSpecificImages = [];
|
||||||
|
|
||||||
$scope.getImageListingClasses = function(image) {
|
$scope.getImageListingClasses = function(image) {
|
||||||
|
@ -35,39 +34,8 @@ angular.module('quay').directive('tagSpecificImagesView', function () {
|
||||||
return classes;
|
return classes;
|
||||||
};
|
};
|
||||||
|
|
||||||
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
|
||||||
if (!tag) { return; }
|
|
||||||
|
|
||||||
if (!$scope.imageByDockerId) {
|
|
||||||
$scope.imageByDockerId = [];
|
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
|
||||||
var currentImage = $scope.images[i];
|
|
||||||
$scope.imageByDockerId[currentImage.id] = currentImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tag_image = $scope.imageByDockerId[tag.image_id];
|
|
||||||
if (!tag_image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(tag_image);
|
|
||||||
|
|
||||||
var ancestors = tag_image.ancestors.split('/').reverse();
|
|
||||||
for (var i = 0; i < ancestors.length; ++i) {
|
|
||||||
var image = $scope.imageByDockerId[ancestors[i]];
|
|
||||||
if (image) {
|
|
||||||
if (image == opt_cutoff) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var refresh = function() {
|
var refresh = function() {
|
||||||
if (!$scope.repository || !$scope.tag || !$scope.images) {
|
if (!$scope.repository || !$scope.tag || !$scope.imageLoader) {
|
||||||
$scope.tagSpecificImages = [];
|
$scope.tagSpecificImages = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -78,49 +46,15 @@ angular.module('quay').directive('tagSpecificImagesView', function () {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var getIdsForTag = function(currentTag) {
|
$scope.loading = true;
|
||||||
var ids = {};
|
$scope.imageLoader.getTagSpecificImages($scope.tag, function(images) {
|
||||||
forAllTagImages(currentTag, function(image) {
|
$scope.loading = false;
|
||||||
ids[image.id] = true;
|
$scope.tagSpecificImages = images;
|
||||||
}, $scope.imageCutoff);
|
|
||||||
return ids;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove any IDs that match other tags.
|
|
||||||
var toDelete = getIdsForTag(tag);
|
|
||||||
for (var currentTagName in $scope.repository.tags) {
|
|
||||||
var currentTag = $scope.repository.tags[currentTagName];
|
|
||||||
if (currentTag != tag) {
|
|
||||||
for (var id in getIdsForTag(currentTag)) {
|
|
||||||
delete toDelete[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matching list of images.
|
|
||||||
var images = [];
|
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
|
||||||
var image = $scope.images[i];
|
|
||||||
if (toDelete[image.id]) {
|
|
||||||
images.push(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
images.sort(function(a, b) {
|
|
||||||
var result = new Date(b.created) - new Date(a.created);
|
|
||||||
if (result != 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.sort_index - a.sort_index;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.tagSpecificImages = images;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$watch('repository', refresh);
|
$scope.$watch('repository', refresh);
|
||||||
$scope.$watch('tag', refresh);
|
$scope.$watch('tag', refresh);
|
||||||
$scope.$watch('images', refresh);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
|
@ -31,8 +31,8 @@ var DEPTH_WIDTH = 140;
|
||||||
/**
|
/**
|
||||||
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
||||||
*/
|
*/
|
||||||
function ImageHistoryTree(namespace, name, images, formatComment, formatTime, formatCommand,
|
function ImageHistoryTree(namespace, name, images, getTagsForImage, formatComment, formatTime,
|
||||||
opt_tagFilter) {
|
formatCommand, opt_tagFilter) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The namespace of the repo.
|
* The namespace of the repo.
|
||||||
|
@ -49,6 +49,11 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime, fo
|
||||||
*/
|
*/
|
||||||
this.images_ = images;
|
this.images_ = images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve the tags for an image.
|
||||||
|
*/
|
||||||
|
this.getTagsForImage_ = getTagsForImage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to invoke to format a comment for an image.
|
* Method to invoke to format a comment for an image.
|
||||||
*/
|
*/
|
||||||
|
@ -424,7 +429,7 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
"name": image.id.substr(0, 12),
|
"name": image.id.substr(0, 12),
|
||||||
"children": [],
|
"children": [],
|
||||||
"image": image,
|
"image": image,
|
||||||
"tags": image.tags,
|
"tags": this.getTagsForImage_(image),
|
||||||
"level": null
|
"level": null
|
||||||
};
|
};
|
||||||
imageByDockerId[image.id] = imageNode;
|
imageByDockerId[image.id] = imageNode;
|
||||||
|
@ -663,8 +668,9 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
this.currentImage_ = null;
|
this.currentImage_ = null;
|
||||||
|
|
||||||
// Update the path.
|
// Update the path.
|
||||||
|
var that = this;
|
||||||
var tagImage = this.findImage_(function(image) {
|
var tagImage = this.findImage_(function(image) {
|
||||||
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
|
return tagName && (that.getTagsForImage_(image).indexOf(tagName) >= 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tagImage) {
|
if (tagImage) {
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
})
|
})
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
function RepoViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel) {
|
function RepoViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel, ImageLoaderService) {
|
||||||
$scope.namespace = $routeParams.namespace;
|
$scope.namespace = $routeParams.namespace;
|
||||||
$scope.name = $routeParams.name;
|
$scope.name = $routeParams.name;
|
||||||
|
|
||||||
$scope.imagesRequired = false;
|
var imageLoader = ImageLoaderService.getLoader($scope.namespace, $scope.name);
|
||||||
|
|
||||||
// Tab-enabled counters.
|
// Tab-enabled counters.
|
||||||
$scope.tagsShown = 0;
|
$scope.tagsShown = 0;
|
||||||
|
@ -25,8 +25,7 @@
|
||||||
$scope.viewScope = {
|
$scope.viewScope = {
|
||||||
'selectedTags': [],
|
'selectedTags': [],
|
||||||
'repository': null,
|
'repository': null,
|
||||||
'images': null,
|
'imageLoader': imageLoader,
|
||||||
'imagesResource': null,
|
|
||||||
'builds': null,
|
'builds': null,
|
||||||
'changesVisible': false
|
'changesVisible': false
|
||||||
};
|
};
|
||||||
|
@ -72,17 +71,6 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var loadImages = function(opt_callback) {
|
|
||||||
var params = {
|
|
||||||
'repository': $scope.namespace + '/' + $scope.name
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.viewScope.imagesResource = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
|
|
||||||
$scope.viewScope.images = resp.images;
|
|
||||||
opt_callback && opt_callback(resp.images);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadRepositoryBuilds = function(callback) {
|
var loadRepositoryBuilds = function(callback) {
|
||||||
var params = {
|
var params = {
|
||||||
'repository': $scope.namespace + '/' + $scope.name,
|
'repository': $scope.namespace + '/' + $scope.name,
|
||||||
|
@ -149,14 +137,6 @@
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.requireImages = function() {
|
|
||||||
// Lazily load the repo's images if this is the first call to a tab
|
|
||||||
// that needs the images.
|
|
||||||
if ($scope.viewScope.images == null) {
|
|
||||||
loadImages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.handleChangesState = function(value) {
|
$scope.handleChangesState = function(value) {
|
||||||
$scope.viewScope.changesVisible = value;
|
$scope.viewScope.changesVisible = value;
|
||||||
};
|
};
|
||||||
|
|
110
static/js/services/image-loader-service.js
Normal file
110
static/js/services/image-loader-service.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* Helper service for tracking images needed by tags and caching them.
|
||||||
|
*/
|
||||||
|
angular.module('quay').factory('ImageLoaderService', ['ApiService', function(ApiService) {
|
||||||
|
var imageLoader = function(namespace, name) {
|
||||||
|
this.namespace = namespace;
|
||||||
|
this.name = name;
|
||||||
|
this.tagCache = {};
|
||||||
|
this.images = [];
|
||||||
|
this.imageMap = {};
|
||||||
|
this.imageTagMap = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
imageLoader.prototype.getTagSpecificImages = function(tag, callback) {
|
||||||
|
var errorDisplay = ApiService.errorDisplay('Could not load tag specific images', function() {
|
||||||
|
callback([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': this.namespace + '/' + this.name,
|
||||||
|
'tag': tag,
|
||||||
|
'owned': true
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listTagImages(null, params).then(function(resp) {
|
||||||
|
callback(resp['images']);
|
||||||
|
}, errorDisplay);
|
||||||
|
};
|
||||||
|
|
||||||
|
imageLoader.prototype.getTagsForImage = function(image) {
|
||||||
|
return this.imageTagMap[image.id] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
imageLoader.prototype.registerTagImages_ = function(tag, images) {
|
||||||
|
this.tagCache[tag] = images;
|
||||||
|
|
||||||
|
if (!images.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
images.forEach(function(image) {
|
||||||
|
if (!that.imageMap[image.id]) {
|
||||||
|
that.imageMap[image.id] = image;
|
||||||
|
that.images.push(image);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var rootImage = images[0];
|
||||||
|
if (!this.imageTagMap[rootImage.id]) {
|
||||||
|
this.imageTagMap[rootImage.id] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.imageTagMap[rootImage.id].push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageLoader.prototype.loadImages = function(tags, callback) {
|
||||||
|
var toLoad = [];
|
||||||
|
var that = this;
|
||||||
|
tags.forEach(function(tag) {
|
||||||
|
if (that.tagCache[tag]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toLoad.push(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!toLoad.length) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadImages = function(index) {
|
||||||
|
if (index >= toLoad.length) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag = toLoad[index];
|
||||||
|
var params = {
|
||||||
|
'repository': that.namespace + '/' + that.name,
|
||||||
|
'tag': tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listTagImages(null, params).then(function(resp) {
|
||||||
|
that.registerTagImages_(tag, resp['images']);
|
||||||
|
loadImages(index + 1);
|
||||||
|
}, function() {
|
||||||
|
loadImages(index + 1);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
loadImages(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
imageLoader.prototype.reset = function() {
|
||||||
|
this.tagCache = {};
|
||||||
|
this.images = [];
|
||||||
|
this.imageMap = {};
|
||||||
|
this.imageTagMap = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
var imageLoaderService = {};
|
||||||
|
|
||||||
|
imageLoaderService.getLoader = function(namespace, name) {
|
||||||
|
return new imageLoader(namespace, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return imageLoaderService
|
||||||
|
}]);
|
|
@ -34,8 +34,7 @@
|
||||||
|
|
||||||
<span class="cor-tab" tab-title="Visualize" tab-target="#changes"
|
<span class="cor-tab" tab-title="Visualize" tab-target="#changes"
|
||||||
tab-shown="handleChangesState(true)"
|
tab-shown="handleChangesState(true)"
|
||||||
tab-hidden="handleChangesState(false)"
|
tab-hidden="handleChangesState(false)">
|
||||||
tab-init="requireImages()">
|
|
||||||
<i class="fa fa-code-fork"></i>
|
<i class="fa fa-code-fork"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -64,10 +63,8 @@
|
||||||
<div id="tags" class="tab-pane">
|
<div id="tags" class="tab-pane">
|
||||||
<div class="repo-panel-tags"
|
<div class="repo-panel-tags"
|
||||||
repository="viewScope.repository"
|
repository="viewScope.repository"
|
||||||
images="viewScope.images"
|
image-loader="viewScope.imageLoader"
|
||||||
images-resource="viewScope.imagesResource"
|
|
||||||
selected-tags="viewScope.selectedTags"
|
selected-tags="viewScope.selectedTags"
|
||||||
get-images="getImages(callback)"
|
|
||||||
is-enabled="tagsShown"></div>
|
is-enabled="tagsShown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -83,8 +80,7 @@
|
||||||
<div id="changes" class="tab-pane">
|
<div id="changes" class="tab-pane">
|
||||||
<div class="repo-panel-changes"
|
<div class="repo-panel-changes"
|
||||||
repository="viewScope.repository"
|
repository="viewScope.repository"
|
||||||
images="viewScope.images"
|
image-loader="viewScope.imageLoader"
|
||||||
images-resource="viewScope.imagesResource"
|
|
||||||
selected-tags="viewScope.selectedTags"
|
selected-tags="viewScope.selectedTags"
|
||||||
is-enabled="viewScope.changesVisible"></div>
|
is-enabled="viewScope.changesVisible"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue