Get full actions working in the repo changes tab
This commit is contained in:
parent
89e71b75dd
commit
a18148b058
9 changed files with 490 additions and 54 deletions
221
static/js/directives/repo-view/repo-panel-changes.js
Normal file
221
static/js/directives/repo-view/repo-panel-changes.js
Normal 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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
34
static/js/directives/ui/image-changes-view.js
Normal file
34
static/js/directives/ui/image-changes-view.js
Normal 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;
|
||||
});
|
32
static/js/directives/ui/image-info-sidebar.js
Normal file
32
static/js/directives/ui/image-info-sidebar.js
Normal 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;
|
||||
});
|
|
@ -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) {
|
||||
|
|
28
static/js/directives/ui/tag-info-sidebar.js
Normal file
28
static/js/directives/ui/tag-info-sidebar.js
Normal 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;
|
||||
});
|
142
static/js/directives/ui/tag-operations-dialog.js
Normal file
142
static/js/directives/ui/tag-operations-dialog.js
Normal 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;
|
||||
});
|
|
@ -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];
|
||||
|
|
|
@ -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 -->
|
||||
|
|
Reference in a new issue