-
-
-
diff --git a/etc/status.js b/etc/status.js
deleted file mode 100644
index c9532e4f06..0000000000
--- a/etc/status.js
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright 2012 OpenStack Foundation
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may
-// not use this file except in compliance with the License. You may obtain
-// a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations
-// under the License.
-
-window.zuul_enable_status_updates = true;
-
-function format_pipeline(data) {
- var html = '
'+
- data['name']+'
';
- if (data['description'] != null) {
- html += '
'
- }
- $.each(head, function(change_i, change) {
- if (change_i > 0) {
- html += '
↑
'
- }
- html += format_change(change);
- });
- });
- });
-
- html += '
';
- return html;
-}
-
-function format_change(change) {
- var html = '
';
-
- html += ''+change['project']+'';
- var id = change['id'];
- var url = change['url'];
- if (id.length == 40) {
- id = id.substr(0,7);
- }
- html += '';
- if (url != null) {
- html += '';
- }
- html += id;
- if (url != null) {
- html += '';
- }
- html += '
';
-
- $.each(change['jobs'], function(i, job) {
- result = job['result'];
- var result_class = "result";
- if (result == null) {
- if (job['url'] != null) {
- result = 'in progress';
- } else {
- result = 'queued';
- }
- } else if (result == 'SUCCESS') {
- result_class += " result_success";
- } else if (result == 'FAILURE') {
- result_class += " result_failure";
- } else if (result == 'LOST') {
- result_class += " result_unstable";
- } else if (result == 'UNSTABLE') {
- result_class += " result_unstable";
- }
- html += '';
- if (job['url'] != null) {
- html += '';
- }
- html += job['name'];
- if (job['url'] != null) {
- html += '';
- }
- html += ': '+result+'';
- if (job['voting'] == false) {
- html += ' (non-voting)';
- }
- html += '';
- });
-
- html += '
';
- return html;
-}
-
-function update_timeout() {
- if (!window.zuul_enable_status_updates) {
- setTimeout(update_timeout, 5000);
- return;
- }
-
- update();
-
- setTimeout(update_timeout, 5000);
-}
-
-function update() {
- var html = '';
-
- $.getJSON('/status.json', function(data) {
- if ('message' in data) {
- $("#message-container").attr('class', 'topMessage');
- $("#message").html(data['message']);
- } else {
- $("#message-container").removeClass('topMessage');
- $("#message").html('');
- }
-
- html += ' ';
-
- $.each(data['pipelines'], function(i, pipeline) {
- html = html + format_pipeline(pipeline);
- });
-
- html += ' ';
- $("#pipeline-container").html(html);
- });
-}
-
-$(function() {
- update_timeout();
-
- $(document).on({
- 'show.visibility': function() {
- window.zuul_enable_status_updates = true;
- update();
- },
- 'hide.visibility': function() {
- window.zuul_enable_status_updates = false;
- }
- });
-
-});
diff --git a/etc/status/.gitignore b/etc/status/.gitignore
new file mode 100644
index 0000000000..a4b95708a1
--- /dev/null
+++ b/etc/status/.gitignore
@@ -0,0 +1,3 @@
+public_html/jquery.min.js
+public_html/jquery-visibility.min.js
+public_html/bootstrap
diff --git a/etc/status/README.rst b/etc/status/README.rst
new file mode 100644
index 0000000000..5b4a6bb104
--- /dev/null
+++ b/etc/status/README.rst
@@ -0,0 +1,27 @@
+Zuul Status
+====
+
+Zuul Status is a web portal for a Zuul server.
+
+Set up
+------------
+
+The markup generated by the javascript is fairly generic so it should be easy
+to drop into an existing portal. All it needs is
+````.
+
+Having said that, the markup is optimised for Twitter Bootstrap, though it in
+no way depends on Boostrap and any element using a bootstrap class has a
+``zuul-`` prefixed class alongside it.
+
+The script depends on jQuery (tested with version 1.8 and 1.9).
+
+The script optimises updates by stopping when the page is not visible.
+This is done by listerning to ``show`` and ``hide`` events emitted by the
+Page Visibility plugin for jQuery. If you don't want to load this plugin you
+can undo undo this optimisation by removing the 9 lines using this on the
+bottom of ``app.js``
+
+To automatically fetch the latest versions of jQuery, the Page Visibility
+plugin and Twitter Boostrap, run the ``fetch-dependencies.sh`` script.
+The default ``index.html`` references these.
diff --git a/etc/status/fetch-dependencies.sh b/etc/status/fetch-dependencies.sh
new file mode 100755
index 0000000000..f224557809
--- /dev/null
+++ b/etc/status/fetch-dependencies.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+BASE_DIR=$(cd $(dirname $0); pwd)
+echo "Destination: $BASE_DIR/public_html"
+echo "Fetching jquery.min.js..."
+curl --silent http://code.jquery.com/jquery.min.js > $BASE_DIR/public_html/jquery.min.js
+echo "Fetching jquery-visibility.min.js..."
+curl --silent https://raw.github.com/mathiasbynens/jquery-visibility/master/jquery-visibility.min.js > $BASE_DIR/public_html/jquery-visibility.min.js
+echo "Fetching bootstrap..."
+curl --silent http://twitter.github.io/bootstrap/assets/bootstrap.zip > bootstrap.zip && unzip -q -o bootstrap.zip -d $BASE_DIR/public_html/ && rm bootstrap.zip
diff --git a/etc/status/public_html/app.js b/etc/status/public_html/app.js
new file mode 100644
index 0000000000..f7de2cce2d
--- /dev/null
+++ b/etc/status/public_html/app.js
@@ -0,0 +1,241 @@
+// Client script for Zuul status page
+//
+// Copyright 2012 OpenStack Foundation
+// Copyright 2013 Timo Tijhof
+// Copyright 2013 Wikimedia Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+(function ($) {
+ var $container, $msg, $msgWrap, $indicator, $queueInfo, $queueEventsNum, $queueResultsNum, $pipelines,
+ prevHtml, xhr, zuul, $jq,
+ demo = location.search.match(/[?&]demo=([^?&]*)/),
+ source = demo ?
+ './status-' + (demo[1] || 'basic') + '.json-sample' :
+ '/zuul/status.json';
+
+ zuul = {
+ enabled: true,
+
+ schedule: function () {
+ if (!zuul.enabled) {
+ setTimeout(zuul.schedule, 5000);
+ return;
+ }
+ zuul.update().complete(function () {
+ setTimeout(zuul.schedule, 5000);
+ });
+ },
+
+ /** @return {jQuery.Promise} */
+ update: function () {
+ // Cancel the previous update if it hasn't completed yet.
+ if (xhr) {
+ xhr.abort();
+ }
+
+ zuul.emit('update-start');
+
+ xhr = $.ajax({
+ url: source,
+ dataType: 'json',
+ cache: false
+ })
+ .done(function (data) {
+ var html = '';
+ data = data || {};
+
+ if ('message' in data) {
+ $msg.html(data.message);
+ $msgWrap.removeClass('zuul-msg-wrap-off');
+ } else {
+ $msg.empty();
+ $msgWrap.addClass('zuul-msg-wrap-off');
+ }
+
+ $.each(data.pipelines, function (i, pipeline) {
+ html += zuul.format.pipeline(pipeline);
+ });
+
+ // Only re-parse the DOM if we have to
+ if (html !== prevHtml) {
+ prevHtml = html;
+ $pipelines.html(html);
+ }
+
+ $queueEventsNum.text(
+ data.trigger_event_queue ? data.trigger_event_queue.length : '0'
+ );
+ $queueResultsNum.text(
+ data.result_event_queue ? data.result_event_queue.length : '0'
+ );
+ })
+ .fail(function (err, jqXHR, errMsg) {
+ $msg.text(source + ': ' + errMsg).show();
+ $msgWrap.removeClass('zuul-msg-wrap-off');
+ })
+ .complete(function () {
+ xhr = undefined;
+ zuul.emit('update-end');
+ });
+
+ return xhr;
+ },
+
+ format: {
+ change: function (change) {
+ var html = '
',
+ id = change.id,
+ url = change.url;
+
+ html += '
';
+ return html;
+ },
+
+ pipeline: function (pipeline) {
+ var html = '
' +
+ pipeline.name + '
';
+ if (typeof pipeline.description === 'string') {
+ html += '
' + pipeline.description + '
';
+ }
+
+ $.each(pipeline.change_queues, function (queueNum, changeQueue) {
+ $.each(changeQueue.heads, function (headNum, changes) {
+ if (pipeline.change_queues.length > 1 && headNum === 0) {
+ var name = changeQueue.name;
+ html += '
Queue: ';
+ if (name.length > 32) {
+ name = name.substr(0, 32) + '...';
+ }
+ html += name + '
';
+ }
+ $.each(changes, function (changeNum, change) {
+ // If there are multiple changes in the same head it means they're connected
+ if (changeNum > 0) {
+ html += '
↑
';
+ }
+ html += zuul.format.change(change);
+ });
+ });
+ });
+
+ html += '
';
+ return html;
+ }
+ },
+
+ emit: function () {
+ $jq.trigger.apply($jq, arguments);
+ return this;
+ },
+ on: function () {
+ $jq.on.apply($jq, arguments);
+ return this;
+ },
+ one: function () {
+ $jq.one.apply($jq, arguments);
+ return this;
+ }
+ };
+
+ $jq = $(zuul);
+
+ $jq.on('update-start', function () {
+ $container.addClass('zuul-container-loading');
+ $indicator.addClass('zuul-spinner-on');
+ });
+
+ $jq.on('update-end', function () {
+ $container.removeClass('zuul-container-loading');
+ setTimeout(function () {
+ $indicator.removeClass('zuul-spinner-on');
+ }, 550);
+ });
+
+ $jq.one('update-end', function () {
+ // Do this asynchronous so that if the first update adds a message, it will not animate
+ // while we fade in the content. Instead it simply appears with the rest of the content.
+ setTimeout(function () {
+ $container.addClass('zuul-container-ready'); // Fades in the content
+ });
+ });
+
+ $(function ($) {
+ $msg = $('');
+ $msgWrap = $msg.wrap('').parent();
+ $indicator = $('updating ');
+ $queueInfo = $('