From 50929102b5b9c12fe342bace54d1949f03f5ec1a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 16 Oct 2013 22:33:35 -0400 Subject: [PATCH 01/15] Have the image tree allow a state with no selected image nor so selected tag. --- static/js/controllers.js | 2 +- static/js/graphing.js | 58 +++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 534ce6020..b103a4255 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -302,7 +302,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image'); imageFetch.get().then(function(resp) { $scope.imageHistory = resp.images; - $scope.tree = new ImageHistoryTree(namespace, name, resp.images, $scope.currentTag, + $scope.tree = new ImageHistoryTree(namespace, name, resp.images, $scope.getCommentFirstLine, $scope.getTimeSince); $scope.tree.draw('image-history-container'); diff --git a/static/js/graphing.js b/static/js/graphing.js index f05943ae4..95d76d1b4 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -4,7 +4,7 @@ var DEPTH_WIDTH = 132; /** * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock) */ -function ImageHistoryTree(namespace, name, images, current, formatComment, formatTime) { +function ImageHistoryTree(namespace, name, images, formatComment, formatTime) { /** * The namespace of the repo. */ @@ -20,21 +20,6 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma */ this.images_ = images; - /** - * The current tag. - */ - this.currentTag_ = current.name; - - /** - * The current image. - */ - this.currentImage_ = current.image; - - /** - * Counter for creating unique IDs. - */ - this.idCounter_ = 0; - /** * Method to invoke to format a comment for an image. */ @@ -44,6 +29,21 @@ function ImageHistoryTree(namespace, name, images, current, formatComment, forma * Method to invoke to format the time for an image. */ this.formatTime_ = formatTime; + + /** + * The current tag (if any). + */ + this.currentTag_ = null; + + /** + * The current image (if any). + */ + this.currentImage_ = null; + + /** + * Counter for creating unique IDs. + */ + this.idCounter_ = 0; } @@ -361,21 +361,27 @@ ImageHistoryTree.prototype.markPath_ = function(startingNode, isHighlighted) { * Sets the current tag displayed in the tree. */ ImageHistoryTree.prototype.setTag_ = function(tagName) { + // Save the current tag. this.currentTag_ = tagName; - // Update the state of each existing node to no longer be highlighted. var imageByDBID = this.imageByDBID_; - var currentNode = imageByDBID[this.currentImage_.dbid]; - this.markPath_(currentNode, false); - // Find the new current image. + // Update the state of each existing node to no longer be highlighted. + if (this.currentImage_) { + var currentNode = imageByDBID[this.currentImage_.dbid]; + this.markPath_(currentNode, false); + } + + // Find the new current image (if any). this.currentImage_ = this.findImage_(function(image) { - return image.tags.indexOf(tagName) >= 0; + return image.tags.indexOf(tagName || '(no tag specified)') >= 0; }); // Update the state of the new node path. - currentNode = imageByDBID[this.currentImage_.dbid]; - this.markPath_(currentNode, true); + if (this.currentImage_) { + var currentNode = imageByDBID[this.currentImage_.dbid]; + this.markPath_(currentNode, true); + } // Ensure that the children are in the correct order. for (var i = 0; i < this.images_.length; ++i) { @@ -432,6 +438,7 @@ ImageHistoryTree.prototype.update_ = function(source) { var diagonal = this.diagonal_; var tip = this.tip_; var currentTag = this.currentTag_; + var currentImage = this.currentImage_; var repoNamespace = this.repoNamespace_; var repoName = this.repoName_; var maxHeight = this.maxHeight_; @@ -520,7 +527,10 @@ ImageHistoryTree.prototype.update_ = function(source) { if (d.collapsed) { return 'collapsed'; } - return d.image.id == that.currentImage_.id ? 'current' : ''; + if (!currentImage) { + return ''; + } + return d.image.id == currentImage.id ? 'current' : ''; }); // Ensure that the node is visible. From d6e89f76ad0965d8a964cf99b15e012c3ecb3c65 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 16 Oct 2013 22:37:29 -0400 Subject: [PATCH 02/15] Select a sane default tag. --- initdb.py | 9 ++++++-- static/js/controllers.js | 46 +++++++++++++++++++++++++++++---------- test.db | Bin 69632 -> 69632 bytes 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/initdb.py b/initdb.py index b21acdcad..74853bdc0 100644 --- a/initdb.py +++ b/initdb.py @@ -52,7 +52,8 @@ def create_subtree(repo, structure, parent): create_subtree(repo, subtree, new_image) -def __generate_repository(user, name, description, is_public, permissions, structure): +def __generate_repository(user, name, description, is_public, permissions, + structure): repo = model.create_repository(user.username, name, user) if is_public: @@ -92,7 +93,7 @@ if __name__ == '__main__': 'Complex repository with many branches and tags.', False, [(new_user_2, 'read')], (2, [(3, [], 'v2.0'), - (1, [(1, [(1, [], ['latest', 'prod'])], + (1, [(1, [(1, [], ['prod'])], 'staging'), (1, [], None)], None)], None)) @@ -113,3 +114,7 @@ if __name__ == '__main__': __generate_repository(new_user_1, 'shared', 'Shared repository, another user can write.', False, [(new_user_2, 'write')], (5, [], 'latest')) + + __generate_repository(new_user_1, 'empty', + 'Empty repository with no images or tags.', False, + [], (0, [], None)) \ No newline at end of file diff --git a/static/js/controllers.js b/static/js/controllers.js index 534ce6020..87620e06e 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -295,11 +295,23 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { if (!string) { return ''; } return getMarkedDown(string); }; - - $scope.listImages = function() { + + var getDefaultTag = function() { + if ($scope.repo === undefined) { + return undefined; + } else if ($scope.repo.tags.hasOwnProperty('latest')) { + return $scope.repo.tags['latest']; + } else { + for (key in $scope.repo.tags) { + return $scope.repo.tags[key]; + } + } + }; + + var listImages = function() { if ($scope.imageHistory) { return; } - var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image'); + var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/'); imageFetch.get().then(function(resp) { $scope.imageHistory = resp.images; $scope.tree = new ImageHistoryTree(namespace, name, resp.images, $scope.currentTag, @@ -324,10 +336,23 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.setTag = function(tagName) { var repo = $scope.repo; - $scope.currentTag = repo.tags[tagName] || repo.tags['latest']; - $scope.currentImage = $scope.currentTag.image; - if ($scope.tree) { - $scope.tree.setTag($scope.currentTag.name); + + var proposedTag = repo.tags[tagName]; + if (!proposedTag) { + // We must find a good default + for (tagName in repo.tags) { + if (!proposedTag || tagName == 'latest') { + proposedTag = repo.tags[tagName]; + } + } + } + + if (proposedTag) { + $scope.currentTag = repo.tags[tagName] || repo.tags['latest']; + $scope.currentImage = $scope.currentTag.image; + if ($scope.tree) { + $scope.tree.setTag($scope.currentTag.name); + } } }; @@ -342,7 +367,6 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { var namespace = $routeParams.namespace; var name = $routeParams.name; - var tag = $routeParams.tag || 'latest'; $scope.loading = true; @@ -351,8 +375,8 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { repositoryFetch.get().then(function(repo) { $rootScope.title = namespace + '/' + name; $scope.repo = repo; - $scope.currentTag = repo.tags[tag] || repo.tags['latest']; - $scope.setImage($scope.currentTag.image); + + $scope.setTag($routeParams.tag); var clip = new ZeroClipboard($('#copyClipboard'), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); clip.on('complete', function() { @@ -374,7 +398,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { }); // Fetch the image history. - $scope.listImages(); + listImages(); } function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { diff --git a/test.db b/test.db index 179b19de4a664e47ed3df83801c4a01a53cc08fb..c5620a9ba68be66202efa7ea2de0d120b0cc26f6 100644 GIT binary patch literal 69632 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 From 0c3c1b9e0e3d672d4e01bb2be2506881e601e748 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 16 Oct 2013 22:42:35 -0400 Subject: [PATCH 03/15] Properly handle empty repos --- static/js/graphing.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index 95d76d1b4..bdd821679 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -101,6 +101,10 @@ ImageHistoryTree.prototype.draw = function(container) { return html; } + if (!d.image) { + return '(This repository is empty)'; + } + if (d.image.comment) { html += '' + formatComment(d.image.comment) + ''; } @@ -198,7 +202,7 @@ ImageHistoryTree.prototype.buildRoot_ = function() { // "name": "...", // "children": [...] // } - var formatted = {}; + var formatted = {"name": "No images found"}; // Build a node for each image. var imageByDBID = {}; @@ -266,8 +270,10 @@ ImageHistoryTree.prototype.buildRoot_ = function() { */ ImageHistoryTree.prototype.determineMaximumHeight_ = function(node) { var maxHeight = 0; - for (var i = 0; i < node.children.length; ++i) { - maxHeight = Math.max(this.determineMaximumHeight_(node.children[i]), maxHeight); + if (node.children) { + for (var i = 0; i < node.children.length; ++i) { + maxHeight = Math.max(this.determineMaximumHeight_(node.children[i]), maxHeight); + } } return maxHeight + 1; }; @@ -476,7 +482,7 @@ ImageHistoryTree.prototype.update_ = function(source) { .attr("dy", ".35em") .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.name; }) - .on("click", function(d) { that.changeImage_(d.image.id); }) + .on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } }) .on('mouseover', tip.show) .on('mouseout', tip.hide); From 8987f32d9de413fd35577a37a500b6dbf6520eb5 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 16 Oct 2013 22:44:29 -0400 Subject: [PATCH 04/15] Fix some tabs. --- static/js/graphing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index 95d76d1b4..39adc9aa3 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -368,8 +368,8 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) { // Update the state of each existing node to no longer be highlighted. if (this.currentImage_) { - var currentNode = imageByDBID[this.currentImage_.dbid]; - this.markPath_(currentNode, false); + var currentNode = imageByDBID[this.currentImage_.dbid]; + this.markPath_(currentNode, false); } // Find the new current image (if any). @@ -379,8 +379,8 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) { // Update the state of the new node path. if (this.currentImage_) { - var currentNode = imageByDBID[this.currentImage_.dbid]; - this.markPath_(currentNode, true); + var currentNode = imageByDBID[this.currentImage_.dbid]; + this.markPath_(currentNode, true); } // Ensure that the children are in the correct order. From 98cb902cf8d1c20d93fd5fbc09954312b15fd1c1 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 16 Oct 2013 22:44:44 -0400 Subject: [PATCH 05/15] Actually set the proposed tag to the current tag. --- static/js/controllers.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 807b9c070..5a0892068 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -317,6 +317,11 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.tree = new ImageHistoryTree(namespace, name, resp.images, $scope.getCommentFirstLine, $scope.getTimeSince); + // If we already have a tag, use it + if ($scope.currentTag) { + $scope.tree.setTag($scope.currentTag.name); + } + $scope.tree.draw('image-history-container'); $($scope.tree).bind('tagChanged', function(e) { $scope.$apply(function() { $scope.setTag(e.tag); }); @@ -348,7 +353,8 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { } if (proposedTag) { - $scope.currentTag = repo.tags[tagName] || repo.tags['latest']; + console.log('Setting tag to: ' + proposedTag.name); + $scope.currentTag = proposedTag; $scope.currentImage = $scope.currentTag.image; if ($scope.tree) { $scope.tree.setTag($scope.currentTag.name); From ace813d88dc49ea99f7a7a0ec3034a5620398855 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 16 Oct 2013 22:49:37 -0400 Subject: [PATCH 06/15] Call setTag in the right place. Remove spurious console logging. --- static/js/controllers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 5a0892068..ae9ef738c 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -317,12 +317,13 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.tree = new ImageHistoryTree(namespace, name, resp.images, $scope.getCommentFirstLine, $scope.getTimeSince); + $scope.tree.draw('image-history-container'); + // If we already have a tag, use it if ($scope.currentTag) { $scope.tree.setTag($scope.currentTag.name); } - $scope.tree.draw('image-history-container'); $($scope.tree).bind('tagChanged', function(e) { $scope.$apply(function() { $scope.setTag(e.tag); }); }); @@ -353,7 +354,6 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { } if (proposedTag) { - console.log('Setting tag to: ' + proposedTag.name); $scope.currentTag = proposedTag; $scope.currentImage = $scope.currentTag.image; if ($scope.tree) { From f798345d1c7d48847f2c4472e626a9ad1f6cc457 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 16 Oct 2013 23:09:43 -0400 Subject: [PATCH 07/15] Have the view repo page's URL be updated for the currently selected tag automatically. --- static/js/app.js | 2 +- static/js/controllers.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index e9e9f39cc..031b82c3c 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -129,7 +129,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', // index rule to make sure that deep links directly deep into the app continue to work. // WARNING WARNING WARNING $routeProvider. - when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). + when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}). when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). diff --git a/static/js/controllers.js b/static/js/controllers.js index b103a4255..c1664cb27 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -256,9 +256,14 @@ function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) { browserchrome.update(); } -function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { +function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) { $rootScope.title = 'Loading...'; + // Watch for changes to the tag parameter. + $scope.$on('$routeUpdate', function(){ + $scope.setTag($location.search().tag, false); + }); + $scope.editDescription = function() { if (!$scope.repo.can_write) { return; } @@ -307,7 +312,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.tree.draw('image-history-container'); $($scope.tree).bind('tagChanged', function(e) { - $scope.$apply(function() { $scope.setTag(e.tag); }); + $scope.$apply(function() { $scope.setTag(e.tag, true); }); }); $($scope.tree).bind('imageChanged', function(e) { $scope.$apply(function() { $scope.setImage(e.image); }); @@ -322,12 +327,18 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { } }; - $scope.setTag = function(tagName) { + $scope.setTag = function(tagName, opt_updateURL) { var repo = $scope.repo; $scope.currentTag = repo.tags[tagName] || repo.tags['latest']; $scope.currentImage = $scope.currentTag.image; + + currentTagName = $scope.currentTag.name; if ($scope.tree) { - $scope.tree.setTag($scope.currentTag.name); + $scope.tree.setTag(currentTagName); + } + + if (opt_updateURL) { + $location.search('tag', currentTagName); } }; From 898210065e7725e75d84946258d2bd54436b471c Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 16 Oct 2013 23:55:11 -0400 Subject: [PATCH 08/15] Fix issue with changing the tag right after selecting the tag's image --- static/js/graphing.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index bb776d816..ba371fba4 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -367,14 +367,23 @@ ImageHistoryTree.prototype.markPath_ = function(startingNode, isHighlighted) { * Sets the current tag displayed in the tree. */ ImageHistoryTree.prototype.setTag_ = function(tagName) { + if (tagName == this.currentTag_) { + return; + } + + var imageByDBID = this.imageByDBID_; + // Save the current tag. + var previousTagName = this.currentTag_; this.currentTag_ = tagName; - var imageByDBID = this.imageByDBID_; - // Update the state of each existing node to no longer be highlighted. - if (this.currentImage_) { - var currentNode = imageByDBID[this.currentImage_.dbid]; + var previousImage = this.findImage_(function(image) { + return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0; + }); + + if (previousImage) { + var currentNode = imageByDBID[previousImage.dbid]; this.markPath_(currentNode, false); } From 7c289beba588189a81b2e7bd5e600cdcb8a1773e Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 17 Oct 2013 14:29:47 -0400 Subject: [PATCH 09/15] Notify the tree when the container was probably resized. --- static/js/app.js | 17 +++++++++++++++++ static/js/controllers.js | 6 ++++++ static/js/graphing.js | 8 ++++++++ static/partials/view-repo.html | 2 +- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/static/js/app.js b/static/js/app.js index 031b82c3c..375779ee0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -117,6 +117,23 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', } }; }). + directive('onresize', function ($window, $parse) { + return function (scope, element, attr) { + var fn = $parse(attr.onresize); + + var notifyResized = function() { + scope.$apply(function () { + fn(scope); + }); + }; + + angular.element($window).on('resize', null, notifyResized); + + scope.$on('$destroy', function() { + angular.element($window).off('resize', null, notifyResized); + }); + }; + }). config(['$routeProvider', '$locationProvider', '$analyticsProvider', function($routeProvider, $locationProvider, $analyticsProvider) { diff --git a/static/js/controllers.js b/static/js/controllers.js index 79d98b4ad..4de9df79f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -313,6 +313,12 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) { } }; + $scope.$watch('repo', function() { + if ($scope.tree) { + $scope.tree.notifyResized(); + } + }); + var listImages = function() { if ($scope.imageHistory) { return; } diff --git a/static/js/graphing.js b/static/js/graphing.js index ba371fba4..b5c1110fa 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -136,6 +136,14 @@ ImageHistoryTree.prototype.draw = function(container) { }; +/** + * Redraws the image history to fit the new size. + */ +ImageHistoryTree.prototype.notifyResized = function() { + console.log('Image history container resized.'); +}; + + /** * Sets the current tag displayed in the tree. */ diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index 834bdb918..1d6af5704 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -83,7 +83,7 @@ -
+
From faf6a5c497062099e70c31be3d39624c1f69eafa Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 17 Oct 2013 14:46:23 -0400 Subject: [PATCH 10/15] Add UI polishing to the token auth stuff --- static/css/quay.css | 63 +++++++++++++++++++++++++++++++-- static/js/controllers.js | 34 +++++++++++------- static/partials/guide.html | 54 +++++++++++++++++++++++----- static/partials/header.html | 4 +-- static/partials/repo-admin.html | 29 ++++++++++----- static/partials/view-repo.html | 2 +- 6 files changed, 152 insertions(+), 34 deletions(-) diff --git a/static/css/quay.css b/static/css/quay.css index bc9175811..be51c55e1 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2,11 +2,48 @@ font-family: 'Droid Sans', sans-serif; } +.description-overview { + padding: 4px; + font-size: 16px; +} + +.description-list { + margin: 10px; + padding: 0px; +} + +.description-list li:before { + content: "\00BB"; + margin-right: 6px; + font-size: 18px; +} + +.description-list li { + list-style-type: none; + margin: 0px; + padding: 6px; +} + +.info-icon { + display: inline-block; + float: right; + vertical-align: middle; + font-size: 20px; +} + .accordion-toggle { cursor: pointer; text-decoration: none !important; } +.user-guide h3 { + margin-bottom: 20px; +} + +.user-guide h3 .label { + float: right; +} + .plans .callout { font-size: 2em; text-align: center; @@ -441,7 +478,8 @@ p.editable:hover i { } .repo dl.dl-horizontal dt { - width: 60px; + width: 80px; + padding-right: 10px; } .repo dl.dl-horizontal dd { @@ -485,18 +523,21 @@ p.editable:hover i { color: white; } -.repo #clipboardCopied { +.repo #clipboardCopied.hovering { position: absolute; right: 0px; top: 40px; +} +.repo #clipboardCopied { font-size: 0.8em; + display: inline-block; + margin-right: 10px; background: black; color: white; padding: 6px; border-radius: 4px; - -webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards; -moz-animation: fadeOut 4s ease-in-out 0s 1 forwards; @@ -557,6 +598,18 @@ p.editable:hover i { padding-left: 36px; } +.repo-admin .token-dialog-body .well { + margin-bottom: 0px; +} + +.repo-admin .token-view { + background: transparent; + display: block; + border: 0px transparent; + font-size: 12px; + width: 100%; +} + .repo-admin .panel { display: inline-block; width: 620px; @@ -571,6 +624,10 @@ p.editable:hover i { min-width: 300px; } +.repo-admin .token a { + cursor: pointer; +} + .repo .description p { margin-bottom: 6px; } diff --git a/static/js/controllers.js b/static/js/controllers.js index d6765a309..357dc6687 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1,3 +1,17 @@ +$.fn.clipboardCopy = function() { + var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); + clip.on('complete', function() { + // Resets the animation. + var elem = $('#clipboardCopied')[0]; + elem.style.display = 'none'; + + // Show the notification. + setTimeout(function() { + elem.style.display = 'inline-block'; + }, 1); + }); +}; + function getFirstTextLine(commentString) { if (!commentString) { return; } @@ -354,18 +368,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.currentTag = repo.tags[tag] || repo.tags['latest']; $scope.setImage($scope.currentTag.image); - var clip = new ZeroClipboard($('#copyClipboard'), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); - clip.on('complete', function() { - // Resets the animation. - var elem = $('#clipboardCopied')[0]; - elem.style.display = 'none'; - - // Show the notification. - setTimeout(function() { - elem.style.display = 'block'; - }, 1); - }); - + $('#copyClipboard').clipboardCopy(); $scope.loading = false; }, function() { $scope.repo = null; @@ -378,6 +381,13 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { } function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { + $('.info-icon').popover({ + 'trigger': 'hover', + 'html': true + }); + + $('#copyClipboard').clipboardCopy(); + var namespace = $routeParams.namespace; var name = $routeParams.name; diff --git a/static/partials/guide.html b/static/partials/guide.html index 5ee657c1d..00d773304 100644 --- a/static/partials/guide.html +++ b/static/partials/guide.html @@ -1,10 +1,20 @@
Warning: Quay requires docker version 0.6.2 or higher to work
-

Getting started guide

-
+

User guide

+
-

Pushing a repository to Quay

+

Pulling a repository from Quay

+
+
Note: Private repositories require you to be logged in or the pull will fail. See below for how to sign into Quay if you have never done so before.
+ To pull a repository from Quay, run the following command: +

+
docker pull quay.io/username/repo_name
+
+
+ + +

Pushing a repository to Quay Requires Write Access

First, tag the image with your repository name:

docker tag 0u123imageid quay.io/username/repo_name
@@ -14,12 +24,40 @@

-

Pulling a repository from Quay

+

Granting and managing permissions to users Requires Admin Access

-
Note: Private repositories require you to be logged in or the pull will fail. See below for how to sign into Quay if you have never done so before.
- To pull a repository from Quay, run the following command: -

-
docker pull quay.io/username/repo_name
+
Quay allows a repository to be shared any number of users and to grant those users any level of permissions for a repository
+ +
    +
  • Permissions for a repository can be granted and managed in the repository's admin interface +
  • Adding a user: Type that user's username in the "Add New User..." field, and select the user +
  • Changing permissions: A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user +
  • Removing a user: A user can be removed from the list by clicking the X and then clicking Delete +
+ +
+
+ +

Using access tokens in place of users Requires Admin Access

+
+
+ There are many circumstances where it makes sense to not user a user's username and password (deployment scripts, etc). + To support this case, Quay allows the user of access tokens which can be created on a repository and have read and/or write + permissions, without any passwords. +
+ +
    +
  • Tokens can be managed in the repository's admin interface +
  • Adding a token: Enter a user-readable description in the "New token description" field +
  • Changing permissions: A token's permissions (read or read/write) can be changed by clicking the field to the right of the token +
  • Deleting a token: A token can be deleted by clicking the X and then clicking Delete +
  • Using a token: To use the token, the following credentials can be used: +
    +
    Username
    $token
    +
    Password
    (token value can be found by clicking on the token)
    +
    +
+

diff --git a/static/partials/header.html b/static/partials/header.html index 5b04f0f85..321e08612 100644 --- a/static/partials/header.html +++ b/static/partials/header.html @@ -15,7 +15,7 @@
\ No newline at end of file +
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 967a36892..4f0423ca4 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -16,7 +16,10 @@
-
User Access Permissions
+
User Access Permissions + + +
@@ -59,20 +62,23 @@
-
Access Token Permissions
+
Access Token Permissions + + +
- + - @@ -93,7 +99,7 @@ + + + +
TokenToken Description Permissions
+ {{ token.friendlyName }}
- + @@ -166,12 +172,19 @@ - From ce458f93ef6425ec3d91f4d88bb2b10778651536 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 17 Oct 2013 15:20:27 -0400 Subject: [PATCH 11/15] Have the image tree update itself in response to resizing --- static/js/graphing.js | 106 ++++++++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index b5c1110fa..b068ed792 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -47,6 +47,64 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime) { } +/** + * Calculates the dimensions of the tree. + */ +ImageHistoryTree.prototype.calculateDimensions_ = function(container) { + var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH); + var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10); + + var margin = { top: 40, right: 20, bottom: 20, left: 40 }; + var m = [margin.top, margin.right, margin.bottom, margin.left]; + var w = cw - m[1] - m[3]; + var h = ch - m[0] - m[2]; + + return { + 'w': w, + 'h': h, + 'm': m, + 'cw': cw, + 'ch': ch + }; +}; + + +/** + * Updates the dimensions of the tree. + */ +ImageHistoryTree.prototype.updateDimensions_ = function() { + var container = this.container_; + var dimensions = this.calculateDimensions_(container); + + var m = dimensions.m; + var w = dimensions.w; + var h = dimensions.h; + var cw = dimensions.cw; + var ch = dimensions.ch; + + // Set the height of the container so that it never goes offscreen. + $('#' + container).removeOverscroll(); + var viewportHeight = $(window).height(); + var boundingBox = document.getElementById(container).getBoundingClientRect(); + document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 30) + 'px'; + $('#' + container).overscroll(); + + // Update the tree. + var rootSvg = this.rootSvg_; + var tree = this.tree_; + var vis = this.vis_; + + rootSvg + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]); + + tree.size([w, h]); + vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")"); + + return dimensions; +}; + + /** * Draws the tree. */ @@ -56,34 +114,20 @@ ImageHistoryTree.prototype.draw = function(container) { this.maxWidth_ = result['maxWidth']; this.maxHeight_ = result['maxHeight']; - // Determine the size of the SVG container. - var width = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH); - var height = this.maxHeight_ * (DEPTH_HEIGHT + 10); - - // Set the height of the container so that it never goes offscreen. - var viewportHeight = $(window).height(); - var boundingBox = document.getElementById(container).getBoundingClientRect(); - document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top) + 'px'; - - var margin = { top: 40, right: 20, bottom: 20, left: 40 }; - var m = [margin.top, margin.right, margin.bottom, margin.left]; - var w = width - m[1] - m[3]; - var h = height - m[0] - m[2]; - + // Save the container. + this.container_ = container; + // Create the tree and all its components. var tree = d3.layout.tree() - .separation(function() { return 2; }) - .size([w, h]); + .separation(function() { return 2; }); var diagonal = d3.svg.diagonal() .projection(function(d) { return [d.x, d.y]; }); - var vis = d3.select("#" + container).append("svg:svg") - .attr("width", w + m[1] + m[3]) - .attr("height", h + m[0] + m[2]) - .attr("class", "image-tree") - .append("svg:g") - .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); + var rootSvg = d3.select("#" + container).append("svg:svg") + .attr("class", "image-tree"); + + var vis = rootSvg.append("svg:g"); var formatComment = this.formatComment_; var formatTime = this.formatTime_; @@ -102,7 +146,7 @@ ImageHistoryTree.prototype.draw = function(container) { } if (!d.image) { - return '(This repository is empty)'; + return '(This repository is empty)'; } if (d.image.comment) { @@ -116,20 +160,19 @@ ImageHistoryTree.prototype.draw = function(container) { vis.call(tip); // Save all the state created. - this.fullWidth_ = width; - - this.width_ = w; - this.height_ = h; - this.diagonal_ = diagonal; this.vis_ = vis; + this.rootSvg_ = rootSvg; this.tip_ = tip; - this.tree_ = tree; + // Update the dimensions of the tree. + var dimensions = this.updateDimensions_(); + // Populate the tree. - this.root_.x0 = this.fullWidth_ / 2; + this.root_.x0 = dimensions.cw / 2; this.root_.y0 = 0; + this.setTag_(this.currentTag_); $('#' + container).overscroll(); @@ -140,7 +183,8 @@ ImageHistoryTree.prototype.draw = function(container) { * Redraws the image history to fit the new size. */ ImageHistoryTree.prototype.notifyResized = function() { - console.log('Image history container resized.'); + this.updateDimensions_(); + this.update_(this.root_); }; From d6216dcc5f69431a116d728b5f09eed7d078923a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 17 Oct 2013 15:39:24 -0400 Subject: [PATCH 12/15] Fix the collapsed node title to show the correct number of images --- static/js/graphing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index b068ed792..4794acbd0 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -350,7 +350,7 @@ ImageHistoryTree.prototype.collapseNodes_ = function(node) { if (encountered.length >= 3) { // Collapse the node. var collapsed = { - "name": '(' + encountered.length + ' images)', + "name": '(' + (encountered.length - 1) + ' images)', "children": [current], "collapsed": true, "encountered": encountered From 982fdd4c5fe64f05ecbdf5fb3c0c07834ba1fda4 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 17 Oct 2013 16:50:58 -0400 Subject: [PATCH 13/15] Fix some tabs and some spelling errors, prevent empty tokens, move the tooltips to the left. --- static/js/controllers.js | 2 + static/partials/guide.html | 29 +++++----- static/partials/repo-admin.html | 95 ++++++++++++++++----------------- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 357dc6687..f12cb9332 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -486,6 +486,8 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); permissionPost.customPOST(friendlyName).then(function(newToken) { + $scope.newToken.friendlyName = ''; + $scope.createTokenForm.$setPristine(); $scope.tokens[newToken.code] = newToken; }); }; diff --git a/static/partials/guide.html b/static/partials/guide.html index 00d773304..70e5f459f 100644 --- a/static/partials/guide.html +++ b/static/partials/guide.html @@ -29,10 +29,10 @@
Quay allows a repository to be shared any number of users and to grant those users any level of permissions for a repository
    -
  • Permissions for a repository can be granted and managed in the repository's admin interface -
  • Adding a user: Type that user's username in the "Add New User..." field, and select the user -
  • Changing permissions: A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user -
  • Removing a user: A user can be removed from the list by clicking the X and then clicking Delete +
  • Permissions for a repository can be granted and managed in the repository's admin interface +
  • Adding a user: Type that user's username in the "Add New User..." field, and select the user +
  • Changing permissions: A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user +
  • Removing a user: A user can be removed from the list by clicking the X and then clicking Delete
@@ -41,21 +41,22 @@

Using access tokens in place of users Requires Admin Access

- There are many circumstances where it makes sense to not user a user's username and password (deployment scripts, etc). - To support this case, Quay allows the user of access tokens which can be created on a repository and have read and/or write + There are many circumstances where it makes sense to not use a user's username and password (deployment scripts, etc). + To support this case, Quay allows the use of access tokens which can be created on a repository and have read and/or write permissions, without any passwords.
  • Tokens can be managed in the repository's admin interface -
  • Adding a token: Enter a user-readable description in the "New token description" field -
  • Changing permissions: A token's permissions (read or read/write) can be changed by clicking the field to the right of the token -
  • Deleting a token: A token can be deleted by clicking the X and then clicking Delete -
  • Using a token: To use the token, the following credentials can be used: -
    -
    Username
    $token
    -
    Password
    (token value can be found by clicking on the token)
    -
    +
  • Adding a token: Enter a user-readable description in the "New token description" field +
  • Changing permissions: A token's permissions (read or read/write) can be changed by clicking the field to the right of the token +
  • Deleting a token: A token can be deleted by clicking the X and then clicking Delete +
  • Using a token: To use the token, the following credentials can be used: +
    +
    Username
    $token
    +
    Password
    (token value can be found by clicking on the token)
    +
    Email
    This value is ignored, any value may be used.
    +
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 4f0423ca4..e48b89658 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -18,7 +18,7 @@
User Access Permissions - +
@@ -64,49 +64,48 @@
Access Token Permissions - +
- - - - - - - + +
Token DescriptionPermissions
+ + + + + + + + + + + + - - - - - - - - - - - - - -
Token DescriptionPermissions
+ + {{ token.friendlyName }} + +
+ + +
+
+ + + + +
- - {{ token.friendlyName }} - -
- - -
-
- - - - -
- - - -
+
+ + + +
+
@@ -175,16 +174,16 @@
From e382fa1e588721c7118c71ab6cb335adcd05abff Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 17 Oct 2013 17:45:08 -0400 Subject: [PATCH 14/15] Add a status page to /v1/ so that people don't get confused by the message that docker emits --- endpoints/web.py | 4 ++++ static/js/app.js | 5 ++++- static/js/controllers.js | 12 +++++++++++- static/partials/v1-page.html | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 static/partials/v1-page.html diff --git a/endpoints/web.py b/endpoints/web.py index 6fb7e2f33..3e6b2fc4d 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -71,6 +71,10 @@ def signin(): def repository(): return index('') +@app.route('/v1') +@app.route('/v1/') +def v1(): + return index('') @app.route('/status', methods=['GET']) def status(): diff --git a/static/js/app.js b/static/js/app.js index 375779ee0..e534c2a97 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -151,9 +151,12 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}). when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). when('/user/', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}). - when('/guide/', {title: 'Getting Started Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). + when('/guide/', {title: 'User Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/signin/', {title: 'Signin', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). + + when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). + when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). otherwise({redirectTo: '/'}); }]). diff --git a/static/js/controllers.js b/static/js/controllers.js index f472f9fbb..c3b58cc1c 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -50,7 +50,7 @@ function getMarkedDown(string) { } function HeaderCtrl($scope, $location, UserService, Restangular) { - $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; }, true); @@ -755,4 +755,14 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, }); }); }; +} + +function V1Ctrl($scope, UserService) { + $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + $scope.user = currentUser; + }, true); + + $scope.browseRepos = function() { + document.location = '/repository/'; + }; } \ No newline at end of file diff --git a/static/partials/v1-page.html b/static/partials/v1-page.html new file mode 100644 index 000000000..821654fdd --- /dev/null +++ b/static/partials/v1-page.html @@ -0,0 +1,20 @@ +
+
+

Welcome {{ user.username }}. Your account is fully activated!

+ +
+ +
+
+
+

Welcome {{ user.username }}. Your account is pending email confirmation.

+

Please check your inbox (and potentially your spam folder) for an email from support@quay.io

+
+
+

Docker sent me here. What do I need to do?

+

+ Sign In to check the activation status of your account. If you have already activated + your account previously, then you do not need to do anything further. +

+
+
From 99eedb14d13cf6a695bf9ff9f0c7a3aedc94c7cc Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 17 Oct 2013 17:59:34 -0400 Subject: [PATCH 15/15] Wait until the image container is really already shown before calling resize. --- static/js/controllers.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index c3b58cc1c..7a6e8bd4b 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -270,7 +270,7 @@ function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) { browserchrome.update(); } -function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) { +function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $timeout) { $rootScope.title = 'Loading...'; // Watch for changes to the tag parameter. @@ -329,7 +329,9 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) { $scope.$watch('repo', function() { if ($scope.tree) { - $scope.tree.notifyResized(); + $timeout(function() { + $scope.tree.notifyResized(); + }); } });