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:
Joseph Schorr 2015-11-03 18:15:23 -05:00
parent 43720b27e7
commit 4f41f79fa8
15 changed files with 268 additions and 254 deletions

View file

@ -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)]
} }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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();
}; };
} }
}; };

View file

@ -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',

View file

@ -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;
} }
}; };

View file

@ -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');
});
} }
}; };
} }

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}; };

View 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
}]);

View file

@ -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>