Review Board

beta

Add the new Admin UI

Updated 6 months ago

Christian Hammond Reviewers
trunk reviewboard
None Review Board SVN
This introduces the new Admin UI for Review Board. Major highlights include:

* A new look and feel.
* A new dashboard, showing the main models people generally need to work with (Users, review groups, default reviewers, repositories), some server information (Review Board version and cache stats), and Review Board news.
* The database models have been moved to a "Database" tab.

Once Extensions lands, there will be an Extensions tab for activating/deactivating/customizing extensions, alongside the "Dashboard" and "Database" tabs.

Further additions will be made in the future for easily changing server settings and possibly for allowing extensions to add dashboard elements.
Made sure modifying the database fields still works fine.

Tested the news feed links, and made sure news loading failed gracefully if the server was inaccessible.
/trunk/reviewboard/__init__.py
Revision 1384 New Change
1
# The version of Review Board.
2
VERSION = "0.4-pre"
/trunk/reviewboard/settings.py
Revision 1384 New Change
76
    'django.contrib.contenttypes',
76
    'django.contrib.contenttypes',
77
    'django.contrib.markup',
77
    'django.contrib.markup',
78
    'django.contrib.sites',
78
    'django.contrib.sites',
79
    'django.contrib.sessions',
79
    'django.contrib.sessions',
80
    'djblets.datagrid',
80
    'djblets.datagrid',
81
    'djblets.feedview',
81
    'djblets.util',
82
    'djblets.util',
82
    'djblets.webapi',
83
    'djblets.webapi',
83
    'reviewboard.accounts',
84
    'reviewboard.accounts',
84
    'reviewboard.admin',
85
    'reviewboard.admin',
85
    'reviewboard.diffviewer',
86
    'reviewboard.diffviewer',
/trunk/reviewboard/urls.py
Revision 1384 New Change
11
                                      AtomGroupReviewsFeed
11
                                      AtomGroupReviewsFeed
12
12
13
13
14
# URLs global to all modes
14
# URLs global to all modes
15
urlpatterns = patterns('',
15
urlpatterns = patterns('',
16
    (r'^admin/', include('django.contrib.admin.urls')),
16
    (r'^admin/', include('reviewboard.admin.urls')),
17
)
17
)
18
18
19
# Add static media if running in DEBUG mode
19
# Add static media if running in DEBUG mode
20
if settings.DEBUG:
20
if settings.DEBUG:
21
    urlpatterns += patterns('django.views.static',
21
    urlpatterns += patterns('django.views.static',
50
    }
50
    }
51
51
52
52
53
    # Main includes
53
    # Main includes
54
    urlpatterns += patterns('',
54
    urlpatterns += patterns('',
55
        (r'^admin/', include('django.contrib.admin.urls')),
56
        (r'^api/json/', include('reviewboard.webapi.urls')),
55
        (r'^api/json/', include('reviewboard.webapi.urls')),
57
        (r'^r/', include('reviewboard.reviews.urls')),
56
        (r'^r/', include('reviewboard.reviews.urls')),
58
        (r'^reports/', include('reviewboard.reports.urls')),
57
        (r'^reports/', include('reviewboard.reports.urls')),
59
    )
58
    )
60
59
/trunk/reviewboard/admin/cache_stats.py
New File
1
import datetime
2
import re
3
import socket
4
5
try:
6
    import cmemcache as memcache
7
except ImportError:
8
    try:
9
        import memcache
10
    except:
11
        memcache = None
12
13
from django.conf import settings
14
15
16
def get_memcached_hosts():
17
    """
18
    Returns the hosts currently configured for memcached.
19
    """
20
    if not memcache:
21
        return None
22
23
    m = re.match("memcached://([.\w]+:\d+;?)", settings.CACHE_BACKEND)
24
25
    if m:
26
        return m.group(1).split(";")
27
28
    return None
29
30
31
def get_has_cache_stats():
32
    """
33
    Returns whether or not cache stats are supported.
34
    """
35
    return get_memcached_hosts() != None
36
37
38
def get_cache_stats():
39
    """
40
    Returns a dictionary containing information on the current cache stats.
41
    This only supports memcache.
42
    """
43
    hostnames = get_memcached_hosts()
44
45
    if not hostnames:
46
        return None
47
48
    all_stats = []
49
50
    for hostname in hostnames:
51
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
52
        host, port = hostname.split(":")
53
54
        try:
55
            s.connect((host, int(port)))
56
        except socket.error:
57
            s.close()
58
            continue
59
60
        s.send("stats\r\n")
61
        data = s.recv(1024)
62
        s.close()
63
64
        stats = {}
65
66
        for line in data.splitlines():
67
            info = line.split(" ")
68
69
            if info[0] == "STAT":
70
                try:
71
                    value = int(info[2])
72
                except ValueError:
73
                    value = info[2]
74
75
                stats[info[1]] = value
76
77
        if stats['cmd_get'] == 0:
78
            stats['hit_rate'] = 0
79
            stats['miss_rate'] = 0
80
        else:
81
            stats['hit_rate'] = 100 * stats['get_hits'] / stats['cmd_get']
82
            stats['miss_rate'] = 100 * stats['get_misses'] / stats['cmd_get']
83
84
        all_stats.append((hostname, stats))
85
86
    return all_stats
/trunk/reviewboard/admin/urls.py
New File
1
from django.conf.urls.defaults import *
2
3
4
NEWS_FEED = "http://www.review-board.org/news/feed/"
5
6
urlpatterns = patterns('reviewboard.admin.views',
7
    ('^$', 'dashboard'),
8
    ('^cache/$', 'cache_stats'),
9
)
10
11
urlpatterns += patterns('',
12
    ('^db/', include('django.contrib.admin.urls')),
13
    ('^feed/news/$', 'djblets.feedview.views.view_feed',
14
     {'template_name': 'admin/feed.html',
15
      'url': NEWS_FEED}),
16
    ('^feed/news/rss/$', 'django.views.generic.simple.redirect_to',
17
     {'url': NEWS_FEED}),
18
)
/trunk/reviewboard/admin/views.py
Revision 1384 New Change
1
from django.contrib.admin.views.decorators import staff_member_required
2
from django.contrib.auth.models import User
3
from django.core.cache import cache
4
from django.http import Http404, HttpResponse
1
from django.shortcuts import render_to_response
5
from django.shortcuts import render_to_response
2
from django.template.context import RequestContext
6
from django.template.context import RequestContext
3
from django.template.loader import render_to_string
7
from django.template.loader import render_to_string
8
from django.utils.translation import ugettext as _
4
9
10
from reviewboard import VERSION
5
from reviewboard.admin.checks import check_updates_required
11
from reviewboard.admin.checks import check_updates_required
12
from reviewboard.admin.cache_stats import get_cache_stats, get_has_cache_stats
13
from reviewboard.reviews.models import Group, DefaultReviewer
14
from reviewboard.scmtools.models import Repository
15
16
17
@staff_member_required
18
def dashboard(request, template_name="admin/dashboard.html"):
19
    """
20
    Displays the administration dashboard, containing news updates and
21
    useful administration tasks.
22
    """
23
    return render_to_response(template_name, RequestContext(request, {
24
        'user_count': User.objects.count(),
25
        'reviewgroup_count': Group.objects.count(),
26
        'defaultreviewer_count': DefaultReviewer.objects.count(),
27
        'repository_count': Repository.objects.count(),
28
        'has_cache_stats': get_has_cache_stats(),
29
        'title': _("Dashboard"),
30
        'version': VERSION,
31
    }))
32
33
34
@staff_member_required
35
def cache_stats(request, template_name="admin/cache_stats.html"):
36
    """
37
    Displays statistics on the cache. This includes such pieces of
38
    information as memory used, cache misses, and uptime.
39
    """
40
    # Make sure we're using memcache
41
    cache_stats = get_cache_stats()
42
43
    return render_to_response(template_name, RequestContext(request, {
44
        'cache_hosts': cache_stats,
45
        'cache_backend': cache.__module__,
46
        'title': _("Server Cache"),
47
    }))
6
48
7
49
8
def manual_updates_required(request,
50
def manual_updates_required(request,
9
                            template_name="admin/manual_updates_required.html"):
51
                            template_name="admin/manual_updates_required.html"):
52
    """
53
    Checks for required manual updates and displays informational pages on
54
    performing the necessary updates.
55
    """
10
    updates = check_updates_required()
56
    updates = check_updates_required()
11
57
12
    return render_to_response(template_name, RequestContext(request, {
58
    return render_to_response(template_name, RequestContext(request, {
13
        'updates': [render_to_string(template_name,
59
        'updates': [render_to_string(template_name,
14
                                     RequestContext(request, extra_context))
60
                                     RequestContext(request, extra_context))
15
                    for (template_name, extra_context) in updates],
61
                    for (template_name, extra_context) in updates],
16
    }))
62
    }))
/trunk/reviewboard/htdocs/media/admin/css/dashboard.css
Revision 1384 New Change
1
@import url('base.css');
1
@import url('base.css');
2
2
3
/* DASHBOARD */
3
/* DASHBOARD */
4
.dashboard .module table th { width:100%; }
4
.dashboard .module table th { width:100%; }
5
.dashboard .module table td { white-space:nowrap; }
5
.dashboard .module table td { white-space:nowrap; }
6
.dashboard .module table td a { display:block; padding-right:.6em; }
6
.dashboard .module table td a { display:block; padding-right:.6em; }
7
7
8
/*  RECENT ACTIONS MODULE  */
8
/*  RECENT ACTIONS MODULE  */
9
.module ul.actionlist { margin-left:0; }
9
.module ul.actionlist { margin-left:0; }
10
ul.actionlist li { list-style-type:none; }
10
ul.actionlist li { list-style-type:none; }
/trunk/reviewboard/htdocs/media/rb/css/admin.css
New File
1
#admin-nav {
2
  background-color: #557ab1;
3
  float: left;
4
  list-style: none;
5
  margin: 0;
6
  padding: 0 10px;
7
  width: 100%;
8
}
9
10
#admin-nav li {
11
  display: block;
12
  float: left;
13
  line-height: normal;
14
  margin: 0 10px 0 0;
15
  padding: 0;
16
}
17
18
#admin-nav li a {
19
  display: block;
20
  float: left;
21
  font-size: 110%;
22
  padding: 4px 8px;
23
}
24
25
#admin-nav li a:hover {
26
  background-color: #2f5183;
27
  text-decoration: none;
28
}
29
30
#admin-nav li a.active {
31
  background-color: #173663;
32
}
33
34
.module.manage .count {
35
  color: #A0A0A0;
36
  font-size: x-small;
37
  font-weight: normal;
38
}
39
40
.module.news {
41
  margin-top: 10px;
42
  width: 100%;
43
}
44
45
.module caption a {
46
  color: white;
47
}
48
49
.module caption img {
50
  vertical-align: bottom;
51
}
52
53
.module caption span.actions {
54
  float: right;
55
}
56
57
.module caption span.title {
58
  float: left;
59
}
60
61
.module caption {
62
  width: 100%;
63
}
64
65
/***************************************************************************
66
 * Django Admin overrides
67
 ***************************************************************************/
68
table thead th.ascending a {
69
  background: transparent url("../../djblets/images/datagrid/sort_asc_primary.png") no-repeat right 0.4em;
70
}
71
72
thead th, tfoot td {
73
  background: transparent url("../../djblets/images/datagrid/header_bg.png") repeat-x;
74
  padding: 5px;
75
  border-bottom: 1px solid #999999;
76
  border-right: 1px solid #CCCCCC;
77
  border-top: 1px solid #999999;
78
  border-left: 0 !important;
79
}
80
81
thead th a:link, thead th a:visited {
82
  color: black;
83
}
84
85
#branding h1 {
86
  color: #EEEEEE;
87
}
88
89
#content-related .module h2 {
90
  background: #5988bb;
91
  color: white;
92
}
93
94
#header {
95
  background-color: #2f5183;
96
}
97
98
.dashboard #content {
99
  width: auto;
100
}
101
102
.module h2, .module caption {
103
  background: #5988bb;
104
}
105
106
.module {
107
  border-color: #C0C0C0;
108
}
109
110
.row1 {
111
  background: #F2F2F2;
112
}
113
114
td, th {
115
  border-bottom: 0;
116
}
117
118
/* Override the way links and wrapping work in modules. */
119
.module.news table td {
120
  white-space: normal;
121
  padding: 0;
122
}
123
124
.module.news table td a {
125
  display: inline;
126
  padding: 0;
127
}
128
129
.module.news .entry {
130
  padding-bottom: 0.8em;
131
}
132
133
.module.news .entry-title {
134
  padding-top: 0.4em;
135
  padding-bottom: 0.8em;
136
}
137
138
.module.news .entry-content {
139
  margin-left: 1.2em;
140
}
141
142
.module.news .loading-indicator {
143
  padding: 4px;
144
}
145
146
.module.news .feed-error {
147
  margin: 0;
148
  padding: 4px;
149
}
/trunk/reviewboard/templates/base.html
Revision 1384 New Change
39
    <ul id="accountnav">
39
    <ul id="accountnav">
40
    {% if user.is_authenticated %}
40
    {% if user.is_authenticated %}
41
     {% blocktrans with user|realname|escape as username %}<li>Welcome, <b>{{username}}</b></li>{% endblocktrans %}
41
     {% blocktrans with user|realname|escape as username %}<li>Welcome, <b>{{username}}</b></li>{% endblocktrans %}
42
     <li>- <a href="{% url user-preferences %}">{% trans "My account" %}</a></li>
42
     <li>- <a href="{% url user-preferences %}">{% trans "My account" %}</a></li>
43
{% if user.is_staff %}
43
{% if user.is_staff %}
44
     <li>- <a href="{% url django.contrib.admin.views.main.index %}">{% trans "Admin" %}</a></li>
44
     <li>- <a href="{% url reviewboard.admin.views.dashboard %}">{% trans "Admin" %}</a></li>
45
{% endif %}
45
{% endif %}
46
     <li>- <a href="{% url logout %}">{% trans "Log out" %}</a></li>
46
     <li>- <a href="{% url logout %}">{% trans "Log out" %}</a></li>
47
    {% else %}
47
    {% else %}
48
     <li><a href="{% url login %}?next_page={{request.path}}">{% trans "Log in" %}</a></li>
48
     <li><a href="{% url login %}?next_page={{request.path}}">{% trans "Log in" %}</a></li>
49
     {% if settings.BUILTIN_AUTH %}
49
     {% if settings.BUILTIN_AUTH %}
/trunk/reviewboard/templates/admin/base_site.html
Revision 1384 New Change
1
{% extends "admin/base.html" %}
1
{% extends "admin/base.html" %}
2
{% load i18n %}
2
{% load i18n %}
3
3
4
{% block title %}{{title}} | {% trans "Review Board administration" %}{% endblock %}
4
{% block title %}{{title}} | {% trans "Review Board administration" %}{% endblock %}
5
5
6
{% block extrastyle %}
7
<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}rb/css/admin.css" />
8
<link rel="icon" type="image/png" href="{{MEDIA_URL}}rb/images/favicon.png" />
9
{% endblock %}
10
6
{% block branding %}
11
{% block branding %}
7
<h1 id="site-name">{% trans "Review Board administration" %}</h1>
12
<h1 id="site-name">{% trans "Review Board administration" %}</h1>
8
{% endblock %}
13
{% endblock %}
9
14
10
{% block nav-global %}{% endblock %}
15
{% block breadcrumbs %}
16
<div class="breadcrumbs"><a href="{{SITE_ROOT}}admin/">{% trans "Home" %}</a> {% if title %} &rsaquo; {{ title|escape }}{% endif %}</div>
17
{% endblock %}
18
19
{% block nav-global %}
20
{%  if user.is_authenticated %}
21
<ul id="admin-nav">
22
 <li><a href="{% url reviewboard.admin.views.dashboard %}">Dashboard</a></li>
23
 <li><a href="{% url django.contrib.admin.views.main.index %}">Database</a></li>
24
</ul>
25
{%  endif %}
26
{% endblock %}
/trunk/reviewboard/templates/admin/cache_stats.html
New File
1
{% extends "admin/base_site.html" %}
2
{% load i18n %}
3
4
{% block content %}
5
<b>Cache backend:</b> {{cache_backend}}
6
7
<h2>Statistics</h2>
8
{% if cache_hosts %}
9
{%  for hostname,stats in cache_hosts %}
10
<div class="module">
11
 <table>
12
  <caption>{{hostname}}</caption>
13
  <tr>
14
   <th scope="row">{% trans "Memory usage:" %}</th>
15
   <td>{{stats.bytes|filesizeformat}}</td>
16
  </tr>
17
  <tr>
18
   <th scope="row">{% trans "Keys in cache:" %}</th>
19
   <td>{{stats.curr_items}} of {{stats.total_items}}</td>
20
  </tr>
21
  <tr>
22
   <th scope="row">{% trans "Cache hits:" %}</th>
23
   <td>{{stats.get_hits}} of {{stats.cmd_get}}: {{stats.hit_rate}}%</td>
24
  </tr>
25
  <tr>
26
   <th scope="row">{% trans "Cache misses:" %}</th>
27
   <td>{{stats.get_misses}} of {{stats.cmd_get}}: {{stats.miss_rate}}%</td>
28
  </tr>
29
  <tr>
30
   <th scope="row">{% trans "Cache evictions:" %}</th>
31
   <td>{{stats.evictions}}</td>
32
  </tr>
33
  <tr>
34
   <th scope="row">{% trans "Cache traffic:" %}</th>
35
   <td>{{stats.bytes_read|filesizeformat}} in,
36
       {{stats.bytes_written|filesizeformat}} out</td>
37
  </tr>
38
  <tr>
39
   <th scope="row">{% trans "Uptime:" %}</th>
40
   <td>{{stats.uptime}}</td>
41
  </tr>
42
 </table>
43
</div>
44
{%  endfor %}
45
{% else %}
46
<p>{% trans "Statistics are not available for this backend." %}</p>
47
{% endif %}
48
49
{% endblock %}
/trunk/reviewboard/templates/admin/dashboard.html
New File
1
{% extends "admin/base_site.html" %}
2
{% load adminmedia %}
3
{% load adminapplist %}
4
{% load i18n %}
5
{% load log %}
6
7
{% block stylesheet %}{% admin_media_prefix %}css/dashboard.css{% endblock %}
8
{% block coltype %}colM{% endblock %}
9
{% block bodyclass %}dashboard{% endblock %}
10
{% block breadcrumbs %}{% endblock %}
11
12
{% block extrahead %}
13
<script type="text/javascript" src="{{MEDIA_URL}}yui/yahoo/yahoo-min.js"></script>
14
<script type="text/javascript" src="{{MEDIA_URL}}yui/connection/connection-min.js"></script>
15
<script type="text/javascript" src="{{MEDIA_URL}}yui/dom/dom-min.js"></script>
16
<script type="text/javascript" src="{{MEDIA_URL}}yui/event/event-min.js"></script>
17
<script type="text/javascript" src="{{MEDIA_URL}}yui-ext/yui-ext.js"></script>
18
{% endblock %}
19
20
{% block content %}
21
<div class="colMS">
22
 <div id="content-main">
23
  {# "Manage" section #}
24
  <div class="module manage">
25
   <table summary="{% trans "Common management operations." %}">
26
    <caption>{% trans "Manage" %}</caption>
27
    <tbody>
28
     <tr>
29
      <th scope="row"><a href="db/auth/user/">{% trans "Users" %}</a>
30
        <span class="count">({{user_count}})</span></th>
31
      <td><a class="addlink" href="db/auth/user/add/">{% trans "Add" %}</a></td>
32
     </tr>
33
     <tr>
34
      <th scope="row"><a href="db/reviews/group/">{% trans "Review groups" %}</a>
35
        <span class="count">({{reviewgroup_count}})</span></th>
36
      <td><a class="addlink" href="db/reviews/group/add/">{% trans "Add" %}</a></td>
37
     </tr>
38
     <tr>
39
      <th scope="row"><a href="db/reviews/defaultreviewer/">{% trans "Default reviewers" %}</a>
40
        <span class="count">({{defaultreviewer_count}})</span></th>
41
      <td><a class="addlink" href="db/reviews/defaultreviewer/add/">{% trans "Add" %}