Get full actions working in the repo changes tab

This commit is contained in:
Joseph Schorr 2015-03-11 17:46:50 -07:00
parent 89e71b75dd
commit a18148b058
9 changed files with 490 additions and 54 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@
<i class="fa fa-tasks"></i>
</span>
<span class="cor-tab" tab-title="Changes" tab-target="#changes">
<span class="cor-tab" tab-title="Visualize" tab-target="#changes">
<i class="fa fa-code-fork"></i>
</span>
@ -59,7 +59,7 @@
<!-- Changes -->
<div id="changes" class="tab-pane">
changes
<div class="repo-panel-changes" repository="repository"></div>
</div>
<!-- Usage Logs -->