d9c3c6689a
- Start the download of the rest of the repo information after the repo itself loads
660 lines
No EOL
19 KiB
JavaScript
660 lines
No EOL
19 KiB
JavaScript
(function() {
|
|
/**
|
|
* Repository view page.
|
|
*/
|
|
angular.module('quayPages').config(['pages', function(pages) {
|
|
pages.create('repo-view', 'repo-view.html', RepoViewCtrl, {
|
|
'newLayout': true,
|
|
'title': '{{ namespace }}/{{ name }}',
|
|
'description': 'Repository {{ namespace }}/{{ name }}'
|
|
}, ['layout'])
|
|
|
|
pages.create('repo-view', 'old-repo-view.html', OldRepoViewCtrl, {
|
|
}, ['old-layout']);
|
|
}]);
|
|
|
|
function RepoViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel) {
|
|
$scope.namespace = $routeParams.namespace;
|
|
$scope.name = $routeParams.name;
|
|
|
|
$scope.logsShown = 0;
|
|
$scope.viewScope = {
|
|
'selectedTags': [],
|
|
'repository': null,
|
|
'images': null,
|
|
'imagesResource': null,
|
|
'builds': null,
|
|
'changesVisible': false
|
|
};
|
|
|
|
var buildPollChannel = null;
|
|
|
|
// Make sure we track the current user.
|
|
UserService.updateUserIn($scope);
|
|
|
|
// Watch the selected tags and update the URL accordingly.
|
|
$scope.$watch('viewScope.selectedTags', function(selectedTags) {
|
|
if (!selectedTags || !$scope.viewScope.repository) { return; }
|
|
|
|
var tags = filterTags(selectedTags);
|
|
if (!tags.length) {
|
|
$location.search('tag', null);
|
|
return;
|
|
}
|
|
|
|
$location.search('tag', tags.join(','));
|
|
}, true);
|
|
|
|
// Watch the repository to filter any tags removed.
|
|
$scope.$watch('viewScope.repository', function(repository) {
|
|
if (!repository) { return; }
|
|
$scope.viewScope.selectedTags = filterTags($scope.viewScope.selectedTags);
|
|
});
|
|
|
|
var filterTags = function(tags) {
|
|
return (tags || []).filter(function(tag) {
|
|
return !!$scope.viewScope.repository.tags[tag];
|
|
});
|
|
};
|
|
|
|
var loadRepository = function() {
|
|
var params = {
|
|
'repository': $scope.namespace + '/' + $scope.name
|
|
};
|
|
|
|
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
|
$scope.viewScope.repository = repo;
|
|
|
|
// Load the remainder of the data async, so we don't block the initial view from
|
|
// showing.
|
|
$timeout(function() {
|
|
$scope.setTags($routeParams.tag);
|
|
|
|
// Load the images.
|
|
loadImages();
|
|
|
|
// Track builds.
|
|
buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */);
|
|
buildPollChannel.start();
|
|
}, 10);
|
|
});
|
|
};
|
|
|
|
var loadImages = function() {
|
|
var params = {
|
|
'repository': $scope.namespace + '/' + $scope.name
|
|
};
|
|
|
|
$scope.viewScope.imagesResource = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
|
|
$scope.viewScope.images = resp.images;
|
|
});
|
|
};
|
|
|
|
var loadRepositoryBuilds = function(callback) {
|
|
var params = {
|
|
'repository': $scope.namespace + '/' + $scope.name,
|
|
'limit': 3
|
|
};
|
|
|
|
var errorHandler = function() {
|
|
callback(false);
|
|
};
|
|
|
|
$scope.repositoryBuildsResource = ApiService.getRepoBuildsAsResource(params, /* background */true).get(function(resp) {
|
|
$scope.viewScope.builds = resp.builds;
|
|
callback(true);
|
|
}, errorHandler);
|
|
};
|
|
|
|
// Load the repository.
|
|
loadRepository();
|
|
|
|
$scope.setTags = function(tagNames) {
|
|
if (!tagNames) {
|
|
$scope.viewScope.selectedTags = [];
|
|
return;
|
|
}
|
|
|
|
$scope.viewScope.selectedTags = $.unique(tagNames.split(','));
|
|
};
|
|
|
|
$scope.showLogs = function() {
|
|
$scope.logsShown++;
|
|
};
|
|
|
|
$scope.handleChangesState = function(value) {
|
|
$scope.viewScope.changesVisible = value;
|
|
};
|
|
}
|
|
|
|
function OldRepoViewCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config, UtilService) {
|
|
$scope.Config = Config;
|
|
|
|
var namespace = $routeParams.namespace;
|
|
var name = $routeParams.name;
|
|
|
|
$scope.pullCommands = [];
|
|
$scope.currentPullCommand = null;
|
|
|
|
$rootScope.title = 'Loading...';
|
|
|
|
// Watch for the destruction of the scope.
|
|
$scope.$on('$destroy', function() {
|
|
if ($scope.tree) {
|
|
$scope.tree.dispose();
|
|
}
|
|
});
|
|
|
|
// Watch for changes to the repository.
|
|
$scope.$watch('repo', function() {
|
|
if ($scope.tree) {
|
|
$timeout(function() {
|
|
$scope.tree.notifyResized();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Watch for changes to the tag parameter.
|
|
$scope.$on('$routeUpdate', function(){
|
|
if ($location.search().tag) {
|
|
$scope.setTag($location.search().tag, false);
|
|
} else if ($location.search().image) {
|
|
$scope.setImage($location.search().image, false);
|
|
} else {
|
|
$scope.setTag($location.search().tag, false);
|
|
}
|
|
});
|
|
|
|
// Start scope methods //////////////////////////////////////////
|
|
|
|
$scope.buildDialogShowCounter = 0;
|
|
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
|
|
|
$scope.setCurrentPullCommand = function(pullCommand) {
|
|
$scope.currentPullCommand = pullCommand;
|
|
};
|
|
|
|
$scope.updatePullCommand = function() {
|
|
$scope.pullCommands = [];
|
|
|
|
if ($scope.currentTag) {
|
|
$scope.pullCommands.push({
|
|
'title': 'docker pull (Tag ' + $scope.currentTag.name + ')',
|
|
'shortTitle': 'Pull Tag',
|
|
'icon': 'fa-tag',
|
|
'command': 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name + ':' + $scope.currentTag.name
|
|
});
|
|
}
|
|
|
|
$scope.pullCommands.push({
|
|
'title': 'docker pull (Full Repository)',
|
|
'shortTitle': 'Pull Repo',
|
|
'icon': 'fa-code-fork',
|
|
'command': 'docker pull ' + Config.getDomain() + '/' + namespace + '/' + name
|
|
});
|
|
|
|
if ($scope.currentTag) {
|
|
var squash = 'curl -L -f ' + Config.getHost('ACCOUNTNAME:PASSWORDORTOKEN');
|
|
squash += '/c1/squash/' + namespace + '/' + name + '/' + $scope.currentTag.name;
|
|
squash += ' | docker load';
|
|
|
|
$scope.pullCommands.push({
|
|
'title': 'Squashed image (Tag ' + $scope.currentTag.name + ')',
|
|
'shortTitle': 'Squashed',
|
|
'icon': 'fa-file-archive-o',
|
|
'command': squash,
|
|
'experimental': true
|
|
});
|
|
}
|
|
|
|
$scope.currentPullCommand = $scope.pullCommands[0];
|
|
};
|
|
|
|
$scope.showNewBuildDialog = function() {
|
|
$scope.buildDialogShowCounter++;
|
|
};
|
|
|
|
$scope.handleBuildStarted = function(build) {
|
|
getBuildInfo($scope.repo);
|
|
startBuildInfoTimer($scope.repo);
|
|
};
|
|
|
|
$scope.showBuild = function(buildInfo) {
|
|
$location.path('/repository/' + namespace + '/' + name + '/build');
|
|
$location.search('current', buildInfo.id);
|
|
};
|
|
|
|
$scope.isPushing = function(images) {
|
|
if (!images) { return false; }
|
|
|
|
var cached = images.__isPushing;
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
return images.__isPushing = $scope.isPushingInternal(images);
|
|
};
|
|
|
|
$scope.isPushingInternal = function(images) {
|
|
if (!images) { return false; }
|
|
|
|
for (var i = 0; i < images.length; ++i) {
|
|
if (images[i].uploading) { return true; }
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
$scope.getTooltipCommand = function(image) {
|
|
var sanitized = ImageMetadataService.getEscapedFormattedCommand(image);
|
|
return '<span class=\'codetooltip\'>' + sanitized + '</span>';
|
|
};
|
|
|
|
$scope.updateForDescription = function(content) {
|
|
$scope.repo.description = content;
|
|
$scope.repo.put();
|
|
};
|
|
|
|
$scope.parseDate = function(dateString) {
|
|
return Date.parse(dateString);
|
|
};
|
|
|
|
$scope.getTimeSince = function(createdTime) {
|
|
return moment($scope.parseDate(createdTime)).fromNow();
|
|
};
|
|
|
|
$scope.loadImageChanges = function(image) {
|
|
if (!image) { return; }
|
|
|
|
var params = {'repository': namespace + '/' + name, 'image_id': image.id};
|
|
$scope.currentImageChangeResource = ApiService.getImageChangesAsResource(params).get(function(ci) {
|
|
$scope.currentImageChanges = ci;
|
|
});
|
|
};
|
|
|
|
$scope.getMoreCount = function(changes) {
|
|
if (!changes) { return 0; }
|
|
var addedDisplayed = Math.min(2, changes.added.length);
|
|
var removedDisplayed = Math.min(2, changes.removed.length);
|
|
var changedDisplayed = Math.min(2, changes.changed.length);
|
|
|
|
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
|
addedDisplayed - removedDisplayed - changedDisplayed;
|
|
};
|
|
|
|
$scope.showAddTag = function(image) {
|
|
$scope.toTagImage = image;
|
|
$('#addTagModal').modal('show');
|
|
setTimeout(function() {
|
|
$('#tagName').focus();
|
|
}, 500);
|
|
};
|
|
|
|
$scope.isOwnedTag = function(image, tagName) {
|
|
if (!image || !tagName) { return false; }
|
|
return image.tags.indexOf(tagName) >= 0;
|
|
};
|
|
|
|
$scope.isAnotherImageTag = function(image, tagName) {
|
|
if (!image || !tagName) { return false; }
|
|
return image.tags.indexOf(tagName) < 0 && $scope.repo.tags[tagName];
|
|
};
|
|
|
|
$scope.askDeleteTag = function(tagName) {
|
|
if (!$scope.repo.can_admin) { return; }
|
|
|
|
$scope.tagToDelete = tagName;
|
|
$('#confirmdeleteTagModal').modal('show');
|
|
};
|
|
|
|
$scope.findImageForTag = function(tag) {
|
|
return tag && $scope.imageByDockerId && $scope.imageByDockerId[tag.image_id];
|
|
};
|
|
|
|
$scope.createOrMoveTag = function(image, tagName, opt_invalid) {
|
|
if (opt_invalid) { return; }
|
|
|
|
$scope.creatingTag = true;
|
|
|
|
var params = {
|
|
'repository': $scope.repo.namespace + '/' + $scope.repo.name,
|
|
'tag': tagName
|
|
};
|
|
|
|
var data = {
|
|
'image': image.id
|
|
};
|
|
|
|
var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) {
|
|
$('#addTagModal').modal('hide');
|
|
});
|
|
|
|
ApiService.changeTagImage(data, params).then(function(resp) {
|
|
$scope.creatingTag = false;
|
|
loadViewInfo();
|
|
$('#addTagModal').modal('hide');
|
|
}, errorHandler);
|
|
};
|
|
|
|
$scope.deleteTag = function(tagName) {
|
|
if (!$scope.repo.can_admin) { return; }
|
|
|
|
var params = {
|
|
'repository': namespace + '/' + name,
|
|
'tag': tagName
|
|
};
|
|
|
|
var errorHandler = ApiService.errorDisplay('Cannot delete tag', function() {
|
|
$('#confirmdeleteTagModal').modal('hide');
|
|
$scope.deletingTag = false;
|
|
});
|
|
|
|
$scope.deletingTag = true;
|
|
|
|
ApiService.deleteFullTag(null, params).then(function() {
|
|
loadViewInfo();
|
|
$('#confirmdeleteTagModal').modal('hide');
|
|
$scope.deletingTag = false;
|
|
}, errorHandler);
|
|
};
|
|
|
|
$scope.getImagesForTagBySize = function(tag) {
|
|
var images = [];
|
|
forAllTagImages(tag, function(image) {
|
|
images.push(image);
|
|
});
|
|
|
|
images.sort(function(a, b) {
|
|
return b.size - a.size;
|
|
});
|
|
|
|
return images;
|
|
};
|
|
|
|
$scope.getTotalSize = function(tag) {
|
|
var size = 0;
|
|
forAllTagImages(tag, function(image) {
|
|
size += image.size;
|
|
});
|
|
return size;
|
|
};
|
|
|
|
$scope.setImage = function(imageId, opt_updateURL) {
|
|
if (!$scope.images) { return; }
|
|
|
|
var image = null;
|
|
for (var i = 0; i < $scope.images.length; ++i) {
|
|
var currentImage = $scope.images[i];
|
|
if (currentImage.id == imageId || currentImage.id.substr(0, 12) == imageId) {
|
|
image = currentImage;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!image) { return; }
|
|
|
|
$scope.currentTag = null;
|
|
$scope.currentImage = image;
|
|
$scope.loadImageChanges(image);
|
|
if ($scope.tree) {
|
|
$scope.tree.setImage(image.id);
|
|
}
|
|
|
|
if (opt_updateURL) {
|
|
$location.search('tag', null);
|
|
$location.search('image', imageId.substr(0, 12));
|
|
}
|
|
|
|
$scope.updatePullCommand();
|
|
};
|
|
|
|
$scope.setTag = function(tagName, opt_updateURL) {
|
|
var repo = $scope.repo;
|
|
if (!repo) { return; }
|
|
|
|
var proposedTag = repo.tags[tagName];
|
|
if (!proposedTag) {
|
|
// We must find a good default.
|
|
for (tagName in repo.tags) {
|
|
if (!proposedTag || tagName == 'latest') {
|
|
proposedTag = repo.tags[tagName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (proposedTag) {
|
|
$scope.currentTag = proposedTag;
|
|
$scope.currentImage = null;
|
|
|
|
if ($scope.tree) {
|
|
$scope.tree.setTag(proposedTag.name);
|
|
}
|
|
|
|
if (opt_updateURL) {
|
|
$location.search('image', null);
|
|
$location.search('tag', proposedTag.name);
|
|
}
|
|
}
|
|
|
|
if ($scope.currentTag && !repo.tags[$scope.currentTag.name]) {
|
|
$scope.currentTag = null;
|
|
$scope.currentImage = null;
|
|
}
|
|
|
|
$scope.updatePullCommand();
|
|
};
|
|
|
|
$scope.getTagCount = function(repo) {
|
|
if (!repo) { return 0; }
|
|
var count = 0;
|
|
for (var tag in repo.tags) {
|
|
++count;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
$scope.hideTagMenu = function(tagName, clientX, clientY) {
|
|
$scope.currentMenuTag = null;
|
|
|
|
var tagMenu = $("#tagContextMenu");
|
|
tagMenu.hide();
|
|
};
|
|
|
|
$scope.showTagMenu = function(tagName, clientX, clientY) {
|
|
if (!$scope.repo.can_admin) { return; }
|
|
|
|
$scope.currentMenuTag = tagName;
|
|
|
|
var tagMenu = $("#tagContextMenu");
|
|
tagMenu.css({
|
|
display: "block",
|
|
left: clientX,
|
|
top: clientY
|
|
});
|
|
|
|
tagMenu.on("blur", function() {
|
|
setTimeout(function() {
|
|
tagMenu.hide();
|
|
}, 100); // Needed to allow clicking on menu items.
|
|
});
|
|
|
|
tagMenu.on("click", "a", function() {
|
|
setTimeout(function() {
|
|
tagMenu.hide();
|
|
}, 100); // Needed to allow clicking on menu items.
|
|
});
|
|
|
|
tagMenu[0].focus();
|
|
};
|
|
|
|
var getDefaultTag = function() {
|
|
if ($scope.repo === undefined) {
|
|
return undefined;
|
|
} else if ($scope.repo.tags.hasOwnProperty('latest')) {
|
|
return $scope.repo.tags['latest'];
|
|
} else {
|
|
for (key in $scope.repo.tags) {
|
|
return $scope.repo.tags[key];
|
|
}
|
|
}
|
|
};
|
|
|
|
var forAllTagImages = function(tag, callback) {
|
|
if (!tag || !$scope.imageByDockerId) { return; }
|
|
|
|
var tag_image = $scope.imageByDockerId[tag.image_id];
|
|
if (!tag_image) { return; }
|
|
|
|
// Callback the tag's image itself.
|
|
callback(tag_image);
|
|
|
|
// Callback any parent images.
|
|
if (!tag_image.ancestors) { return; }
|
|
var ancestors = tag_image.ancestors.split('/');
|
|
for (var i = 0; i < ancestors.length; ++i) {
|
|
var image = $scope.imageByDockerId[ancestors[i]];
|
|
if (image) {
|
|
callback(image);
|
|
}
|
|
}
|
|
};
|
|
|
|
var fetchRepository = function() {
|
|
var params = {'repository': namespace + '/' + name};
|
|
$rootScope.title = 'Loading Repository...';
|
|
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
|
// Set the repository object.
|
|
$scope.repo = repo;
|
|
|
|
// Set the default tag.
|
|
$scope.setTag($routeParams.tag);
|
|
|
|
// Set the title of the page.
|
|
var qualifiedRepoName = namespace + '/' + name;
|
|
$rootScope.title = qualifiedRepoName;
|
|
var kind = repo.is_public ? 'public' : 'private';
|
|
$rootScope.description = jQuery(UtilService.getFirstMarkdownLineAsText(repo.description)).text() ||
|
|
'Visualization of images and tags for ' + kind + ' Docker repository: ' + qualifiedRepoName;
|
|
|
|
// Load the builds for this repository. If none are active it will cancel the poll.
|
|
startBuildInfoTimer(repo);
|
|
});
|
|
};
|
|
|
|
var startBuildInfoTimer = function(repo) {
|
|
if ($scope.interval) { return; }
|
|
|
|
getBuildInfo(repo);
|
|
$scope.interval = setInterval(function() {
|
|
$scope.$apply(function() { getBuildInfo(repo); });
|
|
}, 5000);
|
|
|
|
$scope.$on("$destroy", function() {
|
|
cancelBuildInfoTimer();
|
|
});
|
|
};
|
|
|
|
var cancelBuildInfoTimer = function() {
|
|
if ($scope.interval) {
|
|
clearInterval($scope.interval);
|
|
}
|
|
};
|
|
|
|
var getBuildInfo = function(repo) {
|
|
var params = {
|
|
'repository': repo.namespace + '/' + repo.name
|
|
};
|
|
|
|
ApiService.getRepoBuilds(null, params, true).then(function(resp) {
|
|
// Build a filtered list of the builds that are currently running.
|
|
var runningBuilds = [];
|
|
for (var i = 0; i < resp.builds.length; ++i) {
|
|
var build = resp.builds[i];
|
|
if (build['phase'] != 'complete' && build['phase'] != 'error') {
|
|
runningBuilds.push(build);
|
|
}
|
|
}
|
|
|
|
var existingBuilds = $scope.runningBuilds || [];
|
|
$scope.runningBuilds = runningBuilds;
|
|
$scope.buildHistory = resp.builds;
|
|
|
|
if (!runningBuilds.length) {
|
|
// Cancel the build timer.
|
|
cancelBuildInfoTimer();
|
|
|
|
// Mark the repo as no longer building.
|
|
$scope.repo.is_building = false;
|
|
|
|
// Reload the repo information if all of the builds recently finished.
|
|
if (existingBuilds.length > 0) {
|
|
loadViewInfo();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
var listImages = function() {
|
|
var params = {'repository': namespace + '/' + name};
|
|
$scope.imageHistory = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
|
|
$scope.images = resp.images;
|
|
$scope.specificImages = [];
|
|
|
|
// Build various images for quick lookup of images.
|
|
$scope.imageByDockerId = {};
|
|
for (var i = 0; i < $scope.images.length; ++i) {
|
|
var currentImage = $scope.images[i];
|
|
$scope.imageByDockerId[currentImage.id] = currentImage;
|
|
}
|
|
|
|
// Dispose of any existing tree.
|
|
if ($scope.tree) {
|
|
$scope.tree.dispose();
|
|
}
|
|
|
|
// Create the new tree.
|
|
var tree = new ImageHistoryTree(namespace, name, resp.images,
|
|
UtilService.getFirstMarkdownLineAsText, $scope.getTimeSince, ImageMetadataService.getEscapedFormattedCommand);
|
|
|
|
$scope.tree = tree.draw('image-history-container');
|
|
if ($scope.tree) {
|
|
// If we already have a tag, use it
|
|
if ($scope.currentTag) {
|
|
$scope.tree.setTag($scope.currentTag.name);
|
|
}
|
|
|
|
// 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, true); });
|
|
});
|
|
|
|
$($scope.tree).bind('imageChanged', function(e) {
|
|
$scope.$apply(function() { $scope.setImage(e.image.id, true); });
|
|
});
|
|
|
|
$($scope.tree).bind('showTagMenu', function(e) {
|
|
$scope.$apply(function() { $scope.showTagMenu(e.tag, e.clientX, e.clientY); });
|
|
});
|
|
|
|
$($scope.tree).bind('hideTagMenu', function(e) {
|
|
$scope.$apply(function() { $scope.hideTagMenu(); });
|
|
});
|
|
}
|
|
|
|
if ($routeParams.image) {
|
|
$scope.setImage($routeParams.image);
|
|
}
|
|
|
|
return resp.images;
|
|
});
|
|
};
|
|
|
|
var loadViewInfo = function() {
|
|
fetchRepository();
|
|
listImages();
|
|
};
|
|
|
|
// Fetch the repository itself as well as the image history.
|
|
loadViewInfo();
|
|
}
|
|
})(); |