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/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/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/app.js b/static/js/app.js index e9e9f39cc..e534c2a97 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) { @@ -129,14 +146,17 @@ 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}). 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 d6765a309..7a6e8bd4b 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; } @@ -36,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); @@ -256,9 +270,14 @@ function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) { browserchrome.update(); } -function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { +function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $timeout) { $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; } @@ -295,19 +314,45 @@ 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]; + } + } + }; + + $scope.$watch('repo', function() { + if ($scope.tree) { + $timeout(function() { + $scope.tree.notifyResized(); + }); + } + }); + + 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, + $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).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 +367,28 @@ 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; - 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 = proposedTag; + $scope.currentImage = $scope.currentTag.image; + if ($scope.tree) { + $scope.tree.setTag($scope.currentTag.name); + } + + if (opt_updateURL) { + $location.search('tag', $scope.currentTag.name); + } } }; @@ -342,7 +403,6 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { var namespace = $routeParams.namespace; var name = $routeParams.name; - var tag = $routeParams.tag || 'latest'; $scope.loading = true; @@ -351,21 +411,10 @@ 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); - 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); - }); + $scope.setTag($routeParams.tag); + $('#copyClipboard').clipboardCopy(); $scope.loading = false; }, function() { $scope.repo = null; @@ -374,10 +423,17 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { }); // Fetch the image history. - $scope.listImages(); + listImages(); } function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { + $('.info-icon').popover({ + 'trigger': 'hover', + 'html': true + }); + + $('#copyClipboard').clipboardCopy(); + var namespace = $routeParams.namespace; var name = $routeParams.name; @@ -476,6 +532,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; }); }; @@ -699,4 +757,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/js/graphing.js b/static/js/graphing.js index f05943ae4..4794acbd0 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,9 +29,82 @@ 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; } +/** + * 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_; @@ -101,6 +145,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) + ''; } @@ -112,26 +160,34 @@ 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(); }; +/** + * Redraws the image history to fit the new size. + */ +ImageHistoryTree.prototype.notifyResized = function() { + this.updateDimensions_(); + this.update_(this.root_); +}; + + /** * Sets the current tag displayed in the tree. */ @@ -198,7 +254,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 +322,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; }; @@ -292,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 @@ -361,21 +419,36 @@ 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; // 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); + var previousImage = this.findImage_(function(image) { + return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0; + }); - // Find the new current image. + 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) >= 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 +505,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_; @@ -469,7 +543,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); @@ -520,7 +594,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. diff --git a/static/partials/guide.html b/static/partials/guide.html index 5ee657c1d..70e5f459f 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,41 @@

-

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 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)
    +
    Email
    This value is ignored, any value may be used.
    +
    +
+

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..e48b89658 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -16,7 +16,10 @@
-
User Access Permissions
+
User Access Permissions + + +
@@ -59,48 +62,50 @@
-
Access Token Permissions
-
- -
- - - - - - - - - - - - - +
Access Token Permissions -
- - + + + + + +
TokenPermissions
- - {{ token.friendlyName }} - -
- - -
-
- - - - -
- + + +
+ + + + + + + + + + + + + - - -
Token DescriptionPermissions
+ + {{ token.friendlyName }} + +
+ + +
- + + + +
+
+ + + +
+
@@ -166,12 +171,19 @@
- @@ -83,7 +83,7 @@
-
+
diff --git a/test.db b/test.db index 70f675eb8..c5620a9ba 100644 Binary files a/test.db and b/test.db differ