From a18148b058205480b3945bce6d7ee1ff509e4577 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 11 Mar 2015 17:46:50 -0700 Subject: [PATCH] Get full actions working in the repo changes tab --- .../repo-view/repo-panel-changes.js | 221 ++++++++++++++++++ .../directives/repo-view/repo-panel-tags.js | 57 +---- static/js/directives/ui/image-changes-view.js | 34 +++ static/js/directives/ui/image-info-sidebar.js | 32 +++ static/js/directives/ui/resource-view.js | 4 + static/js/directives/ui/tag-info-sidebar.js | 28 +++ .../js/directives/ui/tag-operations-dialog.js | 142 +++++++++++ static/js/graphing.js | 22 +- static/partials/repo-view.html | 4 +- 9 files changed, 490 insertions(+), 54 deletions(-) create mode 100644 static/js/directives/repo-view/repo-panel-changes.js create mode 100644 static/js/directives/ui/image-changes-view.js create mode 100644 static/js/directives/ui/image-info-sidebar.js create mode 100644 static/js/directives/ui/tag-info-sidebar.js create mode 100644 static/js/directives/ui/tag-operations-dialog.js diff --git a/static/js/directives/repo-view/repo-panel-changes.js b/static/js/directives/repo-view/repo-panel-changes.js new file mode 100644 index 000000000..c85b19530 --- /dev/null +++ b/static/js/directives/repo-view/repo-panel-changes.js @@ -0,0 +1,221 @@ +/** + * An element which displays the changes visualization panel for a repository view. + */ +angular.module('quay').directive('repoPanelChanges', function () { + var RepositoryImageTracker = function(repository, images) { + this.repository = repository; + this.images = images; + + // Build a map of image ID -> image. + var imageIDMap = {}; + this.images.map(function(image) { + imageIDMap[image.id] = image; + }); + + this.imageMap_ = imageIDMap; + }; + + RepositoryImageTracker.prototype.imageLink = function(image) { + return '/repository/' + this.repository.namespace + '/' + + this.repository.name + '/image/' + image; + }; + + RepositoryImageTracker.prototype.getImageForTag = function(tag) { + var tagData = this.lookupTag(tag); + if (!tagData) { return null; } + + return this.imageMap_[tagData.image_id]; + }; + + RepositoryImageTracker.prototype.lookupTag = function(tag) { + return this.repository.tags[tag]; + }; + + RepositoryImageTracker.prototype.lookupImage = function(image) { + return this.imageMap_[image]; + }; + + RepositoryImageTracker.prototype.forAllTagImages = function(tag, callback) { + var tagData = this.lookupTag(tag); + if (!tagData) { return; } + + var tagImage = this.imageMap_[tagData.image_id]; + if (!tagImage) { return; } + + // Callback the tag's image itself. + callback(tagImage); + + // Callback any parent images. + if (!tagImage.ancestors) { return; } + + var ancestors = tagImage.ancestors.split('/'); + for (var i = 0; i < ancestors.length; ++i) { + var image = this.imageMap_[ancestors[i]]; + if (image) { + callback(image); + } + } + }; + + RepositoryImageTracker.prototype.getTotalSize = function(tag) { + var size = 0; + this.forAllTagImages(tag, function(image) { + size += image.size; + }); + return size; + }; + + RepositoryImageTracker.prototype.getImagesForTagBySize = function(tag) { + var images = []; + this.forAllTagImages(tag, function(image) { + images.push(image); + }); + + images.sort(function(a, b) { + return b.size - a.size; + }); + + return images; + }; + + /////////////////////////////////////////////////////////////////////////////////////// + + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/repo-view/repo-panel-changes.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository' + }, + controller: function($scope, $element, $location, $timeout, ApiService, UtilService, ImageMetadataService) { + + var update = function() { + if (!$scope.repository) { return; } + + var tagString = $location.search()['tags'] || ''; + if (!tagString) { + $scope.selectedTags = []; + return; + } + + $scope.currentImage = null; + $scope.currentImage = null; + $scope.selectedTags = tagString.split(','); + + if (!$scope.imageResource) { + loadImages(); + } else { + refreshTree(); + } + }; + + $scope.$on('$routeUpdate', update); + $scope.$watch('repository', update); + + var refreshTree = function() { + if (!$scope.repository || !$scope.images) { return; } + + $('#image-history-container').empty(); + + var tree = new ImageHistoryTree( + $scope.repository.namespace, + $scope.repository.name, + $scope.images, + UtilService.getFirstMarkdownLineAsText, + $scope.getTimeSince, + ImageMetadataService.getEscapedFormattedCommand, + function(tag) { + return $.inArray(tag, $scope.selectedTags) >= 0; + }); + + $scope.tree = tree.draw('image-history-container'); + if ($scope.tree) { + // Give enough time for the UI to be drawn before we resize the tree. + $timeout(function() { + $scope.tree.notifyResized(); + }, 100); + + // 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); }); + }); + + $($scope.tree).bind('imageChanged', function(e) { + $scope.$apply(function() { $scope.setImage(e.image.id); }); + }); + } + }; + + var loadImages = function(opt_callback) { + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name + }; + + $scope.imagesResource = ApiService.listRepositoryImagesAsResource(params).get(function(resp) { + $scope.images = resp.images; + $scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images); + + $scope.selectedTags = $.grep($scope.selectedTags, function(tag) { + return !!$scope.tracker.lookupTag(tag); + }); + + if ($scope.selectedTags && $scope.selectedTags.length) { + refreshTree(); + } + + opt_callback && opt_callback(); + }); + }; + + $scope.setImage = function(image_id) { + $scope.currentTag = null; + $scope.currentImage = image_id; + $scope.tree.setImage(image_id); + }; + + $scope.setTag = function(tag) { + $scope.currentTag = tag; + $scope.currentImage = null; + $scope.tree.setTag(tag); + }; + + $scope.parseDate = function(dateString) { + return Date.parse(dateString); + }; + + $scope.getTimeSince = function(createdTime) { + return moment($scope.parseDate(createdTime)).fromNow(); + }; + + $scope.handleTagChanged = function(data) { + $scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images); + + data.removed.map(function(tag) { + $scope.selectedTags = $.grep($scope.selectedTags, function(cTag) { + return cTag != tag; + }); + + if ($scope.selectedTags.length) { + $location.search('tags', $scope.selectedTags.join(',')); + } else { + $location.search('tags', null); + } + + $scope.currentImage = null; + $scope.currentTag = null; + }); + + data.added.map(function(tag) { + $scope.selectedTags.push(tag); + $location.search('tags', $scope.selectedTags.join(',')); + + $scope.currentTag = tag; + }); + }; + } + }; + return directiveDefinitionObject; +}); + diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js index 8a5cbea90..9934eed18 100644 --- a/static/js/directives/repo-view/repo-panel-tags.js +++ b/static/js/directives/repo-view/repo-panel-tags.js @@ -22,6 +22,7 @@ angular.module('quay').directive('repoPanelTags', function () { }; $scope.iterationState = {}; + $scope.tagActionHandler = null; var loadImages = function() { var params = { @@ -140,57 +141,11 @@ angular.module('quay').directive('repoPanelTags', function () { }; $scope.askDeleteTag = function(tag) { - $scope.deleteTagInfo = { - 'tag': tag - }; + $scope.tagActionHandler.askDeleteTag(tag); }; $scope.askDeleteMultipleTags = function(tags) { - $scope.deleteMultipleTagsInfo = { - 'tags': tags - }; - }; - - $scope.deleteMultipleTags = function(tags, callback) { - var count = tags.length; - var perform = function(index) { - if (index >= count) { - callback(true); - return; - } - - var tag_info = tags[index]; - $scope.deleteTag(tag_info.name, function(result) { - if (!result) { - callback(false); - return; - } - - perform(index + 1); - }); - }; - - perform(0); - }; - - $scope.deleteTag = function(tag, callback) { - if (!$scope.repository.can_admin) { return; } - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'tag': tag - }; - - var errorHandler = ApiService.errorDisplay('Cannot delete tag', callback); - ApiService.deleteFullTag(null, params).then(function() { - callback(true); - $scope.tags = $.grep($scope.tags, function(tagInfo) { - return tagInfo.name != tag; - }); - - $scope.checkedTags = UIService.createCheckStateController($scope.tags); - $scope.repositoryUpdated({}); - }, errorHandler); + $scope.tagActionHandler.askDeleteMultipleTags(tags); }; $scope.orderBy = function(predicate) { @@ -219,6 +174,12 @@ angular.module('quay').directive('repoPanelTags', function () { $scope.imageIDFilter = function(image_id, tag) { return tag.image_id == image_id; }; + + $scope.getCheckedTagsString = function(checked) { + return checked.map(function(tag_info) { + return tag_info.name; + }).join(','); + }; } }; return directiveDefinitionObject; diff --git a/static/js/directives/ui/image-changes-view.js b/static/js/directives/ui/image-changes-view.js new file mode 100644 index 000000000..997473c2c --- /dev/null +++ b/static/js/directives/ui/image-changes-view.js @@ -0,0 +1,34 @@ +/** + * An element which loads and displays UI representing the changes made in an image. + */ +angular.module('quay').directive('imageChangesView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/image-changes-view.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository', + 'image': '=image', + 'hasChanges': '=hasChanges' + }, + controller: function($scope, $element, ApiService) { + $scope.$watch('image', function() { + if (!$scope.image || !$scope.repository) { return; } + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'image_id': $scope.image + }; + + $scope.hasChanges = true; + $scope.imageChangesResource = ApiService.getImageChangesAsResource(params).get(function(resp) { + $scope.changeData = resp; + $scope.hasChanges = resp.added.length || resp.removed.length || resp.changed.length; + }); + }); + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/directives/ui/image-info-sidebar.js b/static/js/directives/ui/image-info-sidebar.js new file mode 100644 index 000000000..2c254a43e --- /dev/null +++ b/static/js/directives/ui/image-info-sidebar.js @@ -0,0 +1,32 @@ +/** + * An element which displays sidebar information for a image. Primarily used in the repo view. + */ +angular.module('quay').directive('imageInfoSidebar', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/image-info-sidebar.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'tracker': '=tracker', + 'image': '=image', + + 'tagSelected': '&tagSelected', + 'addTagRequested': '&addTagRequested' + }, + controller: function($scope, $element, ImageMetadataService) { + $scope.$watch('image', function(image) { + if (!image || !$scope.tracker) { return; } + $scope.imageData = $scope.tracker.lookupImage(image); + }); + + $scope.parseDate = function(dateString) { + return Date.parse(dateString); + }; + + $scope.getFormattedCommand = ImageMetadataService.getFormattedCommand; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/resource-view.js b/static/js/directives/ui/resource-view.js index e423cd003..7295d3e6b 100644 --- a/static/js/directives/ui/resource-view.js +++ b/static/js/directives/ui/resource-view.js @@ -21,6 +21,10 @@ angular.module('quay').directive('resourceView', function () { } var resources = $scope.resources || [$scope.resource]; + if (!resources.length) { + return 'loading'; + } + for (var i = 0; i < resources.length; ++i) { var current = resources[i]; if (current.loading) { diff --git a/static/js/directives/ui/tag-info-sidebar.js b/static/js/directives/ui/tag-info-sidebar.js new file mode 100644 index 000000000..3e029e805 --- /dev/null +++ b/static/js/directives/ui/tag-info-sidebar.js @@ -0,0 +1,28 @@ +/** + * An element which displays sidebar information for a tag. Primarily used in the repo view. + */ +angular.module('quay').directive('tagInfoSidebar', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/tag-info-sidebar.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'tracker': '=tracker', + 'tag': '=tag', + + 'imageSelected': '&imageSelected', + 'deleteTagRequested': '&deleteTagRequested' + }, + controller: function($scope, $element) { + $scope.$watch('tag', function(tag) { + if (!tag || !$scope.tracker) { return; } + + $scope.tagImage = $scope.tracker.getImageForTag(tag); + $scope.tagData = $scope.tracker.lookupTag(tag); + }); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/tag-operations-dialog.js b/static/js/directives/ui/tag-operations-dialog.js new file mode 100644 index 000000000..5deb538ef --- /dev/null +++ b/static/js/directives/ui/tag-operations-dialog.js @@ -0,0 +1,142 @@ +/** + * An element which adds a series of dialogs for performing operations for tags (adding, moving + * deleting). + */ +angular.module('quay').directive('tagOperationsDialog', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/tag-operations-dialog.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository', + 'images': '=images', + 'actionHandler': '=actionHandler', + + 'tagChanged': '&tagChanged' + }, + controller: function($scope, $element, $timeout, ApiService) { + $scope.addingTag = false; + + var markChanged = function(added, removed) { + // Reload the repository and the images. + $scope.repository.get().then(function(resp) { + $scope.repository = resp; + + var params = { + 'repository': resp.namespace + '/' + resp.name + }; + + ApiService.listRepositoryImages(null, params).then(function(resp) { + $scope.images = resp.images; + $scope.tagChanged({ + 'data': { 'added': added, 'removed': removed } + }); + }) + }); + }; + + $scope.isAnotherImageTag = function(image, tag) { + if (!$scope.repository || !$scope.images) { return; } + + var found = $scope.repository.tags[tag]; + if (found == null) { return false; } + return found.image_id != image; + }; + + $scope.isOwnedTag = function(image, tag) { + if (!$scope.repository || !$scope.images) { return; } + + var found = $scope.repository.tags[tag]; + if (found == null) { return false; } + return found.image_id == image; + }; + + $scope.createOrMoveTag = function(image, tag) { + $scope.addingTag = true; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'tag': tag + }; + + var data = { + 'image': image + }; + + var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) { + $element.find('#createOrMoveTagModal').modal('hide'); + }); + + ApiService.changeTagImage(data, params).then(function(resp) { + $element.find('#createOrMoveTagModal').modal('hide'); + $scope.addingTag = false; + + markChanged([tag], []); + }, errorHandler); + }; + + $scope.deleteMultipleTags = function(tags, callback) { + var count = tags.length; + var perform = function(index) { + if (index >= count) { + callback(true); + markChanged([], tags); + return; + } + + var tag_info = tags[index]; + $scope.deleteTag(tag_info.name, function(result) { + if (!result) { + callback(false); + return; + } + + perform(index + 1); + }, true); + }; + + perform(0); + }; + + $scope.deleteTag = function(tag, callback, opt_skipmarking) { + if (!$scope.repository.can_admin) { return; } + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'tag': tag + }; + + var errorHandler = ApiService.errorDisplay('Cannot delete tag', callback); + ApiService.deleteFullTag(null, params).then(function() { + callback(true); + !opt_skipmarking && markChanged([], [tag]); + }, errorHandler); + }; + + $scope.actionHandler = { + 'askDeleteTag': function(tag) { + $scope.deleteTagInfo = { + 'tag': tag + }; + }, + + 'askDeleteMultipleTags': function(tags) { + $scope.deleteMultipleTagsInfo = { + 'tags': tags + }; + }, + + 'askAddTag': function(image) { + $scope.tagToCreate = ''; + $scope.toTagImage = image; + $scope.addingTag = false; + $scope.addTagForm.$setPristine(); + $element.find('#createOrMoveTagModal').modal('show'); + } + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/graphing.js b/static/js/graphing.js index 60e7d1b33..cbb4fe0bc 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -31,7 +31,9 @@ var DEPTH_WIDTH = 140; /** * 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, formatComment, formatTime, formatCommand, + opt_tagFilter) { + /** * The namespace of the repo. */ @@ -62,6 +64,11 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime, fo */ this.formatCommand_ = formatCommand; + /** + * Method for filtering the tags and image paths displayed in the tree. + */ + this.tagFilter_ = opt_tagFilter || function() { return true; }; + /** * The current tag (if any). */ @@ -153,7 +160,7 @@ ImageHistoryTree.prototype.updateDimensions_ = function() { $('#' + container).removeOverscroll(); var viewportHeight = $(window).height(); var boundingBox = document.getElementById(container).getBoundingClientRect(); - document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 150) + 'px'; + document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 100) + 'px'; this.setupOverscroll_(); @@ -526,7 +533,14 @@ ImageHistoryTree.prototype.pruneUnreferenced_ = function(node) { return node.children.length == 0; } - return (node.children.length == 0 && node.tags.length == 0); + var tags = []; + for (var i = 0; i < node.tags.length; ++i) { + if (this.tagFilter_(node.tags[i])) { + tags.push(node.tags[i]); + } + } + + return (node.children.length == 0 && tags.length == 0); }; @@ -554,7 +568,7 @@ ImageHistoryTree.prototype.collapseNodes_ = function(node) { var current = node; var previous = node; var encountered = []; - while (current.children.length == 1) { + while (current.children.length == 1 && current.tags.length == 0) { encountered.push(current); previous = current; current = current.children[0]; diff --git a/static/partials/repo-view.html b/static/partials/repo-view.html index e86fb19bb..c7fdb5b54 100644 --- a/static/partials/repo-view.html +++ b/static/partials/repo-view.html @@ -25,7 +25,7 @@ - + @@ -59,7 +59,7 @@
- changes +