(function() { /** * Repository view page. */ angular.module('quayPages').config(['pages', function(pages) { pages.create('repo-view', 'repo-view.html', RepoCtrl); }]); function RepoCtrl($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 '' + sanitized + ''; }; $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(); } })();