diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c8d40f5d..3a96f3eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ The following are features that have been merged, but not yet deployed: +- Add list/table view to the repositories page (#116) - Add ability to disable users via the superuser panel (#26) - Add a Changelog view to the superuser panel (#186) diff --git a/data/model/legacy.py b/data/model/legacy.py index 68b50370c..721bb38bc 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -330,29 +330,39 @@ def _list_entity_robots(entity_name): .where(User.robot == True, User.username ** (entity_name + '+%'))) -class _TupleWrapper(object): - def __init__(self, data, fields): - self._data = data - self._fields = fields - - def get(self, field): - return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)] - - class TupleSelector(object): """ Helper class for selecting tuples from a peewee query and easily accessing them as if they were objects. """ + class _TupleWrapper(object): + def __init__(self, data, fields): + self._data = data + self._fields = fields + + def get(self, field): + return self._data[self._fields.index(TupleSelector.tuple_reference_key(field))] + + @classmethod + def tuple_reference_key(cls, field): + """ Returns a string key for referencing a field in a TupleSelector. """ + if field._node_type == 'func': + return field.name + ','.join([cls.tuple_reference_key(arg) for arg in field.arguments]) + + if field._node_type == 'field': + return field.name + ':' + field.model_class.__name__ + + raise Exception('Unknown field type %s in TupleSelector' % field._node_type) + def __init__(self, query, fields): self._query = query.select(*fields).tuples() - self._fields = [field.name + ':' + field.model_class.__name__ for field in fields] + self._fields = [TupleSelector.tuple_reference_key(field) for field in fields] def __iter__(self): return self._build_iterator() def _build_iterator(self): for tuple_data in self._query: - yield _TupleWrapper(tuple_data, self._fields) + yield TupleSelector._TupleWrapper(tuple_data, self._fields) @@ -934,22 +944,22 @@ def get_user_teams_within_org(username, organization): User.username == username) -def get_visible_repository_count(username=None, include_public=True, - namespace=None): - query = _visible_repository_query(username=username, - include_public=include_public, - namespace=namespace) - return query.count() - - def get_visible_repositories(username=None, include_public=True, page=None, - limit=None, sort=False, namespace=None, namespace_only=False): + limit=None, namespace=None, namespace_only=False, + include_actions=False, include_latest_tag=False): + + fields = [Repository.name, Repository.id, Repository.description, Visibility.name, + Namespace.username] + + if include_actions: + fields.append(fn.Max(RepositoryActionCount.count)) + + if include_latest_tag: + fields.append(fn.Max(RepositoryTag.lifetime_start_ts)) + query = _visible_repository_query(username=username, include_public=include_public, page=page, limit=limit, namespace=namespace, - select_models=[Repository, Namespace, Visibility]) - - if sort: - query = query.order_by(Repository.id.desc()) + select_models=fields) if limit: query = query.limit(limit) @@ -957,7 +967,24 @@ def get_visible_repositories(username=None, include_public=True, page=None, if namespace and namespace_only: query = query.where(Namespace.username == namespace) - return query + if include_actions: + # Filter the join to recent entries only. + last_week = datetime.now() - timedelta(weeks=1) + join_query = ((RepositoryActionCount.repository == Repository.id) & + (RepositoryActionCount.date >= last_week)) + + query = (query.switch(Repository) + .join(RepositoryActionCount, JOIN_LEFT_OUTER, on=join_query) + .group_by(RepositoryActionCount.repository, Repository.name, Repository.id, + Repository.description, Visibility.name, Namespace.username)) + + if include_latest_tag: + query = (query.switch(Repository) + .join(RepositoryTag, JOIN_LEFT_OUTER) + .group_by(RepositoryTag.repository, Repository.name, Repository.id, + Repository.description, Visibility.name, Namespace.username)) + + return TupleSelector(query, fields) def _visible_repository_query(username=None, include_public=True, limit=None, @@ -1101,7 +1128,7 @@ def get_matching_repositories(repo_term, username=None, limit=10, include_public namespace_term = repo_term name_term = repo_term - visible = get_visible_repositories(username, include_public=include_public) + visible = _visible_repository_query(username, include_public=include_public) search_clauses = (Repository.name ** ('%' + name_term + '%') | Namespace.username ** ('%' + namespace_term + '%')) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 802a0e36f..d2f3f4085 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -3,11 +3,16 @@ import logging import json import datetime + from datetime import timedelta from flask import request from data import model +from data.model import Namespace +from data.database import (Repository as RepositoryTable, Visibility, RepositoryTag, + RepositoryActionCount, fn) + from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request, require_repo_read, require_repo_write, require_repo_admin, RepositoryParamResource, resource, query_param, parse_args, ApiResource, @@ -109,11 +114,12 @@ class RepositoryList(ApiResource): type=truthy_bool, default=True) @query_param('private', 'Whether to include private repositories.', type=truthy_bool, default=True) - @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('namespace_only', 'Whether to limit only to the given namespace.', type=truthy_bool, default=False) + @query_param('last_modified', 'Whether to include when the repository was last modified.', + type=truthy_bool, default=False) + @query_param('popularity', 'Whether to include the repository\'s popularity metric.', + type=truthy_bool, default=False) def get(self, args): """Fetch the list of repositories under a variety of situations.""" username = None @@ -126,25 +132,31 @@ class RepositoryList(ApiResource): response = {} - repo_count = None - if args['count']: - repo_count = model.get_visible_repository_count(username, include_public=args['public'], - namespace=args['namespace']) - response['count'] = repo_count - - repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'], - include_public=args['public'], sort=args['sort'], + repo_query = model.get_visible_repositories(username, + limit=args['limit'], + page=args['page'], + include_public=args['public'], namespace=args['namespace'], - namespace_only=args['namespace_only']) + namespace_only=args['namespace_only'], + include_latest_tag=args['last_modified'], + include_actions=args['popularity']) def repo_view(repo_obj): repo = { - 'namespace': repo_obj.namespace_user.username, - 'name': repo_obj.name, - 'description': repo_obj.description, - 'is_public': repo_obj.visibility.name == 'public', + 'namespace': repo_obj.get(Namespace.username), + 'name': repo_obj.get(RepositoryTable.name), + 'description': repo_obj.get(RepositoryTable.description), + 'is_public': repo_obj.get(Visibility.name) == 'public' } + + if args['last_modified']: + repo['last_modified'] = repo_obj.get(fn.Max(RepositoryTag.lifetime_start_ts)) + + if args['popularity']: + repo['popularity'] = repo_obj.get(fn.Max(RepositoryActionCount.count)) or 0 + if get_authenticated_user(): - repo['is_starred'] = repo_obj.id in star_lookup + repo['is_starred'] = repo_obj.get(RepositoryTable.id) in star_lookup + return repo response['repositories'] = [repo_view(repo) for repo in repo_query] diff --git a/static/css/directives/ui/repo-list-table.css b/static/css/directives/ui/repo-list-table.css new file mode 100644 index 000000000..f0f7682a1 --- /dev/null +++ b/static/css/directives/ui/repo-list-table.css @@ -0,0 +1,32 @@ +.repo-list-table { + margin-top: 40px; +} + +.repo-list-table .repo-name-icon .avatar { + margin-right: 10px; +} + +.repo-list-table .repo-name-icon .namespace { + color: #444; + font-size: 85%; +} + +.repo-list-table .repo-name-icon .namespace:after { + content: " / "; +} + +.repo-list-table .empty { + color: #ccc; +} + +.repo-list-table .last-modified { + font-size: 13px; +} + +.repo-list-table .strength-indicator { + display: inline-block; +} + +.repo-list-table .popularity { + line-height: 10px; +} \ No newline at end of file diff --git a/static/css/directives/ui/strength-indicator.css b/static/css/directives/ui/strength-indicator.css new file mode 100644 index 000000000..dcd20638e --- /dev/null +++ b/static/css/directives/ui/strength-indicator.css @@ -0,0 +1,42 @@ +.strength-indicator .indicator-sliver { + margin: 1px; + width: 14px; + height: 3px; + border: 1px solid #D5D5D5; + transition: 0.5s ease; +} + +.strength-indicator .strength-indicator-element.good .indicator-sliver { + background: green; + border: 1px solid green; +} + +.strength-indicator .strength-indicator-element.fair .indicator-sliver { + background: orange; + border: 1px solid orange; +} + +.strength-indicator .strength-indicator-element.fair .indicator-sliver:last-child { + border: 1px solid #D5D5D5; + background: transparent; +} + +.strength-indicator .strength-indicator-element.barely .indicator-sliver { + background: rgb(255, 61, 0); + border: 1px solid rgb(255, 61, 0); +} + +.strength-indicator .strength-indicator-element.barely .indicator-sliver:last-child { + border: 1px solid #D5D5D5; + background: transparent; +} + +.strength-indicator .strength-indicator-element.barely .indicator-sliver:nth-child(3) { + border: 1px solid #D5D5D5; + background: transparent; +} + +.strength-indicator .strength-indicator-element.poor .indicator-sliver:first-child { + border: 1px solid red; + background: red; +} \ No newline at end of file diff --git a/static/css/pages/repo-list.css b/static/css/pages/repo-list.css index ac0481c86..49679b308 100644 --- a/static/css/pages/repo-list.css +++ b/static/css/pages/repo-list.css @@ -1,6 +1,11 @@ .repo-list .repo-list-panel { padding: 20px; - padding-top: 0px; + padding-top: 20px; +} + +.repo-list .repo-list-panel .btn-group { + float: right; + margin-bottom: 10px; } .repo-list .repo-list-namespaces h4 { diff --git a/static/css/quay.css b/static/css/quay.css index ca3c0109a..69adefbf4 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3943,54 +3943,11 @@ pre.command:before { transition: opacity 0.5s ease; } -.location-view .ping-tower { +.location-view .strength-indicator { display: inline-block; vertical-align: middle; } -.location-view .ping-sliver { - margin: 1px; - width: 14px; - height: 3px; - border: 1px solid #D5D5D5; - transition: 0.5s ease; -} - -.location-view .ping-tower.good .ping-sliver { - background: green; - border: 1px solid green; -} - -.location-view .ping-tower.fair .ping-sliver { - background: orange; - border: 1px solid orange; -} - -.location-view .ping-tower.fair .ping-sliver:first-child { - border: 1px solid #D5D5D5; - background: transparent; -} - -.location-view .ping-tower.barely .ping-sliver { - background: rgb(255, 61, 0); - border: 1px solid rgb(255, 61, 0); -} - -.location-view .ping-tower.barely .ping-sliver:first-child { - border: 1px solid #D5D5D5; - background: transparent; -} - -.location-view .ping-tower.barely .ping-sliver:nth-child(2) { - border: 1px solid #D5D5D5; - background: transparent; -} - -.location-view .ping-tower.poor .ping-sliver:last-child { - border: 1px solid red; - background: red; -} - /* This is the visible area of you carousel. Set a width here to define how much items are visible. diff --git a/static/directives/location-view.html b/static/directives/location-view.html index 8a5ebb421..fbfd241ba 100644 --- a/static/directives/location-view.html +++ b/static/directives/location-view.html @@ -3,10 +3,5 @@ - - - - - - + diff --git a/static/directives/repo-list-table.html b/static/directives/repo-list-table.html new file mode 100644 index 000000000..10daa7d81 --- /dev/null +++ b/static/directives/repo-list-table.html @@ -0,0 +1,49 @@ +
+ Repository Name + | ++ Last Modified + | ++ Activity + | ++ Star + | + + + +
+ + + {{ repository.namespace }} + {{ repository.name }} + + | ++ + {{ repository.last_modified * 1000 | amCalendar }} + + (Empty Repository) + | + ++ | +