(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.repository = 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, 15000 /* 15s */); 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) { // Note: We could just set the builds here, but that causes a full digest cycle. Therefore, // to be more efficient, we do some work here to determine if anything has changed since // the last build load in the common case. if ($scope.viewScope.builds && resp.builds.length == $scope.viewScope.builds.length) { var hasNewInformation = false; for (var i = 0; i < resp.builds.length; ++i) { var current = $scope.viewScope.builds[i]; var updated = resp.builds[i]; if (current.phase != updated.phase || current.id != updated.id) { hasNewInformation = true; break; } } if (!hasNewInformation) { callback(true); return; } } $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() { $timeout(function() { if ($scope.tree) { $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); } $timeout(function() { $scope.tree.notifyResized(); }, 100); return resp.images; }); }; var loadViewInfo = function() { fetchRepository(); listImages(); }; // Fetch the repository itself as well as the image history. loadViewInfo(); } })();