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