Switch to a new single-selected-context layout and system in the view repository screen. Now selecting tags or images changes the context

This commit is contained in:
Joseph Schorr 2014-01-09 18:54:59 -05:00
parent b584d74bf0
commit 7337adf498
4 changed files with 193 additions and 68 deletions

View file

@ -1180,24 +1180,47 @@ p.editable:hover i {
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
} }
.tag-heading:hover .tag-controls {
opacity: 1;
}
.right-title { .right-title {
display: inline-block; display: inline-block;
float: right; float: right;
padding: 4px;
font-size: 12px; font-size: 12px;
color: #aaa; color: #aaa;
} }
.tag-dropdown .tag-count { .right-tag-controls {
display: inline-block;
float: right;
padding: 4px;
padding-left: 10px;
border-left: 1px solid #ccc;
vertical-align: middle;
margin-top: -2px;
margin-right: -10px;
color: #666;
}
.right-tag-controls .tag-count {
display: inline-block; display: inline-block;
margin-left: 4px; margin-left: 4px;
margin-right: 6px; margin-right: 6px;
padding-right: 10px; }
border-right: 1px solid #ccc;
.tag-image-sizes .tag-image-size {
height: 22px;
}
.tag-image-sizes .tag-image-size .size-bar {
display: inline-block;
background: steelblue;
height: 12px;
margin-right: 90px;
}
#current-tag .control-bar {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
text-align: right;
} }
.tag-dropdown a { .tag-dropdown a {
@ -1281,7 +1304,7 @@ p.editable:hover i {
.repo .description { .repo .description {
margin-top: 10px; margin-top: 10px;
margin-bottom: 40px; margin-bottom: 20px;
} }
.repo .empty-message { .repo .empty-message {

View file

@ -151,7 +151,13 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
// Watch for changes to the tag parameter. // Watch for changes to the tag parameter.
$scope.$on('$routeUpdate', function(){ $scope.$on('$routeUpdate', function(){
$scope.setTag($location.search().tag, false); 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 ////////////////////////////////////////// // Start scope methods //////////////////////////////////////////
@ -186,11 +192,28 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
addedDisplayed - removedDisplayed - changedDisplayed; addedDisplayed - removedDisplayed - changedDisplayed;
}; };
$scope.setImage = function(image) { $scope.setImage = function(imageId, opt_updateURL) {
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.currentImage = image;
$scope.loadImageChanges(image); $scope.loadImageChanges(image);
if ($scope.tree) { if ($scope.tree) {
$scope.tree.setImage($scope.currentImage.id); $scope.tree.setImage(image.id);
}
if (opt_updateURL) {
$location.search('tag', null);
$location.search('image', imageId.substr(0, 12));
} }
}; };
@ -205,20 +228,13 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
} }
var getIdsForTag = function(currentTag) { var getIdsForTag = function(currentTag) {
var ancestors = currentTag.image.ancestors.split('/');
var dbid = currentTag.image.dbid;
var ids = {}; var ids = {};
forAllTagImages(currentTag, function(image) {
ids[dbid] = true; ids[image.dbid] = true;
for (var i = 0; i < ancestors.length; ++i) { });
if (ancestors[i]) {
ids[ancestors[i]] = true;
}
}
return ids; return ids;
}; };
// Remove any IDs that match other tags. // Remove any IDs that match other tags.
var toDelete = getIdsForTag(tag); var toDelete = getIdsForTag(tag);
for (var currentTagName in $scope.repo.tags) { for (var currentTagName in $scope.repo.tags) {
@ -252,7 +268,6 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
return images; return images;
}; };
$scope.askDeleteTag = function(tagName) { $scope.askDeleteTag = function(tagName) {
if (!$scope.repo.can_admin) { return; } if (!$scope.repo.can_admin) { return; }
@ -285,6 +300,27 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}); });
}; };
$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.setTag = function(tagName, opt_updateURL) { $scope.setTag = function(tagName, opt_updateURL) {
var repo = $scope.repo; var repo = $scope.repo;
var proposedTag = repo.tags[tagName]; var proposedTag = repo.tags[tagName];
@ -306,6 +342,7 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
} }
if (opt_updateURL) { if (opt_updateURL) {
$location.search('image', null);
$location.search('tag', $scope.currentTag.name); $location.search('tag', $scope.currentTag.name);
} }
} }
@ -387,6 +424,20 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
} }
}; };
var forAllTagImages = function(tag, callback) {
if (!tag || !$scope.imageByDBID) { return; }
callback(tag.image);
var ancestors = tag.image.ancestors.split('/');
for (var i = 0; i < ancestors.length; ++i) {
var image = $scope.imageByDBID[ancestors[i]];
if (image) {
callback(image);
}
}
};
var fetchRepository = function() { var fetchRepository = function() {
var params = {'repository': namespace + '/' + name}; var params = {'repository': namespace + '/' + name};
$rootScope.title = 'Loading Repository...'; $rootScope.title = 'Loading Repository...';
@ -467,6 +518,13 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
$scope.images = resp.images; $scope.images = resp.images;
$scope.specificImages = []; $scope.specificImages = [];
// Build various images for quick lookup of images.
$scope.imageByDBID = {};
for (var i = 0; i < $scope.images.length; ++i) {
var currentImage = $scope.images[i];
$scope.imageByDBID[currentImage.dbid] = currentImage;
}
// Dispose of any existing tree. // Dispose of any existing tree.
if ($scope.tree) { if ($scope.tree) {
$scope.tree.dispose(); $scope.tree.dispose();
@ -489,7 +547,7 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
}); });
$($scope.tree).bind('imageChanged', function(e) { $($scope.tree).bind('imageChanged', function(e) {
$scope.$apply(function() { $scope.setImage(e.image); }); $scope.$apply(function() { $scope.setImage(e.image.id, true); });
}); });
$($scope.tree).bind('showTagMenu', function(e) { $($scope.tree).bind('showTagMenu', function(e) {
@ -500,6 +558,10 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
$scope.$apply(function() { $scope.hideTagMenu(); }); $scope.$apply(function() { $scope.hideTagMenu(); });
}); });
if ($routeParams.image) {
$scope.setImage($routeParams.image);
}
return resp.images; return resp.images;
}); });
}; };
@ -936,7 +998,7 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService) {
}, 10); }, 10);
}; };
var fetchImage = function() { var fetchImages = function() {
var params = { var params = {
'repository': namespace + '/' + name, 'repository': namespace + '/' + name,
'image_id': imageid 'image_id': imageid

View file

@ -40,6 +40,11 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
*/ */
this.currentImage_ = null; this.currentImage_ = null;
/**
* The currently highlighted node (if any).
*/
this.currentNode_ = null;
/** /**
* Counter for creating unique IDs. * Counter for creating unique IDs.
*/ */
@ -231,6 +236,23 @@ ImageHistoryTree.prototype.setImage = function(imageId) {
}; };
/**
* Updates the highlighted path in the tree.
*/
ImageHistoryTree.prototype.setHighlightedPath_ = function(image) {
if (this.currentNode_) {
this.markPath_(this.currentNode_, false);
}
var imageByDBID = this.imageByDBID_;
var currentNode = imageByDBID[image.dbid];
if (currentNode) {
this.markPath_(currentNode, true);
this.currentNode_ = currentNode;
}
};
/** /**
* Returns the ancestors of the given image. * Returns the ancestors of the given image.
*/ */
@ -468,26 +490,15 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
// Save the current tag. // Save the current tag.
var previousTagName = this.currentTag_; var previousTagName = this.currentTag_;
this.currentTag_ = tagName; this.currentTag_ = tagName;
this.currentImage_ = null;
// Update the state of each existing node to no longer be highlighted. // Update the path.
var previousImage = this.findImage_(function(image) { var tagImage = this.findImage_(function(image) {
return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0;
});
if (previousImage) {
var currentNode = imageByDBID[previousImage.dbid];
this.markPath_(currentNode, false);
}
// Find the new current image (if any).
this.currentImage_ = this.findImage_(function(image) {
return image.tags.indexOf(tagName || '(no tag specified)') >= 0; return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
}); });
// Update the state of the new node path. if (tagImage) {
if (this.currentImage_) { this.setHighlightedPath_(tagImage);
var currentNode = imageByDBID[this.currentImage_.dbid];
this.markPath_(currentNode, true);
} }
// Ensure that the children are in the correct order. // Ensure that the children are in the correct order.
@ -531,7 +542,9 @@ ImageHistoryTree.prototype.setImage_ = function(imageId) {
return; return;
} }
this.setHighlightedPath_(newImage);
this.currentImage_ = newImage; this.currentImage_ = newImage;
this.currentTag_ = null;
this.update_(this.root_); this.update_(this.root_);
}; };

View file

@ -53,7 +53,7 @@
content-changed="updateForDescription" field-title="'repository description'"></div> content-changed="updateForDescription" field-title="'repository description'"></div>
<!-- Empty message --> <!-- Empty message -->
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building"> <div class="repo-content" ng-show="!currentTag.image && !currentImage && !repo.is_building">
<div class="empty-message"> <div class="empty-message">
This repository is empty This repository is empty
</div> </div>
@ -72,30 +72,13 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div> </div>
<!-- Content view --> <!-- Content view -->
<div class="repo-content" ng-show="currentTag.image"> <div class="repo-content" ng-show="currentTag.image || currentImage">
<!-- Image History --> <!-- Image History -->
<div id="image-history" style="max-height: 10px;"> <div id="image-history" style="max-height: 10px;">
<div class="row"> <div class="row">
<!-- Tree View container --> <!-- Tree View container -->
<div class="col-md-8"> <div class="col-md-8">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading tag-heading">
<!-- Tag dropdown -->
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li ng-repeat="tag in repo.tags">
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
</li>
</ul>
</div>
<span class="right-title">Tags</span>
<span class="tag-controls" ng-show="repo.can_admin">
<a href="javascript:void(0)" ng-click="askDeleteTag(currentTag.name)">Delete Tag</a>
</span>
</div>
<!-- Image history tree --> <!-- Image history tree -->
<div class="resource-view" resource="imageHistory"> <div class="resource-view" resource="imageHistory">
<div id="image-history-container" onresize="tree.notifyResized()"></div> <div id="image-history-container" onresize="tree.notifyResized()"></div>
@ -107,21 +90,65 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<div class="col-md-4"> <div class="col-md-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<!-- Image dropdown --> <!-- Dropdown -->
<div class="tag-dropdown dropdown" title="Images" bs-tooltip="tooltip.title" data-placement="top"> <div class="tag-dropdown dropdown" data-placement="top">
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.value.length}}</span></i> <i class="fa fa-tag" ng-show="currentTag"></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a> <i class="fa fa-archive" ng-show="!currentTag"></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li ng-repeat="tag in repo.tags">
<a href="javascript:void(0)" ng-click="setTag(tag.name, true)">
<i class="fa fa-tag"></i>{{tag.name}}
</a>
</li>
<li class="divider"></li>
<li ng-repeat="image in imageHistory.value"> <li ng-repeat="image in imageHistory.value">
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a> <a href="javascript:void(0)" ng-click="setImage(image.id, true)">
{{image.id.substr(0, 12)}}
</a>
</li> </li>
</ul> </ul>
</div> </div>
<span class="right-title">Image</span> <span class="right-tag-controls">
<i class="fa fa-tag" title="Tags" bs-tooltip="title">
<span class="tag-count">{{getTagCount(repo)}}</span>
</i>
<i class="fa fa-archive" title="Images" bs-tooltip="title">
<span class="tag-count">{{imageHistory.value.length}}</span>
</i>
</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div id="current-image"> <!-- Current Tag -->
<div id="current-tag" ng-show="currentTag">
<dl class="dl-normal">
<dt>Last Modified</dt>
<dd am-time-ago="parseDate(currentTag.image.created)"></dd>
<dt>Total Compressed Size</dt>
<dd><span class="context-tooltip"
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
bs-tooltip="tooltip.title" data-container="body">{{ getTotalSize(currentTag) | bytes }}</span>
</dd>
</dl>
<div class="tag-image-sizes">
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}"
bs-tooltip="image.size | bytes"></span>
<span class="right-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div>
</div>
<div class="control-bar">
<button class="btn btn-default" ng-click="askDeleteTag(currentTag.name)">
Delete Tag
</button>
</div>
</div>
<!-- Current Image -->
<div id="current-image" ng-show="currentImage && !currentTag">
<div ng-show="currentImage.comment"> <div ng-show="currentImage.comment">
<blockquote style="margin-top: 10px;"> <blockquote style="margin-top: 10px;">
<span class="markdown-view" content="currentImage.comment"></span> <span class="markdown-view" content="currentImage.comment"></span>