From 5a484cfe11545ff0f7ac97b35818df8a8a90f1ec Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 11 Dec 2014 15:06:30 -0500 Subject: [PATCH] Initial redesigned UI for repo listings w/ stars. --- data/model/legacy.py | 15 +-- endpoints/api/repository.py | 3 +- endpoints/api/user.py | 21 +++- endpoints/web.py | 5 + static/css/quay.css | 98 ++++++++++++++---- static/js/app.js | 1 + static/js/controllers.js | 82 +++++++++++++-- static/partials/repo-list.html | 183 ++++++++++++++++++++++----------- static/partials/starred.html | 3 + test/data/test.db | Bin 257024 -> 257024 bytes 10 files changed, 308 insertions(+), 103 deletions(-) create mode 100644 static/partials/starred.html diff --git a/data/model/legacy.py b/data/model/legacy.py index 52b7ce5d9..b712a5271 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -778,9 +778,9 @@ def get_visible_repository_count(username=None, include_public=True, def get_visible_repositories(username=None, include_public=True, page=None, - limit=None, sort=False, namespace=None): + limit=None, sort=False, namespace=None, include_starred=True): query = _visible_repository_query(username=username, include_public=include_public, page=page, - limit=limit, namespace=namespace, + limit=limit, namespace=namespace, include_starred=include_starred, select_models=[Repository, Namespace, Visibility]) if sort: @@ -793,7 +793,7 @@ def get_visible_repositories(username=None, include_public=True, page=None, def _visible_repository_query(username=None, include_public=True, limit=None, - page=None, namespace=None, select_models=[]): + page=None, namespace=None, include_starred=True, select_models=[]): query = (Repository .select(*select_models) # MySQL/RDS complains is there are selected models for counts. .distinct() @@ -803,8 +803,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, .switch(Repository) .join(RepositoryPermission, JOIN_LEFT_OUTER)) - query = _filter_to_repos_for_user(query, username, namespace, include_public) - + query = _filter_to_repos_for_user(query, username, namespace, include_public, include_starred) if page: query = query.paginate(page, limit) elif limit: @@ -814,7 +813,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, def _filter_to_repos_for_user(query, username=None, namespace=None, - include_public=True): + include_public=True, include_starred=True): if not include_public and not username: return Repository.select().where(Repository.id == '-1') @@ -825,6 +824,7 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, AdminTeam = Team.alias() AdminTeamMember = TeamMember.alias() AdminUser = User.alias() + UserThroughStar = User.alias() query = (query .switch(RepositoryPermission) @@ -844,6 +844,9 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, where_clause = ((User.username == username) | (UserThroughTeam.username == username) | ((AdminUser.username == username) & (TeamRole.name == 'admin'))) + if not include_starred: + subquery = Repository.select().join(Star).join(User).where(User.username == username).alias() + where_clause = where_clause & ~(Repository.id << subquery) if namespace: where_clause = where_clause & (Namespace.username == namespace) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 9302f26a0..488053014 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -107,6 +107,7 @@ class RepositoryList(ApiResource): @query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False) @query_param('count', 'Whether to include a count of the total number of results available.', type=truthy_bool, default=False) + @query_param('starred', 'Whether or not to include starred repositories', type=truthy_bool, default=True) def get(self, args): """Fetch the list of repositories under a variety of situations.""" username = None @@ -123,7 +124,7 @@ class RepositoryList(ApiResource): repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'], include_public=args['public'], sort=args['sort'], - namespace=args['namespace']) + namespace=args['namespace'], include_starred=args['starred']) response['repositories'] = [repo_view(repo) for repo in repo_query if (repo.visibility.name == 'public' or diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 427cb5887..1d12d84b6 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -4,6 +4,7 @@ import json from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity +from peewee import IntegrityError from app import app, billing as stripe, authentication, avatar from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, @@ -713,10 +714,17 @@ class StarredRepositoryList(ApiResource): namespace = req['namespace'] repository = req['repository'] repo = model.get_repository(namespace, repository) + if repo: - model.star_repository(user, repo) - log_action('star_repository', user.username, namespace, - {'repo': repository, 'namespace': namespace}) + try: + model.star_repository(user, repo) + except IntegrityError: + pass + + #TODO(jzelinskie): log this action + #log_action('star_repository', user.username, namespace, + # {'repo': repository, 'namespace': namespace}) + return { 'namespace': namespace, 'repository': repository, @@ -732,10 +740,13 @@ class StarredRepository(RepositoryParamResource): def delete(self, namespace, repository): user = get_authenticated_user() repo = model.get_repository(namespace, repository) + if repo: model.unstar_repository(user, repo) - log_action('unstar_repository', user.username, namespace, - {'repo': repository, 'namespace': namespace}) + + #TODO(jzelinskie): log this action + #log_action('unstar_repository', user.username, namespace, + # {'repo': repository, 'namespace': namespace}) return 'Deleted', 204 raise NotFound() diff --git a/endpoints/web.py b/endpoints/web.py index 4717f7d40..998675ac4 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -139,6 +139,11 @@ def confirm_invite(): def repository(path): return index('') +@web.route('/starred/') +@no_cache +def starred(): + return index('') + @web.route('/security/') @no_cache diff --git a/static/css/quay.css b/static/css/quay.css index d15ca1cb4..13bc91905 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -932,6 +932,8 @@ i.toggle-icon:hover { } .repo-circle { + color: #999; + display: inline-block; position: relative; background: #eee; padding: 4px; @@ -939,7 +941,6 @@ i.toggle-icon:hover { display: inline-block; width: 46px; height: 46px; - text-align: center; } .repo-circle.no-background { @@ -950,11 +951,11 @@ i.toggle-icon:hover { } .repo-circle .fa-hdd-o { - font-size: 36px; + font-size: 1.7em; } .repo-circle.no-background .fa-hdd-o { - font-size: 30px; + font-size: 1.7em; } .repo-circle .fa-lock { @@ -962,18 +963,18 @@ i.toggle-icon:hover { bottom: -2px; right: -4px; background: rgb(253, 191, 191); - width: 20px; + width: 16px; display: inline-block; border-radius: 50%; text-align: center; - height: 20px; - line-height: 21px; - font-size: 16px !important; + height: 16px; + line-height: 16px; + font-size: 12px !important; } .repo-circle.no-background .fa-lock { - bottom: -2px; - right: -6px; + bottom: 5px; + right: 7px; color: #444; } @@ -2480,10 +2481,41 @@ p.editable:hover i { cursor: pointer; } +.empty-primary-msg { + font-size: 18px; + margin-bottom: 30px; + text-align: center; +} + +.empty-secondary-msg { + font-size: 14px; + color: #999; + text-align: center; + margin-bottom: 10px; +} + .repo-list { margin-bottom: 40px; } +.repo-list-title { + margin-bottom: 30px; + margin-top: 10px; + line-height: 24px; + font-size: 18px; +} + +.repo-list-title a { + font-size: 18px; + margin: 0; + display: inline-block; +} + +.repo-list-title i { + display: inline-block; + margin-right: 5px; +} + .repo-list .button-bar-right { float: right; } @@ -2503,11 +2535,42 @@ p.editable:hover i { margin-right: 10px; } +.repo-panel { + padding: 20px; + border: 1px solid #eee; + margin-bottom: 30px; +} + +.panel-body.starred { + background: -moz-linear-gradient(top, rgba(255,240,188,1) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0.49) 51%, rgba(255,255,255,0) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,240,188,1)), color-stop(50%,rgba(255,255,255,0.5)), color-stop(51%,rgba(255,255,255,0.49)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff0bc', endColorstr='#00ffffff',GradientType=0 ); /* IE6-9 */ +} + +.star-icon { + color: #ddd; + display: block; + font-size: 1.2em; + text-align: right; + line-height: 2em; +} + +.star-icon:hover { + cursor: pointer; + cursor: hand; +} + +.star-icon.starred { + color: #ffba6d; +} + .repo-listing { display: block; - margin-bottom: 20px; border-bottom: 1px solid #eee; - padding: 10px; font-size: 14px; line-height: normal; } @@ -2521,18 +2584,13 @@ p.editable:hover i { margin-bottom: 0px; } -.repo-listing a { - font-size: 1.5em; -} - -.repo-listing i { - color: #999; - display: inline-block; - margin-right: 6px; +.repo-panel-repo-link { + font-size: 1.2em; } .repo-listing .description { - padding-left: 44px; + font-size: 0.91em; + padding-top: 13px; } diff --git a/static/js/app.js b/static/js/app.js index 5bb37a338..1a84fef29 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2267,6 +2267,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl, pageClass: 'landing-page'}). + when ('/starred/', {title: 'Starred Repositories', templateUrl: '/static/partials/starred.html', controller: StarCtrl}). otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { diff --git a/static/js/controllers.js b/static/js/controllers.js index 9bea2ebdb..16e48df3f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -237,14 +237,52 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.publicPageCount = null; // Monitor changes in the user. - UserService.updateUserIn($scope, function() { - loadMyRepos($scope.namespace); + UserService.load(function() { + console.log("updateUserIn"); + var user = UserService.currentUser(); + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + loadStarredRepos(); + loadRepos(); + console.log($scope.namespaces); }); + // Monitor changes in the namespace. - $scope.$watch('namespace', function(namespace) { - loadMyRepos(namespace); - }); + //$scope.$watch('namespace', function(namespace) { + // loadStarredRepos($scope.namespace) + // loadRepos(); + //}); + + + $scope.starRepo = function(repo) { + var data = { + 'namespace': repo.namespace, + 'repository': repo.name + }; + ApiService.createStar(data).then(function(result) { + loadStarredRepos($scope.namespace); + loadRepos($scope.namespace); + }, function(result) { + loadStarredRepos(); + loadRepos(); + }); + }; + + $scope.unstarRepo = function(repo) { + var data = { + 'repository': repo.namespace + '/' + repo.name + }; + ApiService.deleteStar(null, data).then(function(result) { + loadStarredRepos($scope.namespace); + loadRepos($scope.namespace); + }, function(result) { + loadStarredRepos($scope.namespace); + loadRepos(); + }); + }; $scope.movePublicPage = function(increment) { if ($scope.publicPageCount == null) { @@ -263,18 +301,36 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { loadPublicRepos(); }; - var loadMyRepos = function(namespace) { - if (!$scope.user || $scope.user.anonymous || !namespace) { + var loadStarredRepos = function() { + if (!$scope.user || $scope.user.anonymous) { return; } - var options = {'public': false, 'sort': true, 'namespace': namespace}; - - $scope.user_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { return resp.repositories; }); }; + var loadRepos = function() { + if ($scope.namespaces.length == 0 || $scope.user.anonymous) { + return; + } + + for (var i = 0; i < $scope.namespaces.length; i++) { + var namespace = $scope.namespaces[i]; + var namespaceName = namespace.username || namespace.name; + var options = { + 'public': false, + 'sort': true, + 'namespace': namespaceName, + 'starred': false, + }; + namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + return resp.repositories; + }); + } + }; + var loadPublicRepos = function() { var options = { 'public': true, @@ -293,7 +349,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }); }; - loadPublicRepos(); + //loadPublicRepos(); } function LandingCtrl($scope, UserService, ApiService, Features, Config) { @@ -401,6 +457,10 @@ function LandingCtrl($scope, UserService, ApiService, Features, Config) { }; } +function StarCtrl($scope) { + $scope.test = "hello"; +} + function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config) { $scope.Config = Config; diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 3e69f3d1a..268aedcde 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,73 +1,136 @@ -
-
-
-
- - - - - - +
+
+ +
+
+ Users +
+ - -
- -

Your Repositories

-

Repositories

- -
- -
-
- - - {{repository.namespace}}/{{repository.name}} +
+
+ Organizations +
+
- - -
-
-

You don't have any repositories yet!

-

This organization doesn't have any repositories, or you have not been provided access.

- Click here to learn how to create a repository
-
-
-

Top Public Repositories

-
-
- - - {{repository.namespace}}/{{repository.name}} - -
+
+
+
+
+ + Starred +
+
+
+ +
+
+
You haven't starred any repositories yet.
+
Stars allow you to easily access your favorite repositories.
+
+
+
+
+
+
+
-
- - +
+ + +
+
+
+
+ + {{ namespace.username }} +
+ +
+
+ +
+
There aren't any repositories yet.
+
This origanization does't have any repositories, or you have not been provided access. Create a new repository +
+ + +
+
+

You don't have any repositories yet!

+

This organization doesn't have any repositories, or you have not been provided access.

+ Click here to learn how to create a repository +
+
+
+
+
+
diff --git a/static/partials/starred.html b/static/partials/starred.html new file mode 100644 index 000000000..b07e1bad9 --- /dev/null +++ b/static/partials/starred.html @@ -0,0 +1,3 @@ +
+ {{ test }} +
diff --git a/test/data/test.db b/test/data/test.db index 68f54f0a1d3043188d68d7459addaf31b55f2b08..0dea8501f9ef5c530ff9594cc6f9265ea189bdd4 100644 GIT binary patch delta 8925 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 E02I;qJpcdz delta 8915 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