Initial redesigned UI for repo listings w/ stars.

This commit is contained in:
Jimmy Zelinskie 2014-12-11 15:06:30 -05:00
parent 97b605ca8d
commit 5a484cfe11
10 changed files with 308 additions and 103 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,73 +1,136 @@
<div class="container">
<div class="repo-list" ng-show="!user.anonymous">
<div ng-class="user.organizations.length ? 'section-header' : ''">
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
<a href="/organization/{{ namespace }}" ng-show="namespace != user.username">
<button class="btn btn-default">
<i class="fa fa-group user-tool"></i>
View Organization
</button>
<div class="row">
<div class="col-lg-3 col-lg-push-9 col-md-3 col-md-push-9 col-sm-4 col-sm-push-8 col-xs-12">
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Users
</div>
<div class="panel-body">
<a href="javascript:void(0)">
<span class="avatar" size="24" hash="user.avatar"></span>
{{ user.username }}
</a>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
</div>
<h3 ng-show="namespace == user.username">Your Repositories</h3>
<h3 ng-show="namespace != user.username">Repositories</h3>
<div class="resource-view" resource="user_repositories">
<!-- User/Org has repositories -->
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
<div class="panel panel-default">
<div class="panel-heading">
Organizations
</div>
<div class="panel-body">
<div ng-repeat="org in user.organizations">
<a href="/organization/{{ org.name }}">
<span class="avatar" size="24" hash="org.avatar"></span>
{{ org.name }}
</a>
<a href="/organization/{{ org.name }}/admin">
<i class="fa fa-gear" style="color:#000"></i>
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- User/Org has no repositories -->
<div ng-show="user_repositories.value.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="http://docs.quay.io/solution/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
</div>
</div>
</div>
</div>
<div class="repo-list">
<h3>Top Public Repositories</h3>
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
<div class="col-lg-9 col-lg-pull-3 col-md-9 col-md-pull-3 col-sm-8 col-sm-pull-4 col-xs-12">
<div class="resource-view" resource="starred_repositories">
<div class="repo-listing">
<div class="repo-list-title">
<i class="fa fa-star"></i>
Starred
</div>
<div>
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12" ng-repeat="repository in starred_repositories.value">
<div class="panel panel-default">
<div class="panel-body starred">
<div class="row">
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-10">
<span class="repo-icon repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}" class="repo-panel-repo-link"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<i class="star-icon starred fa fa-star" ng-click="unstarRepo(repository)"></i>
</div>
</div>
<!-- The description automatically gets put in a <p> which adds margin that throws off our .repo-panel padding -->
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
</div>
</div>
<div ng-show="starred_repositories.value.length == 0">
<div class="empty-primary-msg">You haven't starred any repositories yet.</div>
<div class="empty-secondary-msg">Stars allow you to easily access your favorite repositories.</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<hr>
</div>
</div>
</div>
</div>
<div class="page-controls">
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
ng-click="movePublicPage(-1)">
<i class="fa fa-chevron-left"></i>
</button>
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
ng-click="movePublicPage(1)">
<i class="fa fa-chevron-right"></i>
</button>
</div>
<!-- Repo listings for User and Orgs -->
<div ng-repeat="namespace in namespaces">
<div class="resource-view" resource="namespace">
<div class="repo-listing">
<div class="repo-list-title" ng-show="user.username == namespace.username">
<i class="fa fa-user"></i>
{{ namespace.username }}
</div>
<div class="repo-list-title" ng-show="user.username != namespace.username">
<i class="fa fa-sitemap"></i>
<a href="/organization/{{ namespace.name }}">{{ namespace.name }}</a>
</div>
<div ng-show="namespace.repositories.value.length > 0">
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-6 col-xs-12" ng-repeat="repository in namespace.repositories.value">
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-10">
<span class="repo-icon repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}" class="repo-panel-repo-link"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<i class="star-icon fa fa-star-o" ng-click="starRepo(repository)"></i>
</div>
</div>
<!-- The description automatically gets put in a <p> which adds margin that throws off our .repo-panel padding -->
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
</div>
<div ng-show="namespace.value.length == 0">
<div class="empty-primary-msg">There aren't any repositories yet.</div>
<div class="empty-secondary-msg">This origanization does't have any repositories, or you have not been provided access. <a href="/new">Create a new repository</a>
</div>
<!-- User/Org has no repositories -->
<div ng-show="namespace.repositories.value.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="http://docs.quay.io/solution/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
</div>
</div>
</div>
</div>
</div>
<hr>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div ng-show="test">
{{ test }}
</div>

Binary file not shown.