Have tags selected be handled universally throughout the entire repository view page.

This commit is contained in:
Joseph Schorr 2015-03-12 12:22:47 -07:00
parent ea61a68bcb
commit 347bf31f2d
9 changed files with 154 additions and 85 deletions

View file

@ -11,33 +11,29 @@
position: relative; position: relative;
} }
.repo-panel-tags-element .image-track-dot:after { .repo-panel-tags-element .image-track-dot {
content: "\f10c";
font-family: FontAwesome;
display: inline-block; display: inline-block;
position: absolute; position: absolute;
top: 15px; top: 15px;
left: 0px; left: 2px;
width: 17px; width: 12px;
height: 12px;
font-size: 11px;
text-align: center;
background: white; background: white;
z-index: 300; z-index: 300;
height: 13px;
border: 2px solid black;
border-radius: 50%;
} }
.repo-panel-tags-element .image-track-line { .repo-panel-tags-element .image-track-line {
position: absolute; position: absolute;
top: 0px; top: 0px;
bottom: -11px; bottom: -1px;
left: 7px; left: 7px;
width: 0px; width: 0px;
display: inline-block; display: inline-block;
height: 100%;
border-left: 2px solid black; border-left: 2px solid black;
display: none; display: none;

View file

@ -60,5 +60,4 @@
</div> </div>
<div class="tag-operations-dialog" repository="repository" images="images" <div class="tag-operations-dialog" repository="repository" images="images"
action-handler="tagActionHandler" action-handler="tagActionHandler" tag-changed="handleTagChanged(data)"></div>
tag-changed="handleTagChanged(data)"></div>

View file

@ -21,8 +21,9 @@
</span> </span>
<span class="co-checked-actions" ng-if="checkedTags.checked.length"> <span class="co-checked-actions" ng-if="checkedTags.checked.length">
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=changes&tags={{ getCheckedTagsString(checkedTags.checked) }}" <a href="javascript:void(0)" class="btn btn-default" ng-click="setTab('changes')">
class="btn btn-default"><i class="fa fa-code-fork"></i> Visualize</a> <i class="fa fa-code-fork"></i> Visualize
</a>
<button class="btn btn-default" ng-click="askDeleteMultipleTags(checkedTags.checked)"> <button class="btn btn-default" ng-click="askDeleteMultipleTags(checkedTags.checked)">
<i class="fa fa-times"></i> Delete <i class="fa fa-times"></i> Delete
</button> </button>
@ -60,8 +61,9 @@
<td><span am-time-ago="tag.last_modified"></span></td> <td><span am-time-ago="tag.last_modified"></span></td>
<td>{{ tag.size | bytes }}</td> <td>{{ tag.size | bytes }}</td>
<td class="image-id-col">{{ tag.image_id.substr(0, 12) }}</td> <td class="image-id-col">{{ tag.image_id.substr(0, 12) }}</td>
<td class="image-track" ng-repeat="it in imageTracks" ng-style="{'color': it.color}"> <td class="image-track" ng-repeat="it in imageTracks">
<span class="image-track-dot" ng-if="it.image_id == tag.image_id"></span> <span class="image-track-dot" ng-if="it.image_id == tag.image_id"
ng-style="{'borderColor': it.color}"></span>
<span class="image-track-line" ng-class="trackLineClass($parent.$index, it)" <span class="image-track-line" ng-class="trackLineClass($parent.$index, it)"
ng-style="{'borderColor': it.color}"></span> ng-style="{'borderColor': it.color}"></span>
</td> </td>

View file

@ -87,33 +87,32 @@ angular.module('quay').directive('repoPanelChanges', function () {
transclude: false, transclude: false,
restrict: 'C', restrict: 'C',
scope: { scope: {
'repository': '=repository' 'repository': '=repository',
'selectedTags': '=selectedTags',
'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, $location, $timeout, ApiService, UtilService, ImageMetadataService) { controller: function($scope, $element, $timeout, ApiService, UtilService, ImageMetadataService) {
var update = function() { var update = function() {
if (!$scope.repository) { return; } if (!$scope.repository || !$scope.selectedTags) { return; }
var tagString = $location.search()['tags'] || '';
if (!tagString) {
$scope.selectedTags = [];
return;
}
$scope.currentImage = null; $scope.currentImage = null;
$scope.currentImage = null; $scope.currentTag = null;
$scope.selectedTags = tagString.split(',');
if (!$scope.imageResource) { if (!$scope.imagesResource) {
loadImages(); loadImages();
} else {
refreshTree();
} }
}; };
$scope.$on('$routeUpdate', update); $scope.$watch('selectedTags', update)
$scope.$watch('repository', update); $scope.$watch('repository', update);
$scope.$watch('isEnabled', function(isEnabled) {
if (isEnabled) {
refreshTree();
}
});
var refreshTree = function() { var refreshTree = function() {
if (!$scope.repository || !$scope.images) { return; } if (!$scope.repository || !$scope.images) { return; }
@ -157,10 +156,6 @@ angular.module('quay').directive('repoPanelChanges', function () {
$scope.images = resp.images; $scope.images = resp.images;
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.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) { if ($scope.selectedTags && $scope.selectedTags.length) {
refreshTree(); refreshTree();
} }
@ -193,26 +188,16 @@ angular.module('quay').directive('repoPanelChanges', function () {
$scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images); $scope.tracker = new RepositoryImageTracker($scope.repository, $scope.images);
data.removed.map(function(tag) { 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.currentImage = null;
$scope.currentTag = null; $scope.currentTag = null;
}); });
data.added.map(function(tag) { data.added.map(function(tag) {
$scope.selectedTags.push(tag); $scope.selectedTags.push(tag);
$location.search('tags', $scope.selectedTags.join(','));
$scope.currentTag = tag; $scope.currentTag = tag;
}); });
refreshTree();
}; };
} }
}; };

View file

@ -10,9 +10,9 @@ angular.module('quay').directive('repoPanelTags', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
'repository': '=repository', 'repository': '=repository',
'repositoryUpdated': '&repositoryUpdated' 'selectedTags': '=selectedTags'
}, },
controller: function($scope, $element, $filter, ApiService, UIService) { controller: function($scope, $element, $filter, $location, ApiService, UIService) {
var orderBy = $filter('orderBy'); var orderBy = $filter('orderBy');
$scope.checkedTags = UIService.createCheckStateController([]); $scope.checkedTags = UIService.createCheckStateController([]);
@ -35,7 +35,7 @@ angular.module('quay').directive('repoPanelTags', function () {
}; };
var setTagState = function() { var setTagState = function() {
if (!$scope.repository) { return; } if (!$scope.repository || !$scope.selectedTags) { return; }
var tags = []; var tags = [];
var allTags = []; var allTags = [];
@ -60,21 +60,29 @@ angular.module('quay').directive('repoPanelTags', function () {
} }
// Sort the tags by the predicate and the reverse, and map the information. // Sort the tags by the predicate and the reverse, and map the information.
var imageIDs = [];
var ordered = orderBy(tags, $scope.options.predicate, $scope.options.reverse); var ordered = orderBy(tags, $scope.options.predicate, $scope.options.reverse);
var checked = [];
for (var i = 0; i < ordered.length; ++i) { for (var i = 0; i < ordered.length; ++i) {
var tagInfo = ordered[i]; var tagInfo = ordered[i];
if (!imageMap[tagInfo.image_id]) { if (!imageMap[tagInfo.image_id]) {
imageMap[tagInfo.image_id] = []; imageMap[tagInfo.image_id] = [];
imageIDs.push(tagInfo.image_id)
} }
imageMap[tagInfo.image_id].push(tagInfo); imageMap[tagInfo.image_id].push(tagInfo);
if ($.inArray(tagInfo.name, $scope.selectedTags) >= 0) {
checked.push(tagInfo);
}
}; };
// Calculate the image tracks. // Calculate the image tracks.
var colors = d3.scale.category10(); var colors = d3.scale.category10();
var index = 0; var index = 0;
for (var image_id in imageMap) { imageIDs.sort().map(function(image_id) {
if (imageMap[image_id].length >= 2){ if (imageMap[image_id].length >= 2){
imageTracks.push({ imageTracks.push({
'image_id': image_id, 'image_id': image_id,
@ -84,19 +92,34 @@ angular.module('quay').directive('repoPanelTags', function () {
}); });
++index; ++index;
} }
} });
$scope.imageMap = imageMap; $scope.imageMap = imageMap;
$scope.imageTracks = imageTracks; $scope.imageTracks = imageTracks;
$scope.tags = ordered; $scope.tags = ordered;
$scope.checkedTags = UIService.createCheckStateController(ordered);
$scope.allTags = allTags; $scope.allTags = allTags;
$scope.iterationState = {};
$scope.checkedTags = UIService.createCheckStateController(ordered, checked);
$scope.checkedTags.listen(function(checked) {
$scope.selectedTags = checked.map(function(tag_info) {
return tag_info.name;
});
});
} }
$scope.$watch('options.predicate', setTagState); $scope.$watch('options.predicate', setTagState);
$scope.$watch('options.reverse', setTagState); $scope.$watch('options.reverse', setTagState);
$scope.$watch('options.tagFilter', setTagState); $scope.$watch('options.tagFilter', setTagState);
$scope.$watch('selectedTags', function(selectedTags) {
if (!selectedTags || !$scope.repository || !$scope.imageMap) { return; }
$scope.checkedTags.checked = selectedTags.map(function(tag) {
return $scope.repository.tags[tag];
});
}, true);
$scope.$watch('repository', function(repository) { $scope.$watch('repository', function(repository) {
if (!repository) { return; } if (!repository) { return; }
@ -175,10 +198,8 @@ angular.module('quay').directive('repoPanelTags', function () {
return tag.image_id == image_id; return tag.image_id == image_id;
}; };
$scope.getCheckedTagsString = function(checked) { $scope.setTab = function(tab) {
return checked.map(function(tag_info) { $location.search('tab', tab);
return tag_info.name;
}).join(',');
}; };
} }
}; };

View file

@ -30,9 +30,14 @@ angular.module('quay').directive('tagOperationsDialog', function () {
ApiService.listRepositoryImages(null, params).then(function(resp) { ApiService.listRepositoryImages(null, params).then(function(resp) {
$scope.images = resp.images; $scope.images = resp.images;
$scope.tagChanged({
'data': { 'added': added, 'removed': removed } // Note: We need the timeout here so that Angular can $digest the images change
}); // on the parent scope before the tagChanged callback occurs.
$timeout(function() {
$scope.tagChanged({
'data': { 'added': added, 'removed': removed }
});
}, 1);
}) })
}); });
}; };
@ -72,7 +77,6 @@ angular.module('quay').directive('tagOperationsDialog', function () {
ApiService.changeTagImage(data, params).then(function(resp) { ApiService.changeTagImage(data, params).then(function(resp) {
$element.find('#createOrMoveTagModal').modal('hide'); $element.find('#createOrMoveTagModal').modal('hide');
$scope.addingTag = false; $scope.addingTag = false;
markChanged([tag], []); markChanged([tag], []);
}, errorHandler); }, errorHandler);
}; };

View file

@ -13,25 +13,56 @@
}, ['old-layout']); }, ['old-layout']);
}]); }]);
function RepoViewCtrl($scope, $routeParams, ApiService, UserService, AngularPollChannel) { function RepoViewCtrl($scope, $routeParams, $location, ApiService, UserService, AngularPollChannel) {
$scope.namespace = $routeParams.namespace; $scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name; $scope.name = $routeParams.name;
$scope.logsShown = 0; $scope.logsShown = 0;
$scope.viewScope = {
'selectedTags': [],
'repository': null,
'builds': null,
'changesVisible': false
};
var buildPollChannel = null; var buildPollChannel = null;
// Make sure we track the current user. // Make sure we track the current user.
UserService.updateUserIn($scope); 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 loadRepository = function() {
var params = { var params = {
'repository': $scope.namespace + '/' + $scope.name 'repository': $scope.namespace + '/' + $scope.name
}; };
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) { $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo; $scope.viewScope.repository = repo;
$scope.setTag($routeParams.tag); $scope.setTags($routeParams.tag);
// Track builds. // Track builds.
buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */); buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */);
@ -49,7 +80,7 @@
}; };
$scope.repositoryBuildsResource = ApiService.getRepoBuildsAsResource(params, /* background */true).get(function(resp) { $scope.repositoryBuildsResource = ApiService.getRepoBuildsAsResource(params, /* background */true).get(function(resp) {
$scope.builds = resp.builds; $scope.viewScope.builds = resp.builds;
callback(true); callback(true);
}, errorHandler); }, errorHandler);
}; };
@ -57,14 +88,22 @@
// Load the repository. // Load the repository.
loadRepository(); loadRepository();
$scope.setTags = function(tagNames) {
if (!tagNames) {
$scope.viewScope.selectedTags = [];
return;
}
$scope.setTag = function(tagName) { $scope.viewScope.selectedTags = $.unique(tagNames.split(','));
window.console.log('set tag')
}; };
$scope.showLogs = function() { $scope.showLogs = function() {
$scope.logsShown++; $scope.logsShown++;
}; };
$scope.handleChangesState = function(value) {
$scope.viewScope.changesVisible = value;
};
} }
function OldRepoViewCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config, UtilService) { function OldRepoViewCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config, UtilService) {

View file

@ -2,9 +2,14 @@
* Service which provides helper methods for performing some simple UI operations. * Service which provides helper methods for performing some simple UI operations.
*/ */
angular.module('quay').factory('UIService', [function() { angular.module('quay').factory('UIService', [function() {
var CheckStateController = function(items) { var CheckStateController = function(items, opt_checked) {
this.items = items; this.items = items;
this.checked = []; this.checked = opt_checked || [];
this.listeners_ = [];
};
CheckStateController.prototype.listen = function(callback) {
this.listeners_.push(callback);
}; };
CheckStateController.prototype.isChecked = function(item) { CheckStateController.prototype.isChecked = function(item) {
@ -25,22 +30,32 @@ angular.module('quay').factory('UIService', [function() {
} else { } else {
this.checked = this.items.slice(); this.checked = this.items.slice();
} }
this.callListeners_();
}; };
CheckStateController.prototype.checkByFilter = function(filter) { CheckStateController.prototype.checkByFilter = function(filter) {
this.checked = $.grep(this.items, filter); this.checked = $.grep(this.items, filter);
this.callListeners_();
}; };
CheckStateController.prototype.checkItem = function(item) { CheckStateController.prototype.checkItem = function(item) {
this.checked.push(item); this.checked.push(item);
this.callListeners_();
}; };
CheckStateController.prototype.uncheckItem = function(item) { CheckStateController.prototype.uncheckItem = function(item) {
this.checked = $.grep(this.checked, function(cItem) { this.checked = $.grep(this.checked, function(cItem) {
return cItem != item; return cItem != item;
}); });
this.callListeners_();
}; };
CheckStateController.prototype.callListeners_ = function() {
var checked = this.checked;
this.listeners_.map(function(listener) {
listener(checked);
});
};
var uiService = {}; var uiService = {};
@ -73,8 +88,8 @@ angular.module('quay').factory('UIService', [function() {
} }
}; };
uiService.createCheckStateController = function(items) { uiService.createCheckStateController = function(items, opt_checked) {
return new CheckStateController(items); return new CheckStateController(items, opt_checked);
}; };
return uiService; return uiService;

View file

@ -5,9 +5,9 @@
<div class="cor-title"> <div class="cor-title">
<span class="cor-title-link"></span> <span class="cor-title-link"></span>
<span class="cor-title-content"> <span class="cor-title-content">
<span class="repo-circle no-background" repo="repository"></span> <span class="repo-circle no-background" repo="viewScope.repository"></span>
{{ namespace }} / {{ name }} {{ namespace }} / {{ name }}
<span class="repo-star" repository="repository" ng-if="!user.anonymous"></span> <span class="repo-star" repository="viewScope.repository" ng-if="!user.anonymous"></span>
</span> </span>
</div> </div>
@ -25,18 +25,19 @@
<i class="fa fa-tasks"></i> <i class="fa fa-tasks"></i>
</span> </span>
<span class="cor-tab" tab-title="Visualize" tab-target="#changes"> <span class="cor-tab" tab-title="Visualize" tab-target="#changes"
tab-shown="handleChangesState(true)" tab-hidden="handleChangesState(false)">
<i class="fa fa-code-fork"></i> <i class="fa fa-code-fork"></i>
</span> </span>
<!-- Admin Only Tabs --> <!-- Admin Only Tabs -->
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()" <span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
ng-if="repository.can_admin"> ng-if="viewScope.repository.can_admin">
<i class="fa fa-bar-chart"></i> <i class="fa fa-bar-chart"></i>
</span> </span>
<span class="cor-tab" tab-title="Settings" tab-target="#settings" <span class="cor-tab" tab-title="Settings" tab-target="#settings"
ng-if="repository.can_admin"> ng-if="viewScope.repository.can_admin">
<i class="fa fa-gear"></i> <i class="fa fa-gear"></i>
</span> </span>
</div> <!-- /cor-tabs --> </div> <!-- /cor-tabs -->
@ -44,12 +45,16 @@
<div class="cor-tab-content"> <div class="cor-tab-content">
<!-- Information --> <!-- Information -->
<div id="info" class="tab-pane active"> <div id="info" class="tab-pane active">
<div class="repo-panel-info" repository="repository" builds="builds"></div> <div class="repo-panel-info"
repository="viewScope.repository"
builds="viewScope.builds"></div>
</div> </div>
<!-- Tags --> <!-- Tags -->
<div id="tags" class="tab-pane"> <div id="tags" class="tab-pane">
<div class="repo-panel-tags" repository="repository"></div> <div class="repo-panel-tags"
repository="viewScope.repository"
selected-tags="viewScope.selectedTags"></div>
</div> </div>
<!-- Builds --> <!-- Builds -->
@ -59,16 +64,19 @@
<!-- Changes --> <!-- Changes -->
<div id="changes" class="tab-pane"> <div id="changes" class="tab-pane">
<div class="repo-panel-changes" repository="repository"></div> <div class="repo-panel-changes"
repository="viewScope.repository"
selected-tags="viewScope.selectedTags"
is-enabled="viewScope.changesVisible"></div>
</div> </div>
<!-- Usage Logs --> <!-- Usage Logs -->
<div id="logs" class="tab-pane" ng-if="repository.can_admin"> <div id="logs" class="tab-pane" ng-if="viewScope.repository.can_admin">
<div class="logs-view" repository="repository" makevisible="logsShown"></div> <div class="logs-view" repository="viewScope.repository" makevisible="logsShown"></div>
</div> </div>
<!-- Settings --> <!-- Settings -->
<div id="settings" class="tab-pane" ng-if="repository.can_admin"> <div id="settings" class="tab-pane" ng-if="viewScope.repository.can_admin">
settings settings
</div> </div>