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 @@ -