summaryrefslogtreecommitdiffstats
path: root/WebKitTools/QueueStatusServer
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/QueueStatusServer')
-rw-r--r--WebKitTools/QueueStatusServer/handlers/dashboard.py45
-rw-r--r--WebKitTools/QueueStatusServer/handlers/statusbubble.py37
-rw-r--r--WebKitTools/QueueStatusServer/handlers/updatestatus.py9
-rw-r--r--WebKitTools/QueueStatusServer/handlers/updateworkitems.py64
-rw-r--r--WebKitTools/QueueStatusServer/main.py3
-rw-r--r--WebKitTools/QueueStatusServer/model/attachment.py50
-rw-r--r--WebKitTools/QueueStatusServer/model/queues.py10
-rw-r--r--WebKitTools/QueueStatusServer/model/workitems.py35
-rw-r--r--WebKitTools/QueueStatusServer/templates/dashboard.html56
-rw-r--r--WebKitTools/QueueStatusServer/templates/statusbubble.html52
-rw-r--r--WebKitTools/QueueStatusServer/templates/updateworkitems.html8
11 files changed, 270 insertions, 99 deletions
diff --git a/WebKitTools/QueueStatusServer/handlers/dashboard.py b/WebKitTools/QueueStatusServer/handlers/dashboard.py
index 80f30ec..bbb65b8 100644
--- a/WebKitTools/QueueStatusServer/handlers/dashboard.py
+++ b/WebKitTools/QueueStatusServer/handlers/dashboard.py
@@ -26,16 +26,55 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import operator
+
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from model.attachment import Attachment
+from model.queues import queues
+
class Dashboard(webapp.RequestHandler):
- def get(self):
- attachments = Attachment.recent(limit=25)
+ # FIXME: This list probably belongs as part of a Queue object in queues.py
+ # Arrays are bubble_name, queue_name
+ # FIXME: Can this be unified with StatusBubble._queues_to_display?
+ _queues_to_display = [
+ ["Style", "style-queue"],
+ ["Cr-Linux", "chromium-ews"],
+ ["Cr-Win", "cr-win-ews"],
+ ["Qt", "qt-ews"],
+ ["Gtk", "gtk-ews"],
+ ["Mac", "mac-ews"],
+ ["Win", "win-ews"],
+ ["Commit", "commit-queue"],
+ ]
+ # Split the zipped list into component parts
+ _header_names, _ordered_queue_names = zip(*_queues_to_display)
+
+ # This asserts that all of the queues listed above are valid queue names.
+ assert(reduce(operator.and_, map(lambda name: name in queues, _ordered_queue_names)))
+
+ def _build_bubble(self, attachment, queue_name):
+ queue_status = attachment.status_for_queue(queue_name)
+ bubble = {
+ "status_class": attachment.state_from_queue_status(queue_status) if queue_status else "none",
+ "status_date": queue_status.date if queue_status else None,
+ }
+ return bubble
+
+ def _build_row(self, attachment):
+ row = {
+ "bug_id": attachment.bug_id(),
+ "attachment_id": attachment.id,
+ "bubbles": [self._build_bubble(attachment, queue_name) for queue_name in self._ordered_queue_names],
+ }
+ return row
+
+ def get(self):
template_values = {
- "summaries" : [attachment.summary() for attachment in attachments],
+ "headers": self._header_names,
+ "rows": [self._build_row(attachment) for attachment in Attachment.recent(limit=25)],
}
self.response.out.write(template.render("templates/dashboard.html", template_values))
diff --git a/WebKitTools/QueueStatusServer/handlers/statusbubble.py b/WebKitTools/QueueStatusServer/handlers/statusbubble.py
index d52509f..0e2b8de 100644
--- a/WebKitTools/QueueStatusServer/handlers/statusbubble.py
+++ b/WebKitTools/QueueStatusServer/handlers/statusbubble.py
@@ -26,17 +26,50 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import operator
+
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from model.attachment import Attachment
+from model.workitems import WorkItems
+from model.queues import queues, name_with_underscores
class StatusBubble(webapp.RequestHandler):
+ # FIXME: This list probably belongs as part of a Queue object in queues.py
+ # Arrays are bubble_name, queue_name
+ _queues_to_display = [
+ ["style", "style-queue"],
+ ["cr-linux", "chromium-ews"],
+ ["cr-win", "cr-win-ews"],
+ ["gtk", "gtk-ews"],
+ ["qt", "qt-ews"],
+ ["mac", "mac-ews"],
+ ["win", "win-ews"],
+ ]
+
+ # This asserts that all of the queues listed above are valid queue names.
+ assert(reduce(operator.and_, map(lambda name_pair: name_pair[1] in queues, _queues_to_display)))
+
+ def _build_bubble(self, queue_name_pair, attachment):
+ bubble_name = queue_name_pair[0]
+ queue_name = queue_name_pair[1]
+
+ queue_status = attachment.status_for_queue(queue_name)
+ bubble = {
+ "name": bubble_name,
+ "attachment_id": attachment.id,
+ "queue_position": attachment.position_in_queue(queue_name),
+ "state": attachment.state_from_queue_status(queue_status) if queue_status else "none",
+ "status": queue_status,
+ }
+ return bubble
+
def get(self, attachment_id):
attachment = Attachment(int(attachment_id))
-
+ bubbles = [self._build_bubble(name_pair, attachment) for name_pair in self._queues_to_display]
template_values = {
- "summary" : attachment.summary()
+ "bubbles": bubbles,
}
self.response.out.write(template.render("templates/statusbubble.html", template_values))
diff --git a/WebKitTools/QueueStatusServer/handlers/updatestatus.py b/WebKitTools/QueueStatusServer/handlers/updatestatus.py
index 50d4b6e..5a93dbd 100644
--- a/WebKitTools/QueueStatusServer/handlers/updatestatus.py
+++ b/WebKitTools/QueueStatusServer/handlers/updatestatus.py
@@ -38,9 +38,10 @@ class UpdateStatus(UpdateBase):
def get(self):
self.response.out.write(template.render("templates/updatestatus.html", None))
- def post(self):
+ def _queue_status_from_request(self):
queue_status = QueueStatus()
+ # FIXME: I think this can be removed, no one uses it.
if users.get_current_user():
queue_status.author = users.get_current_user()
@@ -53,6 +54,10 @@ class UpdateStatus(UpdateBase):
queue_status.message = self.request.get("status")
results_file = self.request.get("results_file")
queue_status.results_file = db.Blob(str(results_file))
+ return queue_status
+
+ def post(self):
+ queue_status = self._queue_status_from_request()
queue_status.put()
- Attachment.dirty(patch_id)
+ Attachment.dirty(queue_status.active_patch_id)
self.response.out.write(queue_status.key().id())
diff --git a/WebKitTools/QueueStatusServer/handlers/updateworkitems.py b/WebKitTools/QueueStatusServer/handlers/updateworkitems.py
new file mode 100644
index 0000000..b58e743
--- /dev/null
+++ b/WebKitTools/QueueStatusServer/handlers/updateworkitems.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.appengine.ext import webapp, db
+from google.appengine.ext.webapp import template
+
+from handlers.updatebase import UpdateBase
+from model.queues import queues
+from model.workitems import WorkItems
+
+from datetime import datetime
+
+
+class UpdateWorkItems(UpdateBase):
+ def get(self):
+ self.response.out.write(template.render("templates/updateworkitems.html", None))
+
+ def _work_items_for_queue(self, queue_name):
+ if queue_name not in queues:
+ self.response.set_status(500)
+ return
+ work_items = WorkItems.all().filter("queue_name =", queue_name).get()
+ if not work_items:
+ work_items = WorkItems()
+ work_items.queue_name = queue_name
+ return work_items
+
+ def _work_items_from_request(self):
+ queue_name = self.request.get("queue_name")
+ work_items = self._work_items_for_queue(queue_name)
+ items_string = self.request.get("work_items")
+ # Our parsing could be much more robust.
+ work_items.item_ids = map(int, items_string.split(" "))
+ work_items.date = datetime.now()
+ return work_items
+
+ def post(self):
+ work_items = self._work_items_from_request()
+ work_items.put()
diff --git a/WebKitTools/QueueStatusServer/main.py b/WebKitTools/QueueStatusServer/main.py
index 8bfa7e9..fb6fc4b 100644
--- a/WebKitTools/QueueStatusServer/main.py
+++ b/WebKitTools/QueueStatusServer/main.py
@@ -43,6 +43,8 @@ from handlers.statusbubble import StatusBubble
from handlers.svnrevision import SVNRevision
from handlers.updatestatus import UpdateStatus
from handlers.updatesvnrevision import UpdateSVNRevision
+from handlers.updateworkitems import UpdateWorkItems
+
webapp.template.register_template_library('filters.webkit_extras')
@@ -57,6 +59,7 @@ routes = [
(r'/svn-revision/(.*)', SVNRevision),
(r'/queue-status/(.*)', RecentStatus),
('/update-status', UpdateStatus),
+ ('/update-work-items', UpdateWorkItems),
('/update-svn-revision', UpdateSVNRevision),
]
diff --git a/WebKitTools/QueueStatusServer/model/attachment.py b/WebKitTools/QueueStatusServer/model/attachment.py
index 751f78e..9ae59e8 100644
--- a/WebKitTools/QueueStatusServer/model/attachment.py
+++ b/WebKitTools/QueueStatusServer/model/attachment.py
@@ -30,8 +30,9 @@ import re
from google.appengine.api import memcache
-from model.queues import queues
+from model.queues import queues, name_with_underscores
from model.queuestatus import QueueStatus
+from model.workitems import WorkItems
class Attachment(object):
@@ -60,6 +61,7 @@ class Attachment(object):
def __init__(self, attachment_id):
self.id = attachment_id
self._summary = None
+ self._cached_queue_positions = None
def summary(self):
if self._summary:
@@ -71,11 +73,7 @@ class Attachment(object):
memcache.set(str(self.id), self._summary, namespace="attachment-summary")
return self._summary
- def _dash_to_underscore(self, dashed_name):
- regexp = re.compile("-")
- return regexp.sub("_", dashed_name)
-
- def _state_from_status(self, status):
+ def state_from_queue_status(self, status):
table = {
"Pass" : "pass",
"Fail" : "fail",
@@ -89,6 +87,40 @@ class Attachment(object):
return "pending"
return None
+ def position_in_queue(self, queue_name):
+ return self._queue_positions().get(queue_name)
+
+ def status_for_queue(self, queue_name):
+ underscore_queue_name = name_with_underscores(queue_name)
+ # summary() is a horrible API and should be killed.
+ queue_summary = self.summary().get(underscore_queue_name)
+ if not queue_summary:
+ return None
+ return queue_summary.get("status")
+
+ def bug_id(self):
+ return self.summary().get("bug_id")
+
+ def _queue_positions(self):
+ if self._cached_queue_positions:
+ return self._cached_queue_positions
+ # FIXME: Should we be mem-caching this?
+ self._cached_queue_positions = self._calculate_queue_positions()
+ return self._cached_queue_positions
+
+ def _calculate_queue_positions(self):
+ queue_positions = {}
+ for work_items in WorkItems.all().fetch(limit=len(queues)):
+ queue_name = str(work_items.queue_name)
+ try:
+ position = work_items.item_ids.index(self.id)
+ # Display 1-based indecies to the user.
+ queue_positions[queue_name] = position + 1
+ except ValueError, e:
+ queue_positions[queue_name] = None
+ return queue_positions
+
+ # FIXME: This is controller/view code and does not belong in a model.
def _fetch_summary(self):
summary = { "attachment_id" : self.id }
@@ -102,8 +134,8 @@ class Attachment(object):
summary[queue] = None
status = QueueStatus.all().filter('queue_name =', queue).filter('active_patch_id =', self.id).order('-date').get()
if status:
- summary[self._dash_to_underscore(queue)] = {
- "state" : self._state_from_status(status),
- "status" : status,
+ summary[name_with_underscores(queue)] = {
+ "state": self.state_from_queue_status(status),
+ "status": status,
}
return summary
diff --git a/WebKitTools/QueueStatusServer/model/queues.py b/WebKitTools/QueueStatusServer/model/queues.py
index 57463de..46f2f15 100644
--- a/WebKitTools/QueueStatusServer/model/queues.py
+++ b/WebKitTools/QueueStatusServer/model/queues.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2010 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
@@ -26,6 +26,9 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import re
+
+
queues = [
"commit-queue",
"style-queue",
@@ -36,3 +39,8 @@ queues = [
"mac-ews",
"win-ews",
]
+
+
+def name_with_underscores(dashed_name):
+ regexp = re.compile("-")
+ return regexp.sub("_", dashed_name)
diff --git a/WebKitTools/QueueStatusServer/model/workitems.py b/WebKitTools/QueueStatusServer/model/workitems.py
new file mode 100644
index 0000000..3ea59cb
--- /dev/null
+++ b/WebKitTools/QueueStatusServer/model/workitems.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.appengine.ext import db
+
+
+class WorkItems(db.Model):
+ queue_name = db.StringProperty()
+ item_ids = db.ListProperty(int)
+ date = db.DateTimeProperty(auto_now_add=True)
diff --git a/WebKitTools/QueueStatusServer/templates/dashboard.html b/WebKitTools/QueueStatusServer/templates/dashboard.html
index 14b7ede..c5c2359 100644
--- a/WebKitTools/QueueStatusServer/templates/dashboard.html
+++ b/WebKitTools/QueueStatusServer/templates/dashboard.html
@@ -17,57 +17,27 @@ function statusDetail(patch_id) {
<tr>
<th>Bug</th>
<th>Attachment</th>
- <th>Style</th>
- <th>Cr-Linux</th>
- <th>Cr-Win</th>
- <th>Qt</th>
- <th>Mac</th>
- <th>Win</th>
- <th>Gtk</th>
- <th>Commit</th>
+ {% for header in headers %}
+ <th>{{ header }}</th>
+ {% endfor %}
</tr>
</thead>
- <tbody>{% for summary in summaries %}
+ <tbody>{% for row in rows %}
<tr>
<td class="status">
- {{ summary.bug_id|force_escape|webkit_bug_id|safe }}
+ {{ row.bug_id|force_escape|webkit_bug_id|safe }}
</td>
<td class="status">
- {{ summary.attachment_id|force_escape|webkit_attachment_id|safe }}
+ {{ row.attachment_id|force_escape|webkit_attachment_id|safe }}
</td>
- <!-- FIXME: Find some way to remove this copy-and-paste code! -->
- <td class="status {{ summary.style_queue.state }}"{% if summary.style_queue.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.style_queue.status.date|timesince }}"{% endif %}>
- </td>
- <td class="status {{ summary.chromium_ews.state }}"{% if summary.chromium_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.chromium_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.cr_win_ews.state }}"{% if summary.cr_win_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.cr_win_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.qt_ews.state }}"{% if summary.qt_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.qt_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.mac_ews.state }}"{% if summary.mac_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.mac_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.win_ews.state }}"{% if summary.win_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.win_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.gtk_ews.state }}"{% if summary.gtk_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.gtk_ews.status.date|timesince }} ago"{% endif %}>
- </td>
- <td class="status {{ summary.commit_queue.state }}"{% if summary.commit_queue.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.commit_queue.status.date|timesince }} ago"{% endif %}>
+ {% for bubble in row.bubbles %}
+ <td class="status {{ bubble.status_class }}"
+ {% if bubble.status %}
+ onclick="statusDetail({{ row.attachment_id }})"
+ title="{{ bubble.status_date|timesince }}"
+ {% endif %}>
</td>
+ {% endfor %}
</tr>{% endfor %}
</tbody>
</table>
diff --git a/WebKitTools/QueueStatusServer/templates/statusbubble.html b/WebKitTools/QueueStatusServer/templates/statusbubble.html
index c6e2134..3102741 100644
--- a/WebKitTools/QueueStatusServer/templates/statusbubble.html
+++ b/WebKitTools/QueueStatusServer/templates/statusbubble.html
@@ -18,26 +18,26 @@ body {
border: 1px solid #AAA;
background-color: white;
font-size: 11px;
+ cursor: pointer;
+}
+.none {
+ cursor: auto;
}
.pass {
background-color: #8FDF5F;
border: 1px solid #4F8530;
- cursor: pointer;
}
.fail {
background-color: #E98080;
border: 1px solid #A77272;
- cursor: pointer;
}
.pending {
background-color: #FFFC6C;
border: 1px solid #C5C56D;
- cursor: pointer;
}
.error {
background-color: #E0B0FF;
border: 1px solid #ACA0B3;
- cursor: pointer;
}
</style>
<script>
@@ -47,41 +47,15 @@ function statusDetail(patch_id) {
</script>
</head>
<body>
-<!-- FIXME: Find some way to remove this copy-and-paste code! -->
-<div class="status {{ summary.style_queue.state }}"{% if summary.style_queue.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.style_queue.status.date|timesince }} ago"{% endif %}>
- style
-</div>
-<div class="status {{ summary.chromium_ews.state }}"{% if summary.chromium_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.chromium_ews.status.date|timesince }} ago"{% endif %}>
- cr-linux
-</div>
-<div class="status {{ summary.cr_win_ews.state }}"{% if summary.cr_win_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.cr_win_ews.status.date|timesince }} ago"{% endif %}>
- cr-win
-</div>
-<div class="status {{ summary.qt_ews.state }}"{% if summary.qt_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.qt_ews.status.date|timesince }} ago"{% endif %}>
- qt
-</div>
-<div class="status {{ summary.gtk_ews.state }}"{% if summary.gtk_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.gtk_ews.status.date|timesince }} ago"{% endif %}>
- gtk
-</div>
-<div class="status {{ summary.mac_ews.state }}"{% if summary.mac_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.mac_ews.status.date|timesince }} ago"{% endif %}>
- mac
-</div>
-<div class="status {{ summary.win_ews.state }}"{% if summary.win_ews.status %}
- onclick="statusDetail({{ summary.attachment_id }})"
- title="{{ summary.win_ews.status.date|timesince }} ago"{% endif %}>
- win
+{% for bubble in bubbles %}
+<div class="status {{ bubble.state }}"{% if bubble.status %}
+ onclick="statusDetail({{ bubble.attachment_id }})"
+ title="{{ bubble.status.date|timesince }} ago"{% endif %}>
+ {{ bubble.name }}
+ {% if bubble.queue_position %}
+ (#{{ bubble.queue_position }})
+ {% endif %}
</div>
+{% endfor %}
</body>
</html>
diff --git a/WebKitTools/QueueStatusServer/templates/updateworkitems.html b/WebKitTools/QueueStatusServer/templates/updateworkitems.html
new file mode 100644
index 0000000..b086fc3
--- /dev/null
+++ b/WebKitTools/QueueStatusServer/templates/updateworkitems.html
@@ -0,0 +1,8 @@
+<form name="update_work_items" enctype="multipart/form-data" method="post">
+Update work items for a queue: <input name="queue_name">
+ <div>
+ Work Items:
+ <input name="work_items">
+ </div>
+ <div><input type="submit" value="Update Work Items"></div>
+</form>