Merge "Use yarn and webpack to manage zuul-web javascript"
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": ['env'],
|
||||
"plugins": [[
|
||||
"angularjs-annotate", {
|
||||
"explicitOnly": false
|
||||
}
|
||||
]]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
parser: babel-eslint
|
||||
plugins:
|
||||
- standard
|
||||
rules:
|
||||
camelcase: off
|
||||
indent:
|
||||
- off
|
||||
- 2
|
||||
extends:
|
||||
- ./node_modules/eslint-config-standard/eslintrc.json
|
|
@ -17,3 +17,6 @@ zuul/versioninfo
|
|||
dist/
|
||||
cover/
|
||||
htmlcov/
|
||||
zuul/web/static
|
||||
node_modules
|
||||
yarn-error.log
|
||||
|
|
55
.zuul.yaml
|
@ -29,6 +29,18 @@
|
|||
- playbooks/zuul-stream/.*
|
||||
- requirements.txt
|
||||
|
||||
- job:
|
||||
name: zuul-tox-py35
|
||||
parent: tox-py35
|
||||
description: |
|
||||
Runs javascript build before running python 35 unit tests.
|
||||
pre-run: playbooks/tox/pre.yaml
|
||||
run: playbooks/tox/run.yaml
|
||||
post-run: playbooks/tox/post.yaml
|
||||
vars:
|
||||
node_version: 8
|
||||
npm_command: build
|
||||
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
|
@ -39,10 +51,29 @@
|
|||
vars:
|
||||
sphinx_python: python3
|
||||
- tox-pep8
|
||||
- tox-py35:
|
||||
- zuul-tox-py35:
|
||||
irrelevant-files:
|
||||
- zuul/cmd/migrate.py
|
||||
- playbooks/zuul-migrate/.*
|
||||
- build-javascript-content:
|
||||
success-url: 'npm/html/status.html'
|
||||
files:
|
||||
- package.json
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- web/.*
|
||||
vars:
|
||||
javascript_content_dir: zuul/web/static
|
||||
npm_command: build:dist -- --define ZUUL_API_URL="'https://zuul.openstack.org'"
|
||||
- nodejs-npm-run-lint:
|
||||
vars:
|
||||
node_version: 8
|
||||
success-url: 'npm/reports/bundle.html'
|
||||
files:
|
||||
- package.json
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- web/.*
|
||||
- zuul-stream-functional
|
||||
- nodepool-zuul-functional:
|
||||
voting: false
|
||||
|
@ -55,12 +86,32 @@
|
|||
vars:
|
||||
sphinx_python: python3
|
||||
- tox-pep8
|
||||
- tox-py35:
|
||||
- zuul-tox-py35:
|
||||
irrelevant-files:
|
||||
- zuul/cmd/migrate.py
|
||||
- playbooks/zuul-migrate/.*
|
||||
- build-javascript-content:
|
||||
success-url: 'npm/html/status.html'
|
||||
files:
|
||||
- package.json
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- web/.*
|
||||
vars:
|
||||
javascript_content_dir: zuul/web/static
|
||||
npm_command: build:dist -- --define ZUUL_API_URL="'https://zuul.openstack.org'"
|
||||
- nodejs-npm-run-lint:
|
||||
vars:
|
||||
node_version: 8
|
||||
success-url: 'npm/reports/bundle.html'
|
||||
files:
|
||||
- package.json
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- web/.*
|
||||
- zuul-stream-functional
|
||||
post:
|
||||
jobs:
|
||||
- publish-openstack-sphinx-docs-infra-python3
|
||||
- publish-openstack-python-branch-tarball
|
||||
- publish-openstack-javascript-content
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
include AUTHORS
|
||||
include ChangeLog
|
||||
include zuul/web/static/*
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
|
17
TESTING.rst
|
@ -11,6 +11,7 @@ Detailed information on testing can be found here: https://wiki.openstack.org/wi
|
|||
*Install pip*::
|
||||
|
||||
[apt-get | yum] install python-pip
|
||||
|
||||
More information on pip here: http://www.pip-installer.org/en/latest/
|
||||
|
||||
*Use pip to install tox*::
|
||||
|
@ -27,6 +28,22 @@ As of zuul v3, a running zookeeper is required to execute tests.
|
|||
|
||||
service zookeeper start
|
||||
|
||||
.. note:: Installing and bulding javascript is not required, but tests that
|
||||
depend on the javascript assets having been built will be skipped
|
||||
if you don't.
|
||||
|
||||
*Install javascript tools*::
|
||||
|
||||
tools/install-js-tools.sh
|
||||
|
||||
*Install javascript dependencies*::
|
||||
|
||||
yarn install
|
||||
|
||||
*Build javascript assets*::
|
||||
|
||||
npm run build:dev
|
||||
|
||||
Run The Tests
|
||||
-------------
|
||||
|
||||
|
|
|
@ -679,6 +679,11 @@ sections of ``zuul.conf`` are used by the web server:
|
|||
Type of server hosting the statistics information. Currently only
|
||||
'graphite' is supported by the dashboard.
|
||||
|
||||
.. attr:: static_path
|
||||
:default: zuul/web/static
|
||||
|
||||
Path containing the static web assets.
|
||||
|
||||
.. attr:: static_cache_expiry
|
||||
:default: 3600
|
||||
|
||||
|
|
|
@ -16,3 +16,4 @@ Zuul, though advanced users may find it interesting.
|
|||
testing
|
||||
docs
|
||||
ansible
|
||||
javascript
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
Zuul Web Javascript
|
||||
===================
|
||||
|
||||
zuul-web has an html, css and javascript component that is managed
|
||||
using Javascript toolchains. It is intended to be served by zuul-web
|
||||
directly from zuul/web/static in the simple case, or to be published to
|
||||
an alternate static web location, such as an Apache server.
|
||||
|
||||
The web applications are managed by `yarn`_ and `webpack`_ which in turn both
|
||||
assume a functioning and recent `nodejs`_ installation.
|
||||
|
||||
For the impatient who don't want deal with javascript toolchains
|
||||
----------------------------------------------------------------
|
||||
|
||||
tl;dr - You have to build stuff with javascript tools.
|
||||
|
||||
The best thing would be to get familiar with the tools, there are a lot of
|
||||
good features available. But, if you don't want to know anything about the
|
||||
Javascript toolchains a few helpers have been provided.
|
||||
|
||||
If you have npm and docker installed and don't want to install newer nodejs
|
||||
or a bunch of javascript libraries, you can run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run build:docker
|
||||
|
||||
If you have docker but do not have npm or nodejs installed, you can build
|
||||
the web app with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -it --rm -v $(PWD):/usr/src/app -w /usr/src/app node:alpine \
|
||||
npm run build:dist-with-depends
|
||||
|
||||
Both do the same thing. Both versions will result in the built files being
|
||||
put into ``zuul/web/static``.
|
||||
|
||||
.. note:: Because the user inside of the Docker container is root, the files
|
||||
that it emits into zuul/web/static will be owned by root.
|
||||
|
||||
yarn dependency management
|
||||
--------------------------
|
||||
|
||||
`yarn`_ manages the javascript dependencies. That means the first step is
|
||||
getting `yarn`_ installed.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tools/install-js-tools.sh
|
||||
|
||||
The ``tools/install-js-tools.sh`` script will add apt or yum repositories and
|
||||
install `nodejs`_ and `yarn`_ from them. For RPM-based distros it needs to know
|
||||
which repo description file to download, so it calls out to
|
||||
``tools/install-js-repos-rpm.sh``.
|
||||
|
||||
Once yarn is installed, getting dependencies installed is:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yarn install
|
||||
|
||||
The ``yarn.lock`` file contains all of the specific versions that were
|
||||
installed before. Since this is an application it has been added to the repo.
|
||||
|
||||
To add new dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yarn add awesome-package
|
||||
|
||||
To remove dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yarn remove terrible-package
|
||||
|
||||
Adding or removing packages will add the logical dependency to ``package.json``
|
||||
and will record the version of the package and any of its dependencies that
|
||||
were installed into ``yarn.lock`` so that other users can simply run
|
||||
``yarn install`` and get the same environment.
|
||||
|
||||
To update a dependency:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yarn add awesome-package
|
||||
|
||||
Dependencies are installed into the ``node_modules`` directory. Deleting that
|
||||
directory and re-running ``yarn install`` should always be safe.
|
||||
|
||||
webpack asset management
|
||||
------------------------
|
||||
|
||||
`webpack`_ takes care of bundling web assets for deployment, including tasks
|
||||
such as minifying and transpiling for older browsers. It takes a
|
||||
javascript-first approach, and generates a html file that includes the
|
||||
appropriate javascript and CSS to get going.
|
||||
|
||||
We need to modify the html generated for each of our pages, so there are
|
||||
templates in ``web/templates``.
|
||||
|
||||
The main `webpack`_ config file is ``webpack.config.js``. In the Zuul tree that
|
||||
file is a stub file that includes either a dev or a prod environment from
|
||||
``web/config/webpack.dev.js`` or ``web/config/webpack.prod.js``. Most of the
|
||||
important bits are in ``web/config/webpack.common.js``.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
Building the code can be done with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run build
|
||||
|
||||
zuul-web has a ``static`` route defined which serves files from
|
||||
``zuul/web/static``. ``npm run build`` will put the build output files
|
||||
into the ``zuul/web/static`` directory so that zuul-web can serve them.
|
||||
|
||||
There is a also a development-oriented version of that same command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run build:dev
|
||||
|
||||
which will build for the ``dev`` environment. This causes some sample data
|
||||
to be bundled and included.
|
||||
|
||||
Webpack includes a development server that handles things like reloading and
|
||||
hot-updating of code. The following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start
|
||||
|
||||
will build the code and launch the dev server on `localhost:8080`. It will
|
||||
additionally watch for changes to the files and re-compile/refresh as needed.
|
||||
Arbitrary command line options will be passed through after a ``--`` such as:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start -- --open-file='static/status.html'
|
||||
|
||||
That's kind of annoying though, so additional targets exist for common tasks:
|
||||
|
||||
Run status against `basic` built-in demo data.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:status:basic
|
||||
|
||||
Run status against `openstack` built-in demo data
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:status:openstack
|
||||
|
||||
Run status against `tree` built-in demo data.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:status:tree
|
||||
|
||||
Run status against live data from OpenStack's Zuul.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:status
|
||||
|
||||
Run builds against live data from OpenStack's Zuul.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:builds
|
||||
|
||||
Run jobs against live data from OpenStack's Zuul.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:jobs
|
||||
|
||||
Run console streamer.
|
||||
|
||||
.. note:: There is not currently a good way to pass build_id paramter.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:stream
|
||||
|
||||
Additional run commands can be added in `package.json` in the ``scripts``
|
||||
section.
|
||||
|
||||
Deploying
|
||||
---------
|
||||
|
||||
The web application is a set of static files and is designed to be served
|
||||
by zuul-web from its ``static`` route. In order to make sure this works
|
||||
properly, the javascript build needs to be performed so that the javascript
|
||||
files are in the ``zuul/web/static`` directory. Because the javascript
|
||||
build outputs into the ``zuul/web/static`` directory, as long as
|
||||
``npm run build`` has been done before ``pip install .`` or
|
||||
``python setup.py sdist``, all the files will be where they need to be.
|
||||
|
||||
Debugging minified code
|
||||
-----------------------
|
||||
|
||||
Both the ``dev`` and ``prod`` ennvironments use the same `devtool`_
|
||||
called ``source-map`` which makes debugging errors easier by including mapping
|
||||
information from the minified and bundled resources to their approriate
|
||||
non-minified source code locations. Javascript errors in the browser as seen
|
||||
in the developer console can be clicked on and the appropriate actual source
|
||||
code location will be shown.
|
||||
|
||||
``source-map`` is considered an appropriate `devtool`_ for production, but has
|
||||
the downside that it is slower to update. However, since it includes the most
|
||||
complete mapping information and doesn't impact execution performance, so in
|
||||
our case we use it for both.
|
||||
|
||||
.. _yarn: https://yarnpkg.com/en/
|
||||
.. _nodejs: https://nodejs.org/
|
||||
.. _webpack: https://webpack.js.org/
|
||||
.. _devtool: https://webpack.js.org/configuration/devtool/#devtool
|
|
@ -1,27 +0,0 @@
|
|||
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
|
||||
``<div id="id="zuul-container"></div>``.
|
||||
|
||||
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 code at the bottom of
|
||||
``index.html``
|
||||
|
||||
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.
|
|
@ -1,23 +0,0 @@
|
|||
#!/bin/bash
|
||||
BASE_DIR=$(cd $(dirname $0); pwd)
|
||||
DEST_DIR=$BASE_DIR/public_html/lib
|
||||
mkdir -p $DEST_DIR
|
||||
echo "Destination: $DEST_DIR"
|
||||
|
||||
echo "Fetching jquery.min.js..."
|
||||
curl -L --silent http://code.jquery.com/jquery.min.js > $DEST_DIR/jquery.min.js
|
||||
|
||||
echo "Fetching jquery-visibility.min.js..."
|
||||
curl -L --silent https://raw.githubusercontent.com/mathiasbynens/jquery-visibility/master/jquery-visibility.js > $DEST_DIR/jquery-visibility.js
|
||||
|
||||
echo "Fetching jquery.graphite.js..."
|
||||
curl -L --silent https://github.com/prestontimmons/graphitejs/archive/master.zip > jquery-graphite.zip
|
||||
unzip -q -o jquery-graphite.zip -d $DEST_DIR/
|
||||
mv $DEST_DIR/graphitejs-master/jquery.graphite.js $DEST_DIR/
|
||||
rm -R jquery-graphite.zip $DEST_DIR/graphitejs-master
|
||||
|
||||
echo "Fetching bootstrap..."
|
||||
curl -L --silent https://github.com/twbs/bootstrap/releases/download/v3.1.1/bootstrap-3.1.1-dist.zip > bootstrap.zip
|
||||
unzip -q -o bootstrap.zip -d $DEST_DIR/
|
||||
mv $DEST_DIR/bootstrap-3.1.1-dist $DEST_DIR/bootstrap
|
||||
rm bootstrap.zip
|
|
@ -1,39 +0,0 @@
|
|||
<!--
|
||||
Copyright 2013 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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<title>Zuul Status</title>
|
||||
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="styles/zuul.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="zuul_container"></div>
|
||||
<script src="lib/jquery.min.js"></script>
|
||||
<script src="lib/jquery-visibility.js"></script>
|
||||
<script src="lib/jquery.graphite.js"></script>
|
||||
<script src="jquery.zuul.js"></script>
|
||||
<script src="zuul.app.js"></script>
|
||||
<script>
|
||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache 2.0
|
||||
zuul_build_dom(jQuery, '#zuul_container');
|
||||
zuul_start(jQuery);
|
||||
// @license-end
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,945 +0,0 @@
|
|||
// jquery plugin for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2012 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
function set_cookie(name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/';
|
||||
}
|
||||
|
||||
function read_cookie(name, default_value) {
|
||||
var nameEQ = name + '=';
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0;i < ca.length;i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
$.zuul = function(options) {
|
||||
options = $.extend({
|
||||
'enabled': true,
|
||||
'graphite_url': '',
|
||||
'source': 'status',
|
||||
'msg_id': '#zuul_msg',
|
||||
'pipelines_id': '#zuul_pipelines',
|
||||
'queue_events_num': '#zuul_queue_events_num',
|
||||
'queue_management_events_num': '#zuul_queue_management_events_num',
|
||||
'queue_results_num': '#zuul_queue_results_num',
|
||||
}, options);
|
||||
|
||||
var collapsed_exceptions = [];
|
||||
var current_filter = read_cookie('zuul_filter_string', '');
|
||||
var change_set_in_url = window.location.href.split('#')[1];
|
||||
if (change_set_in_url) {
|
||||
current_filter = change_set_in_url;
|
||||
}
|
||||
var $jq;
|
||||
|
||||
var xhr,
|
||||
zuul_graph_update_count = 0,
|
||||
zuul_sparkline_urls = {};
|
||||
|
||||
function get_sparkline_url(pipeline_name) {
|
||||
if (options.graphite_url !== '') {
|
||||
if (!(pipeline_name in zuul_sparkline_urls)) {
|
||||
zuul_sparkline_urls[pipeline_name] = $.fn.graphite
|
||||
.geturl({
|
||||
url: options.graphite_url,
|
||||
from: "-8hours",
|
||||
width: 100,
|
||||
height: 26,
|
||||
margin: 0,
|
||||
hideLegend: true,
|
||||
hideAxes: true,
|
||||
hideGrid: true,
|
||||
target: [
|
||||
"color(stats.gauges.zuul.pipeline." + pipeline_name
|
||||
+ ".current_changes, '6b8182')"
|
||||
]
|
||||
});
|
||||
}
|
||||
return zuul_sparkline_urls[pipeline_name];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var format = {
|
||||
job: function(job) {
|
||||
var $job_line = $('<span />');
|
||||
|
||||
if (job.result !== null) {
|
||||
$job_line.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.report_url)
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
else if (job.url !== null) {
|
||||
$job_line.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.url)
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$job_line.append(
|
||||
$('<span />')
|
||||
.addClass('zuul-job-name')
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
|
||||
$job_line.append(this.job_status(job));
|
||||
|
||||
if (job.voting === false) {
|
||||
$job_line.append(
|
||||
$(' <small />')
|
||||
.addClass('zuul-non-voting-desc')
|
||||
.text(' (non-voting)')
|
||||
);
|
||||
}
|
||||
|
||||
$job_line.append($('<div style="clear: both"></div>'));
|
||||
return $job_line;
|
||||
},
|
||||
|
||||
job_status: function(job) {
|
||||
var result = job.result ? job.result.toLowerCase() : null;
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued';
|
||||
}
|
||||
|
||||
if (result === 'in progress') {
|
||||
return this.job_progress_bar(job.elapsed_time,
|
||||
job.remaining_time);
|
||||
}
|
||||
else {
|
||||
return this.status_label(result);
|
||||
}
|
||||
},
|
||||
|
||||
status_label: function(result) {
|
||||
var $status = $('<span />');
|
||||
$status.addClass('zuul-job-result label');
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$status.addClass('label-success');
|
||||
break;
|
||||
case 'failure':
|
||||
$status.addClass('label-danger');
|
||||
break;
|
||||
case 'unstable':
|
||||
$status.addClass('label-warning');
|
||||
break;
|
||||
case 'skipped':
|
||||
$status.addClass('label-info');
|
||||
break;
|
||||
// 'in progress' 'queued' 'lost' 'aborted' ...
|
||||
default:
|
||||
$status.addClass('label-default');
|
||||
}
|
||||
$status.text(result);
|
||||
return $status;
|
||||
},
|
||||
|
||||
job_progress_bar: function(elapsed_time, remaining_time) {
|
||||
var progress_percent = 100 * (elapsed_time / (elapsed_time +
|
||||
remaining_time));
|
||||
var $bar_inner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
.attr('role', 'progressbar')
|
||||
.attr('aria-valuenow', 'progressbar')
|
||||
.attr('aria-valuemin', progress_percent)
|
||||
.attr('aria-valuemin', '0')
|
||||
.attr('aria-valuemax', '100')
|
||||
.css('width', progress_percent + '%');
|
||||
|
||||
var $bar_outter = $('<div />')
|
||||
.addClass('progress zuul-job-result')
|
||||
.append($bar_inner);
|
||||
|
||||
return $bar_outter;
|
||||
},
|
||||
|
||||
enqueue_time: function(ms) {
|
||||
// Special format case for enqueue time to add style
|
||||
var hours = 60 * 60 * 1000;
|
||||
var now = Date.now();
|
||||
var delta = now - ms;
|
||||
var status = 'text-success';
|
||||
var text = this.time(delta, true);
|
||||
if (delta > (4 * hours)) {
|
||||
status = 'text-danger';
|
||||
} else if (delta > (2 * hours)) {
|
||||
status = 'text-warning';
|
||||
}
|
||||
return '<span class="' + status + '">' + text + '</span>';
|
||||
},
|
||||
|
||||
time: function(ms, words) {
|
||||
if (typeof(words) === 'undefined') {
|
||||
words = false;
|
||||
}
|
||||
var seconds = (+ms)/1000;
|
||||
var minutes = Math.floor(seconds/60);
|
||||
var hours = Math.floor(minutes/60);
|
||||
seconds = Math.floor(seconds % 60);
|
||||
minutes = Math.floor(minutes % 60);
|
||||
var r = '';
|
||||
if (words) {
|
||||
if (hours) {
|
||||
r += hours;
|
||||
r += ' hr ';
|
||||
}
|
||||
r += minutes + ' min';
|
||||
} else {
|
||||
if (hours < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += hours + ':';
|
||||
if (minutes < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += minutes + ':';
|
||||
if (seconds < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += seconds;
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
change_total_progress_bar: function(change) {
|
||||
var job_percent = Math.floor(100 / change.jobs.length);
|
||||
var $bar_outter = $('<div />')
|
||||
.addClass('progress zuul-change-total-result');
|
||||
|
||||
$.each(change.jobs, function (i, job) {
|
||||
var result = job.result ? job.result.toLowerCase() : null;
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued';
|
||||
}
|
||||
|
||||
if (result !== 'queued') {
|
||||
var $bar_inner = $('<div />')
|
||||
.addClass('progress-bar');
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$bar_inner.addClass('progress-bar-success');
|
||||
break;
|
||||
case 'lost':
|
||||
case 'failure':
|
||||
$bar_inner.addClass('progress-bar-danger');
|
||||
break;
|
||||
case 'unstable':
|
||||
$bar_inner.addClass('progress-bar-warning');
|
||||
break;
|
||||
case 'in progress':
|
||||
case 'queued':
|
||||
break;
|
||||
}
|
||||
$bar_inner.attr('title', job.name)
|
||||
.css('width', job_percent + '%');
|
||||
$bar_outter.append($bar_inner);
|
||||
}
|
||||
});
|
||||
return $bar_outter;
|
||||
},
|
||||
|
||||
change_header: function(change) {
|
||||
var change_id = change.id || 'NA';
|
||||
|
||||
var $change_link = $('<small />');
|
||||
if (change.url !== null) {
|
||||
var github_id = change_id.match(/^([0-9]+),([0-9a-f]{40})$/);
|
||||
if (github_id) {
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', change_id)
|
||||
.text('#' + github_id[1])
|
||||
)
|
||||
);
|
||||
} else if (/^[0-9a-f]{40}$/.test(change_id)) {
|
||||
var change_id_short = change_id.slice(0, 7);
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', change_id)
|
||||
.text(change_id_short)
|
||||
)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).text(change_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (change_id.length === 40) {
|
||||
change_id = change_id.substr(0, 7);
|
||||
}
|
||||
$change_link.text(change_id);
|
||||
}
|
||||
|
||||
var $change_progress_row_left = $('<div />')
|
||||
.addClass('col-xs-4')
|
||||
.append($change_link);
|
||||
var $change_progress_row_right = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append(this.change_total_progress_bar(change));
|
||||
|
||||
var $change_progress_row = $('<div />')
|
||||
.addClass('row')
|
||||
.append($change_progress_row_left)
|
||||
.append($change_progress_row_right);
|
||||
|
||||
var $project_span = $('<span />')
|
||||
.addClass('change_project')
|
||||
.text(change.project);
|
||||
|
||||
var $left = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append($project_span, $change_progress_row);
|
||||
|
||||
var remaining_time = this.time(
|
||||
change.remaining_time, true);
|
||||
var enqueue_time = this.enqueue_time(
|
||||
change.enqueue_time);
|
||||
var $remaining_time = $('<small />').addClass('time')
|
||||
.attr('title', 'Remaining Time').html(remaining_time);
|
||||
var $enqueue_time = $('<small />').addClass('time')
|
||||
.attr('title', 'Elapsed Time').html(enqueue_time);
|
||||
|
||||
var $right = $('<div />');
|
||||
if (change.live === true) {
|
||||
$right.addClass('col-xs-4 text-right')
|
||||
.append($remaining_time, $('<br />'), $enqueue_time);
|
||||
}
|
||||
|
||||
var $header = $('<div />')
|
||||
.addClass('row')
|
||||
.append($left, $right);
|
||||
return $header;
|
||||
},
|
||||
|
||||
change_list: function(jobs) {
|
||||
var format = this;
|
||||
var $list = $('<ul />')
|
||||
.addClass('list-group zuul-patchset-body');
|
||||
|
||||
$.each(jobs, function (i, job) {
|
||||
var $item = $('<li />')
|
||||
.addClass('list-group-item')
|
||||
.addClass('zuul-change-job')
|
||||
.append(format.job(job));
|
||||
$list.append($item);
|
||||
});
|
||||
|
||||
return $list;
|
||||
},
|
||||
|
||||
change_panel: function (change) {
|
||||
var $header = $('<div />')
|
||||
.addClass('panel-heading zuul-patchset-header')
|
||||
.append(this.change_header(change));
|
||||
|
||||
var panel_id = change.id ? change.id.replace(',', '_')
|
||||
: change.project.replace('/', '_') +
|
||||
'-' + change.enqueue_time;
|
||||
var $panel = $('<div />')
|
||||
.attr('id', panel_id)
|
||||
.addClass('panel panel-default zuul-change')
|
||||
.append($header)
|
||||
.append(this.change_list(change.jobs));
|
||||
|
||||
$header.click(this.toggle_patchset);
|
||||
return $panel;
|
||||
},
|
||||
|
||||
change_status_icon: function(change) {
|
||||
var icon_name = 'green.png';
|
||||
var icon_title = 'Succeeding';
|
||||
|
||||
if (change.active !== true) {
|
||||
// Grey icon
|
||||
icon_name = 'grey.png';
|
||||
icon_title = 'Waiting until closer to head of queue to' +
|
||||
' start jobs';
|
||||
}
|
||||
else if (change.live !== true) {
|
||||
// Grey icon
|
||||
icon_name = 'grey.png';
|
||||
icon_title = 'Dependent change required for testing';
|
||||
}
|
||||
else if (change.failing_reasons &&
|
||||
change.failing_reasons.length > 0) {
|
||||
var reason = change.failing_reasons.join(', ');
|
||||
icon_title = 'Failing because ' + reason;
|
||||
if (reason.match(/merge conflict/)) {
|
||||
// Black icon
|
||||
icon_name = 'black.png';
|
||||
}
|
||||
else {
|
||||
// Red icon
|
||||
icon_name = 'red.png';
|
||||
}
|
||||
}
|
||||
|
||||
var $icon = $('<img />')
|
||||
.attr('src', 'images/' + icon_name)
|
||||
.attr('title', icon_title)
|
||||
.css('margin-top', '-6px');
|
||||
|
||||
return $icon;
|
||||
},
|
||||
|
||||
change_with_status_tree: function(change, change_queue) {
|
||||
var $change_row = $('<tr />');
|
||||
|
||||
for (var i = 0; i < change_queue._tree_columns; i++) {
|
||||
var $tree_cell = $('<td />')
|
||||
.css('height', '100%')
|
||||
.css('padding', '0 0 10px 0')
|
||||
.css('margin', '0')
|
||||
.css('width', '16px')
|
||||
.css('min-width', '16px')
|
||||
.css('overflow', 'hidden')
|
||||
.css('vertical-align', 'top');
|
||||
|
||||
if (i < change._tree.length && change._tree[i] !== null) {
|
||||
$tree_cell.css('background-image',
|
||||
'url(\'images/line.png\')')
|
||||
.css('background-repeat', 'repeat-y');
|
||||
}
|
||||
|
||||
if (i === change._tree_index) {
|
||||
$tree_cell.append(
|
||||
this.change_status_icon(change));
|
||||
}
|
||||
if (change._tree_branches.indexOf(i) !== -1) {
|
||||
var $image = $('<img />')
|
||||
.css('vertical-align', 'baseline');
|
||||
if (change._tree_branches.indexOf(i) ===
|
||||
change._tree_branches.length - 1) {
|
||||
// Angle line
|
||||
$image.attr('src', 'images/line-angle.png');
|
||||
}
|
||||
else {
|
||||
// T line
|
||||
$image.attr('src', 'images/line-t.png');
|
||||
}
|
||||
$tree_cell.append($image);
|
||||
}
|
||||
$change_row.append($tree_cell);
|
||||
}
|
||||
|
||||
var change_width = 360 - 16*change_queue._tree_columns;
|
||||
var $change_column = $('<td />')
|
||||
.css('width', change_width + 'px')
|
||||
.addClass('zuul-change-cell')
|
||||
.append(this.change_panel(change));
|
||||
|
||||
$change_row.append($change_column);
|
||||
|
||||
var $change_table = $('<table />')
|
||||
.addClass('zuul-change-box')
|
||||
.css('-moz-box-sizing', 'content-box')
|
||||
.css('box-sizing', 'content-box')
|
||||
.append($change_row);
|
||||
|
||||
return $change_table;
|
||||
},
|
||||
|
||||
pipeline_sparkline: function(pipeline_name) {
|
||||
if (options.graphite_url !== '') {
|
||||
var $sparkline = $('<img />')
|
||||
.addClass('pull-right')
|
||||
.attr('src', get_sparkline_url(pipeline_name));
|
||||
return $sparkline;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
pipeline_header: function(pipeline, count) {
|
||||
// Format the pipeline name, sparkline and description
|
||||
var $header_div = $('<div />')
|
||||
.addClass('zuul-pipeline-header');
|
||||
|
||||
var $heading = $('<h3 />')
|
||||
.css('vertical-align', 'middle')
|
||||
.text(pipeline.name)
|
||||
.append(
|
||||
$('<span />')
|
||||
.addClass('badge pull-right')
|
||||
.css('vertical-align', 'middle')
|
||||
.css('margin-top', '0.5em')
|
||||
.text(count)
|
||||
)
|
||||
.append(this.pipeline_sparkline(pipeline.name));
|
||||
|
||||
$header_div.append($heading);
|
||||
|
||||
if (typeof pipeline.description === 'string') {
|
||||
var descr = $('<small />')
|
||||
$.each( pipeline.description.split(/\r?\n\r?\n/), function(index, descr_part){
|
||||
descr.append($('<p />').text(descr_part));
|
||||
});
|
||||
$header_div.append(
|
||||
$('<p />').append(descr)
|
||||
);
|
||||
}
|
||||
return $header_div;
|
||||
},
|
||||
|
||||
pipeline: function (pipeline, count) {
|
||||
var format = this;
|
||||
var $html = $('<div />')
|
||||
.addClass('zuul-pipeline col-md-4')
|
||||
.append(this.pipeline_header(pipeline, count));
|
||||
|
||||
$.each(pipeline.change_queues,
|
||||
function (queue_i, change_queue) {
|
||||
$.each(change_queue.heads, function (head_i, changes) {
|
||||
if (pipeline.change_queues.length > 1 &&
|
||||
head_i === 0) {
|
||||
var name = change_queue.name;
|
||||
var short_name = name;
|
||||
if (short_name.length > 32) {
|
||||
short_name = short_name.substr(0, 32) + '...';
|
||||
}
|
||||
$html.append(
|
||||
$('<p />')
|
||||
.text('Queue: ')
|
||||
.append(
|
||||
$('<abbr />')
|
||||
.attr('title', name)
|
||||
.text(short_name)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$.each(changes, function (change_i, change) {
|
||||
var $change_box =
|
||||
format.change_with_status_tree(
|
||||
change, change_queue);
|
||||
$html.append($change_box);
|
||||
format.display_patchset($change_box);
|
||||
});
|
||||
});
|
||||
});
|
||||
return $html;
|
||||
},
|
||||
|
||||
toggle_patchset: function(e) {
|
||||
// Toggle showing/hiding the patchset when the header is
|
||||
// clicked.
|
||||
|
||||
if (e.target.nodeName.toLowerCase() === 'a') {
|
||||
// Ignore clicks from gerrit patch set link
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab the patchset panel
|
||||
var $panel = $(e.target).parents('.zuul-change');
|
||||
var $body = $panel.children('.zuul-patchset-body');
|
||||
$body.toggle(200);
|
||||
var collapsed_index = collapsed_exceptions.indexOf(
|
||||
$panel.attr('id'));
|
||||
if (collapsed_index === -1 ) {
|
||||
// Currently not an exception, add it to list
|
||||
collapsed_exceptions.push($panel.attr('id'));
|
||||
}
|
||||
else {
|
||||
// Currently an except, remove from exceptions
|
||||
collapsed_exceptions.splice(collapsed_index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
display_patchset: function($change_box, animate) {
|
||||
// Determine if to show or hide the patchset and/or the results
|
||||
// when loaded
|
||||
|
||||
// See if we should hide the body/results
|
||||
var $panel = $change_box.find('.zuul-change');
|
||||
var panel_change = $panel.attr('id');
|
||||
var $body = $panel.children('.zuul-patchset-body');
|
||||
var expand_by_default = $('#expand_by_default')
|
||||
.prop('checked');
|
||||
|
||||
var collapsed_index = collapsed_exceptions
|
||||
.indexOf(panel_change);
|
||||
|
||||
if (expand_by_default && collapsed_index === -1 ||
|
||||
!expand_by_default && collapsed_index !== -1) {
|
||||
// Expand by default, or is an exception
|
||||
$body.show(animate);
|
||||
}
|
||||
else {
|
||||
$body.hide(animate);
|
||||
}
|
||||
|
||||
// Check if we should hide the whole panel
|
||||
var panel_project = $panel.find('.change_project').text()
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
var panel_pipeline = $change_box
|
||||
.parents('.zuul-pipeline')
|
||||
.find('.zuul-pipeline-header > h3')
|
||||
.html()
|
||||
.toLowerCase();
|
||||
|
||||
if (current_filter !== '') {
|
||||
var show_panel = false;
|
||||
var filter = current_filter.trim().split(/[\s,]+/);
|
||||
$.each(filter, function(index, f_val) {
|
||||
if (f_val !== '') {
|
||||
f_val = f_val.toLowerCase();
|
||||
if (panel_project.indexOf(f_val) !== -1 ||
|
||||
panel_pipeline.indexOf(f_val) !== -1 ||
|
||||
panel_change.indexOf(f_val) !== -1) {
|
||||
show_panel = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (show_panel === true) {
|
||||
$change_box.show(animate);
|
||||
}
|
||||
else {
|
||||
$change_box.hide(animate);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$change_box.show(animate);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var app = {
|
||||
schedule: function (app) {
|
||||
app = app || this;
|
||||
if (!options.enabled) {
|
||||
setTimeout(function() {app.schedule(app);}, 5000);
|
||||
return;
|
||||
}
|
||||
app.update().always(function () {
|
||||
setTimeout(function() {app.schedule(app);}, 5000);
|
||||
});
|
||||
|
||||
/* Only update graphs every minute */
|
||||
if (zuul_graph_update_count > 11) {
|
||||
zuul_graph_update_count = 0;
|
||||
zuul.update_sparklines();
|
||||
}
|
||||
},
|
||||
|
||||
/** @return {jQuery.Promise} */
|
||||
update: function () {
|
||||
// Cancel the previous update if it hasn't completed yet.
|
||||
if (xhr) {
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
this.emit('update-start');
|
||||
var app = this;
|
||||
|
||||
var $msg = $(options.msg_id);
|
||||
xhr = $.getJSON(options.source)
|
||||
.done(function (data) {
|
||||
if ('message' in data) {
|
||||
$msg.removeClass('alert-danger')
|
||||
.addClass('alert-info')
|
||||
.text(data.message)
|
||||
.show();
|
||||
} else {
|
||||
$msg.empty()
|
||||
.hide();
|
||||
}
|
||||
|
||||
if ('zuul_version' in data) {
|
||||
$('#zuul-version-span').text(data.zuul_version);
|
||||
}
|
||||
if ('last_reconfigured' in data) {
|
||||
var last_reconfigured =
|
||||
new Date(data.last_reconfigured);
|
||||
$('#last-reconfigured-span').text(
|
||||
last_reconfigured.toString());
|
||||
}
|
||||
|
||||
var $pipelines = $(options.pipelines_id);
|
||||
$pipelines.html('');
|
||||
$.each(data.pipelines, function (i, pipeline) {
|
||||
var count = app.create_tree(pipeline);
|
||||
$pipelines.append(
|
||||
format.pipeline(pipeline, count));
|
||||
});
|
||||
|
||||
$(options.queue_events_num).text(
|
||||
data.trigger_event_queue ?
|
||||
data.trigger_event_queue.length : '0'
|
||||
);
|
||||
$(options.queue_management_events_num).text(
|
||||
data.management_event_queue ?
|
||||
data.management_event_queue.length : '0'
|
||||
);
|
||||
$(options.queue_results_num).text(
|
||||
data.result_event_queue ?
|
||||
data.result_event_queue.length : '0'
|
||||
);
|
||||
})
|
||||
.fail(function (jqXHR, statusText, errMsg) {
|
||||
if (statusText === 'abort') {
|
||||
return;
|
||||
}
|
||||
$msg.text(options.source + ': ' + errMsg)
|
||||
.addClass('alert-danger')
|
||||
.removeClass('zuul-msg-wrap-off')
|
||||
.show();
|
||||
})
|
||||
.always(function () {
|
||||
xhr = undefined;
|
||||
app.emit('update-end');
|
||||
});
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
update_sparklines: function() {
|
||||
$.each(zuul_sparkline_urls, function(name, url) {
|
||||
var newimg = new Image();
|
||||
var parts = url.split('#');
|
||||
newimg.src = parts[0] + '#' + new Date().getTime();
|
||||
$(newimg).load(function () {
|
||||
zuul_sparkline_urls[name] = newimg.src;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
control_form: function() {
|
||||
// Build the filter form filling anything from cookies
|
||||
|
||||
var $control_form = $('<form />')
|
||||
.attr('role', 'form')
|
||||
.addClass('form-inline')
|
||||
.submit(this.handle_filter_change);
|
||||
|
||||
$control_form
|
||||
.append(this.filter_form_group())
|
||||
.append(this.expand_form_group());
|
||||
|
||||
return $control_form;
|
||||
},
|
||||
|
||||
filter_form_group: function() {
|
||||
// Update the filter form with a clear button if required
|
||||
|
||||
var $label = $('<label />')
|
||||
.addClass('control-label')
|
||||
.attr('for', 'filter_string')
|
||||
.text('Filters')
|
||||
.css('padding-right', '0.5em');
|
||||
|
||||
var $input = $('<input />')
|
||||
.attr('type', 'text')
|
||||
.attr('id', 'filter_string')
|
||||
.addClass('form-control')
|
||||
.attr('title',
|
||||
'project(s), pipeline(s) or review(s) comma ' +
|
||||
'separated')
|
||||
.attr('value', current_filter);
|
||||
|
||||
$input.change(this.handle_filter_change);
|
||||
|
||||
var $clear_icon = $('<span />')
|
||||
.addClass('form-control-feedback')
|
||||
.addClass('glyphicon glyphicon-remove-circle')
|
||||
.attr('id', 'filter_form_clear_box')
|
||||
.attr('title', 'clear filter')
|
||||
.css('cursor', 'pointer');
|
||||
|
||||
$clear_icon.click(function() {
|
||||
$('#filter_string').val('').change();
|
||||
});
|
||||
|
||||
if (current_filter === '') {
|
||||
$clear_icon.hide();
|
||||
}
|
||||
|
||||
var $form_group = $('<div />')
|
||||
.addClass('form-group has-feedback')
|
||||
.append($label, $input, $clear_icon);
|
||||
return $form_group;
|
||||
},
|
||||
|
||||
expand_form_group: function() {
|
||||
var expand_by_default = (
|
||||
read_cookie('zuul_expand_by_default', false) === 'true');
|
||||
|
||||
var $checkbox = $('<input />')
|
||||
.attr('type', 'checkbox')
|
||||
.attr('id', 'expand_by_default')
|
||||
.prop('checked', expand_by_default)
|
||||
.change(this.handle_expand_by_default);
|
||||
|
||||
var $label = $('<label />')
|
||||
.css('padding-left', '1em')
|
||||
.html('Expand by default: ')
|
||||
.append($checkbox);
|
||||
|
||||
var $form_group = $('<div />')
|
||||
.addClass('checkbox')
|
||||
.append($label);
|
||||
return $form_group;
|
||||
},
|
||||
|
||||
handle_filter_change: function() {
|
||||
// Update the filter and save it to a cookie
|
||||
current_filter = $('#filter_string').val();
|
||||
set_cookie('zuul_filter_string', current_filter);
|
||||
if (current_filter === '') {
|
||||
$('#filter_form_clear_box').hide();
|
||||
}
|
||||
else {
|
||||
$('#filter_form_clear_box').show();
|
||||
}
|
||||
|
||||
$('.zuul-change-box').each(function(index, obj) {
|
||||
var $change_box = $(obj);
|
||||
format.display_patchset($change_box, 200);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
handle_expand_by_default: function(e) {
|
||||
// Handle toggling expand by default
|
||||
set_cookie('zuul_expand_by_default', e.target.checked);
|
||||
collapsed_exceptions = [];
|
||||
$('.zuul-change-box').each(function(index, obj) {
|
||||
var $change_box = $(obj);
|
||||
format.display_patchset($change_box, 200);
|
||||
});
|
||||
},
|
||||
|
||||
create_tree: function(pipeline) {
|
||||
var count = 0;
|
||||
var pipeline_max_tree_columns = 1;
|
||||
$.each(pipeline.change_queues, function(change_queue_i,
|
||||
change_queue) {
|
||||
var tree = [];
|
||||
var max_tree_columns = 1;
|
||||
var changes = [];
|
||||
var last_tree_length = 0;
|
||||
$.each(change_queue.heads, function(head_i, head) {
|
||||
$.each(head, function(change_i, change) {
|
||||
changes[change.id] = change;
|
||||
change._tree_position = change_i;
|
||||
});
|
||||
});
|
||||
$.each(change_queue.heads, function(head_i, head) {
|
||||
$.each(head, function(change_i, change) {
|
||||
if (change.live === true) {
|
||||
count += 1;
|
||||
}
|
||||
var idx = tree.indexOf(change.id);
|
||||
if (idx > -1) {
|
||||
change._tree_index = idx;
|
||||
// remove...
|
||||
tree[idx] = null;
|
||||
while (tree[tree.length - 1] === null) {
|
||||
tree.pop();
|
||||
}
|
||||
} else {
|
||||
change._tree_index = 0;
|
||||
}
|
||||
change._tree_branches = [];
|
||||
change._tree = [];
|
||||
if (typeof(change.items_behind) === 'undefined') {
|
||||
change.items_behind = [];
|
||||
}
|
||||
change.items_behind.sort(function(a, b) {
|
||||
return (changes[b]._tree_position -
|
||||
changes[a]._tree_position);
|
||||
});
|
||||
$.each(change.items_behind, function(i, id) {
|
||||
tree.push(id);
|
||||
if (tree.length>last_tree_length &&
|
||||
last_tree_length > 0) {
|
||||
change._tree_branches.push(
|
||||
tree.length - 1);
|
||||
}
|
||||
});
|
||||
if (tree.length > max_tree_columns) {
|
||||
max_tree_columns = tree.length;
|
||||
}
|
||||
if (tree.length > pipeline_max_tree_columns) {
|
||||
pipeline_max_tree_columns = tree.length;
|
||||
}
|
||||
change._tree = tree.slice(0); // make a copy
|
||||
last_tree_length = tree.length;
|
||||
});
|
||||
});
|
||||
change_queue._tree_columns = max_tree_columns;
|
||||
});
|
||||
pipeline._tree_columns = pipeline_max_tree_columns;
|
||||
return count;
|
||||
},
|
||||
};
|
||||
|
||||
$jq = $(app);
|
||||
return {
|
||||
options: options,
|
||||
format: format,
|
||||
app: app,
|
||||
jq: $jq
|
||||
};
|
||||
};
|
||||
}(jQuery));
|
|
@ -1,58 +0,0 @@
|
|||
.zuul-change {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.zuul-change-id {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.zuul-job-result {
|
||||
float: right;
|
||||
width: 70px;
|
||||
height: 15px;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.zuul-change-total-result {
|
||||
height: 10px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.zuul-spinner,
|
||||
.zuul-spinner:hover {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-out;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zuul-spinner-on,
|
||||
.zuul-spinner-on:hover {
|
||||
opacity: 1;
|
||||
transition-duration: 0.2s;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.zuul-change-cell {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.zuul-change-job {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.zuul-job-name {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.zuul-non-voting-desc {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.zuul-patchset-header {
|
||||
font-size: small;
|
||||
padding: 8px 12px;
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// Client script for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2013 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
/*exported zuul_build_dom, zuul_start */
|
||||
|
||||
function zuul_build_dom($, container) {
|
||||
// Build a default-looking DOM
|
||||
var default_layout = '<div class="container">'
|
||||
+ '<div class="zuul-container" id="zuul-container">'
|
||||
+ '<div style="display: none;" class="alert" id="zuul_msg"></div>'
|
||||
+ '<button class="btn pull-right zuul-spinner">updating <span class="glyphicon glyphicon-refresh"></span></button>'
|
||||
+ '<p>Queue lengths: <span id="zuul_queue_events_num">0</span> events, <span id="zuul_queue_management_events_num">0</span> management events, <span id="zuul_queue_results_num">0</span> results.</p>'
|
||||
+ '<div id="zuul_controls"></div>'
|
||||
+ '<div id="zuul_pipelines" class="row"></div>'
|
||||
+ '<p>Zuul version: <span id="zuul-version-span"></span></p>'
|
||||
+ '<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>'
|
||||
+ '</div></div>';
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
var $container = $(container);
|
||||
$container.html(default_layout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The $.zuul instance
|
||||
*/
|
||||
function zuul_start($) {
|
||||
// Start the zuul app (expects default dom)
|
||||
|
||||
var $container, $indicator;
|
||||
var demo = location.search.match(/[?&]demo=([^?&]*)/),
|
||||
source_url = location.search.match(/[?&]source_url=([^?&]*)/),
|
||||
source = demo ? './status-' + (demo[1] || 'basic') + '.json-sample' :
|
||||
'status';
|
||||
source = source_url ? source_url[1] : source;
|
||||
|
||||
var zuul = $.zuul({
|
||||
source: source,
|
||||
//graphite_url: 'http://graphite.openstack.org/render/'
|
||||
});
|
||||
|
||||
zuul.jq.on('update-start', function () {
|
||||
$container.addClass('zuul-container-loading');
|
||||
$indicator.addClass('zuul-spinner-on');
|
||||
});
|
||||
|
||||
zuul.jq.on('update-end', function () {
|
||||
$container.removeClass('zuul-container-loading');
|
||||
setTimeout(function () {
|
||||
$indicator.removeClass('zuul-spinner-on');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
zuul.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 () {
|
||||
// Fade in the content
|
||||
$container.addClass('zuul-container-ready');
|
||||
});
|
||||
});
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
$container = $('#zuul-container');
|
||||
$indicator = $('#zuul-spinner');
|
||||
$('#zuul_controls').append(zuul.app.control_form());
|
||||
|
||||
zuul.app.schedule();
|
||||
|
||||
$(document).on({
|
||||
'show.visibility': function () {
|
||||
zuul.options.enabled = true;
|
||||
zuul.app.update();
|
||||
},
|
||||
'hide.visibility': function () {
|
||||
zuul.options.enabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return zuul;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "zuul-web",
|
||||
"version": "1.0.0",
|
||||
"description": "Web Client for Zuul",
|
||||
"main": "index.js",
|
||||
"repository": "https://git.openstack.org/openstack-infra/zuul",
|
||||
"author": "OpenStack Infra",
|
||||
"license": "Apache-2.0",
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"angular": "^1.5.8",
|
||||
"bootstrap": "^3.3.7",
|
||||
"graphitejs": "https://github.com/prestontimmons/graphitejs/archive/master.tar.gz",
|
||||
"jquery": "^1.11.1",
|
||||
"jquery-visibility": "https://github.com/mathiasbynens/jquery-visibility/archive/master.tar.gz"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:dist",
|
||||
"build:dev": "webpack --env=dev",
|
||||
"build:dist": "webpack --env=prod",
|
||||
"build:docker": "docker run -it --rm -v $(pwd):/usr/src/app -w /usr/src/app node:alpine npm run build:dist-with-depends",
|
||||
"build:dist-with-depends": "yarn install && npm run build:dist",
|
||||
"format": "eslint --fix web/*.js",
|
||||
"lint": "webpack --env=lint",
|
||||
"start": "webpack-dev-server --env=dev --define ZUUL_API_URL=\"'https://zuul.openstack.org'\" --open-page='status.html'",
|
||||
"start:basic": "webpack-dev-server --env=dev --open-page='status.html?demo=basic'",
|
||||
"start:openstack": "webpack-dev-server --env=dev --open-page='status.html?demo=openstack'",
|
||||
"start:tree": "webpack-dev-server --env=dev --open-page='status.html?demo=tree'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-angularjs-annotate": "^0.8.2",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"clean-webpack-plugin": "^0.1.16",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": ">=3.19.0",
|
||||
"eslint-config-standard": "^11.0.0-beta.0",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^6.0.0",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"file-loader": "^0.11.2",
|
||||
"html-webpack-plugin": "^2.29.0",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^3.3.0",
|
||||
"webpack-archive-plugin": "^3.0.0",
|
||||
"webpack-bundle-analyzer": "^2.9.1",
|
||||
"webpack-dev-server": "^2.6.1",
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- fetch-javascript-output
|
|
@ -0,0 +1,5 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- install-nodejs
|
||||
- install-yarn
|
||||
- install-javascript-packages
|
|
@ -0,0 +1,5 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- revoke-sudo
|
||||
- npm
|
||||
- tox
|
|
@ -18,6 +18,10 @@ classifier =
|
|||
[pbr]
|
||||
warnerrors = True
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
zuul._setup_hook.setup_hook
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
zuul-scheduler = zuul.cmd.scheduler:main
|
||||
|
|
|
@ -13,3 +13,4 @@ mock
|
|||
PyMySQL
|
||||
mypy
|
||||
zuul-sphinx
|
||||
beautifulsoup4
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import asyncio
|
||||
import configparser
|
||||
from contextlib import contextmanager
|
||||
import datetime
|
||||
|
@ -39,6 +40,8 @@ import traceback
|
|||
import time
|
||||
import uuid
|
||||
import urllib
|
||||
import socketserver
|
||||
import http.server
|
||||
|
||||
import git
|
||||
import gear
|
||||
|
@ -1755,6 +1758,74 @@ class ChrootedKazooFixture(fixtures.Fixture):
|
|||
_tmp_client.close()
|
||||
|
||||
|
||||
class WebProxyFixture(fixtures.Fixture):
|
||||
def __init__(self, rules):
|
||||
super(WebProxyFixture, self).__init__()
|
||||
self.rules = rules
|
||||
|
||||
def _setUp(self):
|
||||
rules = self.rules
|
||||
|
||||
class Proxy(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = self.path
|
||||
for (pattern, replace) in rules:
|
||||
path = re.sub(pattern, replace, path)
|
||||
try:
|
||||
remote = urllib.request.urlopen(path)
|
||||
except urllib.error.HTTPError as e:
|
||||
self.send_response(e.code)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(int(remote.getcode()))
|
||||
for header in remote.info():
|
||||
self.send_header(header, remote.info()[header])
|
||||
self.end_headers()
|
||||
self.wfile.write(remote.read())
|
||||
|
||||
self.httpd = socketserver.ThreadingTCPServer(('', 0), Proxy)
|
||||
self.port = self.httpd.socket.getsockname()[1]
|
||||
self.thread = threading.Thread(target=self.httpd.serve_forever)
|
||||
self.thread.start()
|
||||
self.addCleanup(self._cleanup)
|
||||
|
||||
def _cleanup(self):
|
||||
self.httpd.shutdown()
|
||||
self.thread.join()
|
||||
|
||||
|
||||
class ZuulWebFixture(fixtures.Fixture):
|
||||
def __init__(self, gearman_server_port):
|
||||
super(ZuulWebFixture, self).__init__()
|
||||
self.gearman_server_port = gearman_server_port
|
||||
|
||||
def _setUp(self):
|
||||
# Start the web server
|
||||
self.web = zuul.web.ZuulWeb(
|
||||
listen_address='127.0.0.1', listen_port=0,
|
||||
gear_server='127.0.0.1', gear_port=self.gearman_server_port)
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.set_debug(True)
|
||||
ws_thread = threading.Thread(target=self.web.run, args=(loop,))
|
||||
ws_thread.start()
|
||||
self.addCleanup(loop.close)
|
||||
self.addCleanup(ws_thread.join)
|
||||
self.addCleanup(self.web.stop)
|
||||
|
||||
self.host = 'localhost'
|
||||
# Wait until web server is started
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
if self.web.server is None:
|
||||
continue
|
||||
self.port = self.web.server.sockets[0].getsockname()[1]
|
||||
try:
|
||||
with socket.create_connection((self.host, self.port)):
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
pass
|
||||
|
||||
|
||||
class MySQLSchemaFixture(fixtures.Fixture):
|
||||
def setUp(self):
|
||||
super(MySQLSchemaFixture, self).setUp()
|
||||
|
|
|
@ -249,7 +249,8 @@ class TestStreaming(tests.base.AnsibleZuulTestCase):
|
|||
# Start the web server
|
||||
web_server = zuul.web.ZuulWeb(
|
||||
listen_address='::', listen_port=9000,
|
||||
gear_server='127.0.0.1', gear_port=self.gearman_server.port)
|
||||
gear_server='127.0.0.1', gear_port=self.gearman_server.port,
|
||||
static_path=tempfile.gettempdir())
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.set_debug(True)
|
||||
ws_thread = threading.Thread(target=web_server.run, args=(loop,))
|
||||
|
|
|
@ -45,6 +45,9 @@ class BaseTestWeb(ZuulTestCase):
|
|||
config_ini_data = {}
|
||||
|
||||
def setUp(self):
|
||||
self.assertTrue(
|
||||
os.path.exists(zuul.web.STATIC_DIR),
|
||||
"Static web assets are missing, be sure to run 'npm run build'")
|
||||
super(BaseTestWeb, self).setUp()
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2017 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import urllib
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from tests.base import ZuulTestCase, WebProxyFixture
|
||||
from tests.base import ZuulWebFixture
|
||||
|
||||
|
||||
class TestWebURLs(object):
|
||||
tenant_config_file = 'config/single-tenant/main.yaml'
|
||||
|
||||
def setUp(self):
|
||||
super(TestWebURLs, self).setUp()
|
||||
self.web = self.useFixture(
|
||||
ZuulWebFixture(self.gearman_server.port))
|
||||
|
||||
def _get(self, port, uri):
|
||||
url = "http://localhost:{}{}".format(port, uri)
|
||||
self.log.debug("GET {}".format(url))
|
||||
req = urllib.request.Request(url)
|
||||
try:
|
||||
f = urllib.request.urlopen(req)
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Error on URL {}".format(url))
|
||||
return f.read()
|
||||
|
||||
def _crawl(self, url):
|
||||
page = self._get(self.port, url)
|
||||
page = BeautifulSoup(page, 'html.parser')
|
||||
for (tag, attr) in [
|
||||
('script', 'src'),
|
||||
('link', 'href'),
|
||||
('a', 'href'),
|
||||
('img', 'src'),
|
||||
]:
|
||||
for item in page.find_all(tag):
|
||||
suburl = item.get(attr)
|
||||
# Skip empty urls. Also skip the navbar relative link for now.
|
||||
# TODO(mordred) Remove when we have the top navbar link sorted.
|
||||
if suburl is None or suburl == "../":
|
||||
continue
|
||||
link = urllib.parse.urljoin(url, suburl)
|
||||
self._get(self.port, link)
|
||||
|
||||
|
||||
class TestDirect(TestWebURLs, ZuulTestCase):
|
||||
# Test directly accessing the zuul-web server with no proxy
|
||||
def setUp(self):
|
||||
super(TestDirect, self).setUp()
|
||||
self.port = self.web.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/tenant-one/status.html')
|
||||
|
||||
|
||||
class TestWhiteLabel(TestWebURLs, ZuulTestCase):
|
||||
# Test a zuul-web behind a whitelabel proxy (i.e., what
|
||||
# zuul.openstack.org does).
|
||||
def setUp(self):
|
||||
super(TestWhiteLabel, self).setUp()
|
||||
rules = [
|
||||
('^/(.*)$', 'http://localhost:{}/tenant-one/\\1'.format(
|
||||
self.web.port)),
|
||||
]
|
||||
self.proxy = self.useFixture(WebProxyFixture(rules))
|
||||
self.port = self.proxy.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/status.html')
|
||||
|
||||
|
||||
class TestSuburl(TestWebURLs, ZuulTestCase):
|
||||
# Test a zuul-web mounted on a suburl (i.e., what software factory
|
||||
# does).
|
||||
def setUp(self):
|
||||
super(TestSuburl, self).setUp()
|
||||
rules = [
|
||||
('^/zuul3/(.*)$', 'http://localhost:{}/\\1'.format(
|
||||
self.web.port)),
|
||||
]
|
||||
self.proxy = self.useFixture(WebProxyFixture(rules))
|
||||
self.port = self.proxy.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/zuul3/tenant-one/status.html')
|
|
@ -0,0 +1,256 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2016 NodeSource LLC
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the "Software"),
|
||||
# to deal in the Software without restriction, including without limitation
|
||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
# and/or sell copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# The above license is inferred from the
|
||||
# https://github.com/nodesource/distributions source repository.
|
||||
|
||||
# Discussion, issues and change requests at:
|
||||
# https://github.com/nodesource/distributions
|
||||
#
|
||||
# Script to install the NodeSource Node.js 8.x repo onto an
|
||||
# Enterprise Linux or Fedora Core based system.
|
||||
#
|
||||
# Run as root or insert `sudo -E` before `bash`:
|
||||
#
|
||||
# This was downloaded from https://rpm.nodesource.com/setup_8.x
|
||||
# A few modifications have been made.
|
||||
|
||||
print_status() {
|
||||
local outp=$(echo "$1" | sed -r 's/\\n/\\n## /mg')
|
||||
echo
|
||||
echo -e "## ${outp}"
|
||||
echo
|
||||
}
|
||||
|
||||
bail() {
|
||||
echo 'Error executing command, exiting'
|
||||
exit 1
|
||||
}
|
||||
|
||||
exec_cmd_nobail() {
|
||||
echo "+ $1"
|
||||
bash -c "$1"
|
||||
}
|
||||
|
||||
exec_cmd() {
|
||||
exec_cmd_nobail "$1" || bail
|
||||
}
|
||||
|
||||
print_status "Installing the NodeSource Node.js 8.x repo..."
|
||||
|
||||
print_status "Inspecting system..."
|
||||
|
||||
if [ ! -x /bin/rpm ]; then
|
||||
print_status "\
|
||||
You don't appear to be running an Enterprise Linux based \
|
||||
system, please contact NodeSource at \
|
||||
https://github.com/nodesource/distributions/issues if you think this \
|
||||
is incorrect or would like your distribution to be considered for \
|
||||
support.\
|
||||
"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Annotated section for auto extraction in test.sh
|
||||
#-check-distro-#
|
||||
|
||||
## Check distro and arch
|
||||
echo "+ rpm -q --whatprovides redhat-release || rpm -q --whatprovides centos-release || rpm -q --whatprovides cloudlinux-release || rpm -q --whatprovides sl-release"
|
||||
DISTRO_PKG=$(rpm -q --whatprovides redhat-release || rpm -q --whatprovides centos-release || rpm -q --whatprovides cloudlinux-release || rpm -q --whatprovides sl-release)
|
||||
echo "+ uname -m"
|
||||
UNAME_ARCH=$(uname -m)
|
||||
|
||||
|
||||
if [ "X${UNAME_ARCH}" == "Xi686" ]; then
|
||||
DIST_ARCH=i386
|
||||
elif [ "X${UNAME_ARCH}" == "Xx86_64" ]; then
|
||||
DIST_ARCH=x86_64
|
||||
else
|
||||
|
||||
print_status "\
|
||||
You don't appear to be running a supported machine architecture: ${UNAME_ARCH}. \
|
||||
Please contact NodeSource at \
|
||||
https://github.com/nodesource/distributions/issues if you think this is \
|
||||
incorrect or would like your architecture to be considered for support. \
|
||||
"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
if [[ $DISTRO_PKG =~ ^(redhat|centos|cloudlinux|sl)- ]]; then
|
||||
DIST_TYPE=el
|
||||
elif [[ $DISTRO_PKG =~ ^system-release- ]]; then # Amazon Linux
|
||||
DIST_TYPE=el
|
||||
elif [[ $DISTRO_PKG =~ ^(fedora|korora)- ]]; then
|
||||
DIST_TYPE=fc
|
||||
else
|
||||
|
||||
print_status "\
|
||||
You don't appear to be running a supported version of Enterprise Linux. \
|
||||
Please contact NodeSource at \
|
||||
https://github.com/nodesource/distributions/issues if you think this is \
|
||||
incorrect or would like your architecture to be considered for support. \
|
||||
Include your 'distribution package' name: ${DISTRO_PKG}. \
|
||||
"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
if [[ $DISTRO_PKG =~ ^system-release-201[4-9]\. ]]; then #NOTE: not really future-proof
|
||||
|
||||
# Amazon Linux, for 2014.* use el7, older versions are unknown, perhaps el6
|
||||
DIST_VERSION=7
|
||||
|
||||
else
|
||||
|
||||
## Using the redhat-release-server-X, centos-release-X, etc. pattern
|
||||
## extract the major version number of the distro
|
||||
DIST_VERSION=$(echo $DISTRO_PKG | sed -r 's/^[[:alpha:]]+-release(-server|-workstation)?-([0-9]+).*$/\2/')
|
||||
|
||||
if ! [[ $DIST_VERSION =~ ^[0-9][0-9]?$ ]]; then
|
||||
|
||||
print_status "\
|
||||
Could not determine your distribution version, you may not be running a \
|
||||
supported version of Enterprise Linux. \
|
||||
Please contact NodeSource at \
|
||||
https://github.com/nodesource/distributions/issues if you think this is \
|
||||
incorrect. Include your 'distribution package' name: ${DISTRO_PKG}. \
|
||||
"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
|
||||
## Given the distro, version and arch, construct the url for
|
||||
## the appropriate nodesource-release package (it's noarch but
|
||||
## we include the arch in the directory tree anyway)
|
||||
RELEASE_URL_VERSION_STRING="${DIST_TYPE}${DIST_VERSION}"
|
||||
RELEASE_URL="\
|
||||
https://rpm.nodesource.com/pub_8.x/\
|
||||
${DIST_TYPE}/\
|
||||
${DIST_VERSION}/\
|
||||
${DIST_ARCH}/\
|
||||
nodesource-release-${RELEASE_URL_VERSION_STRING}-1.noarch.rpm"
|
||||
|
||||
#-check-distro-#
|
||||
|
||||
print_status "Confirming \"${DIST_TYPE}${DIST_VERSION}-${DIST_ARCH}\" is supported..."
|
||||
|
||||
## Simple fetch & fast-fail to see if the nodesource-release
|
||||
## file exists for this distro/version/arch
|
||||
exec_cmd_nobail "curl -sLf -o /dev/null '${RELEASE_URL}'"
|
||||
RC=$?
|
||||
|
||||
if [[ $RC != 0 ]]; then
|
||||
print_status "\
|
||||
Your distribution, identified as \"${DISTRO_PKG}\", \
|
||||
is not currently supported, please contact NodeSource at \
|
||||
https://github.com/nodesource/distributions/issues \
|
||||
if you think this is incorrect or would like your distribution to be considered for support"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## EPEL is needed for EL5, we don't install it if it's missing but
|
||||
## we can give guidance
|
||||
if [ "$DIST_TYPE" == "el" ] && [ "$DIST_VERSION" == "5" ]; then
|
||||
|
||||
print_status "Checking if EPEL is enabled..."
|
||||
|
||||
echo "+ yum repolist enabled 2> /dev/null | grep epel"
|
||||
repolist=$(yum repolist enabled 2> /dev/null | grep epel)
|
||||
|
||||
if [ "X${repolist}" == "X" ]; then
|
||||
print_status "Finding current EPEL release RPM..."
|
||||
|
||||
## We can scrape the html to find the latest epel-release (likely 5.4)
|
||||
epel_url="http://dl.fedoraproject.org/pub/epel/5/${DIST_ARCH}/"
|
||||
epel_release_view="${epel_url}repoview/epel-release.html"
|
||||
echo "+ curl -s $epel_release_view | grep -oE 'epel-release-[0-9\-]+\.noarch\.rpm'"
|
||||
epel=$(curl -s $epel_release_view | grep -oE 'epel-release-[0-9\-]+\.noarch\.rpm')
|
||||
if [ "X${epel}" = "X" ]; then
|
||||
print_status "Error: Could not find current EPEL release RPM!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "\
|
||||
The EPEL (Extra Packages for Enterprise Linux) repository is a\n\
|
||||
prerequisite for installing Node.js on your operating system. Please\n\
|
||||
add it and re-run this setup script.\n\
|
||||
\n\
|
||||
The EPEL repository RPM is available at:\n\
|
||||
${epel_url}${epel}\n\
|
||||
You can try installing with: \`rpm -ivh <url>\`\
|
||||
"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
print_status "Downloading release setup RPM..."
|
||||
|
||||
## Two-step process to install the nodesource-release RPM,
|
||||
## Download to a tmp file then install it directly with `rpm`.
|
||||
## We don't rely on RPM's ability to fetch from HTTPS directly
|
||||
echo "+ mktemp"
|
||||
RPM_TMP=$(mktemp || bail)
|
||||
|
||||
exec_cmd "curl -sL -o '${RPM_TMP}' '${RELEASE_URL}'"
|
||||
|
||||
print_status "Installing release setup RPM..."
|
||||
|
||||
## --nosignature because nodesource-release contains the signature!
|
||||
exec_cmd "rpm -i --nosignature --force '${RPM_TMP}'"
|
||||
|
||||
print_status "Cleaning up..."
|
||||
|
||||
exec_cmd "rm -f '${RPM_TMP}'"
|
||||
|
||||
print_status "Checking for existing installations..."
|
||||
|
||||
## Nasty consequences if you have an existing Node or npm package
|
||||
## installed, need to inform if they are there
|
||||
echo "+ rpm -qa 'node|npm' | grep -v nodesource"
|
||||
EXISTING_NODE=$(rpm -qa 'node|npm|iojs' | grep -v nodesource)
|
||||
|
||||
if [ "X${EXISTING_NODE}" != "X" ]; then
|
||||
|
||||
# NOTE(mordred) Removed -y from the yum command below.
|
||||
print_status "\
|
||||
Your system appears to already have Node.js installed from an alternative source.\n\
|
||||
Run \`\033[1myum remove nodejs npm\033[22m\` (as root) to remove these first.\
|
||||
"
|
||||
|
||||
fi
|
||||
|
||||
# NOTE(mordred) Removed -y from the yum commands below.
|
||||
print_status "\
|
||||
Run \`\033[1myum install nodejs\033[22m\` (as root) to install Node.js 8.x and npm.\n\
|
||||
You may also need development tools to build native addons:\n\
|
||||
\`yum install gcc-c++ make\`\
|
||||
"
|
||||
|
||||
## Alternative to install dev tools: `yum groupinstall 'Development Tools'
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/bash
|
||||
# Copyright 2017 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
if type apt-get; then
|
||||
# Install https transport - otherwise apt-get HANGS on https urls
|
||||
sudo apt-get update
|
||||
sudo apt-get install apt-transport-https
|
||||
# Install recent NodeJS repo
|
||||
curl -sS https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo apt-key add -
|
||||
echo "deb https://deb.nodesource.com/node_8.x xenial main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
# Install yarn repo
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get -q --option "Dpkg::Options::=--force-confold" --assume-yes \
|
||||
install nodejs yarn
|
||||
else
|
||||
sudo curl https://dl.yarnpkg.com/rpm/yarn.repo -o /etc/yum.repos.d/yarn.repo
|
||||
sudo $(dirname $0)/install-js-repos-rpm.sh
|
||||
sudo yum -y install nodejs yarn
|
||||
fi
|
|
@ -0,0 +1,137 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
main: './web/main.js',
|
||||
// Tell webpack to extract 3rd party depdenencies which change less
|
||||
// frequently.
|
||||
vendor: [
|
||||
'angular',
|
||||
'bootstrap/dist/css/bootstrap.css',
|
||||
'jquery-visibility/jquery-visibility',
|
||||
'graphitejs/jquery.graphite.js'
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
// path.resolve(__dirname winds up relative to the config dir
|
||||
path: path.resolve(__dirname, '../../zuul/web/static'),
|
||||
publicPath: ''
|
||||
},
|
||||
// Some folks prefer "cheaper" source-map for dev and one that is more
|
||||
// expensive to build for prod. Debugging without the full source-map sucks,
|
||||
// so define it here in common.
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
$: 'jquery/dist/jquery',
|
||||
jQuery: 'jquery/dist/jquery',
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
}),
|
||||
new CleanWebpackPlugin(
|
||||
['zuul/web/static'], { root: path.resolve(__dirname, '../..')}),
|
||||
// Each of the entries below lists a specific 'chunk' which is one of the
|
||||
// entry items from above. We can collapse this to just do one single
|
||||
// output file.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'status.html',
|
||||
template: 'web/templates/status.ejs',
|
||||
title: 'Zuul Status'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Builds',
|
||||
template: 'web/templates/builds.ejs',
|
||||
filename: 'builds.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Jobs',
|
||||
template: 'web/templates/jobs.ejs',
|
||||
filename: 'jobs.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Tenants',
|
||||
template: 'web/templates/tenants.ejs',
|
||||
filename: 'tenants.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Console Stream',
|
||||
template: 'web/templates/stream.ejs',
|
||||
filename: 'stream.html'
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
'babel-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
// The majority of the rules below are all about getting bootstrap copied
|
||||
// appropriately.
|
||||
{
|
||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: ['raw-loader'],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
|
||||
use: ['file-loader']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
|
||||
// Enable Hot Module Replacement for devServer
|
||||
devServer: {
|
||||
hot: true,
|
||||
contentBase: path.resolve(__dirname, './zuul/web/static'),
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
'eslint-loader'
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// We only need to bundle the demo files when we're running locally
|
||||
new webpack.ProvidePlugin({
|
||||
DemoStatusBasic: './status-basic.json',
|
||||
DemoStatusOpenStack: './status-openstack.json',
|
||||
DemoStatusTree: './status-tree.json'
|
||||
}),
|
||||
]
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
'eslint-loader'
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: '../../../reports/bundle.html',
|
||||
generateStatsFile: true,
|
||||
openAnalyzer: false,
|
||||
statsFilename: '../../../reports/stats.json',
|
||||
}),
|
||||
]
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
const ArchivePlugin = require('webpack-archive-plugin');
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
output: {
|
||||
filename: '[name].[chunkhash].js',
|
||||
// path.resolve(__dirname winds up relative to the config dir
|
||||
path: path.resolve(__dirname, '../../zuul/web/static'),
|
||||
publicPath: ''
|
||||
},
|
||||
plugins: [
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true,
|
||||
debug: false
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
}),
|
||||
// Keeps the vendor bundle from changing needlessly.
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
warningsFilter: function(filename) {
|
||||
return ! /node_modules/.test(filename);
|
||||
},
|
||||
beautify: false,
|
||||
mangle: {
|
||||
screw_ie8: true,
|
||||
keep_fnames: true
|
||||
},
|
||||
compress: {
|
||||
screw_ie8: true
|
||||
},
|
||||
sourceMap: true,
|
||||
comments: false
|
||||
}),
|
||||
new ArchivePlugin({
|
||||
output: path.resolve(__dirname, '../../zuul-web'),
|
||||
format: [
|
||||
'tar',
|
||||
],
|
||||
ext: 'tgz'
|
||||
})
|
||||
]
|
||||
})
|
|
@ -0,0 +1,115 @@
|
|||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import angular from 'angular'
|
||||
|
||||
import './styles/zuul.css'
|
||||
import './jquery.zuul'
|
||||
import { getSourceUrl } from './util'
|
||||
|
||||
angular.module('zuulTenants', []).controller(
|
||||
'mainController', function ($scope, $http, $location) {
|
||||
$scope.tenants = undefined
|
||||
$scope.tenants_fetch = function () {
|
||||
$http.get(getSourceUrl('tenants', $location))
|
||||
.then(function success (result) {
|
||||
$scope.tenants = result.data
|
||||
})
|
||||
}
|
||||
$scope.tenants_fetch()
|
||||
})
|
||||
|
||||
angular.module('zuulJobs', [], function ($locationProvider) {
|
||||
$locationProvider.html5Mode({
|
||||
enabled: true,
|
||||
requireBase: false
|
||||
})
|
||||
}).controller(
|
||||
'mainController', function ($scope, $http, $location) {
|
||||
$scope.jobs = undefined
|
||||
$scope.jobs_fetch = function () {
|
||||
$http.get(getSourceUrl('jobs', $location))
|
||||
.then(function success (result) {
|
||||
$scope.jobs = result.data
|
||||
})
|
||||
}
|
||||
$scope.jobs_fetch()
|
||||
})
|
||||
|
||||
angular.module('zuulBuilds', [], function ($locationProvider) {
|
||||
$locationProvider.html5Mode({
|
||||
enabled: true,
|
||||
requireBase: false
|
||||
})
|
||||
}).controller('mainController', function ($scope, $http, $location) {
|
||||
$scope.rowClass = function (build) {
|
||||
if (build.result === 'SUCCESS') {
|
||||
return 'success'
|
||||
} else {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
let queryArgs = $location.search()
|
||||
let url = $location.url()
|
||||
if (queryArgs['source_url']) {
|
||||
$scope.tenant = undefined
|
||||
} else {
|
||||
let tenantStart = url.lastIndexOf(
|
||||
'/', url.lastIndexOf('/builds.html') - 1) + 1
|
||||
let tenantLength = url.lastIndexOf('/builds.html') - tenantStart
|
||||
$scope.tenant = url.substr(tenantStart, tenantLength)
|
||||
}
|
||||
$scope.builds = undefined
|
||||
if (queryArgs['pipeline']) {
|
||||
$scope.pipeline = queryArgs['pipeline']
|
||||
} else { $scope.pipeline = '' }
|
||||
if (queryArgs['job_name']) {
|
||||
$scope.job_name = queryArgs['job_name']
|
||||
} else { $scope.job_name = '' }
|
||||
if (queryArgs['project']) {
|
||||
$scope.project = queryArgs['project']
|
||||
} else { $scope.project = '' }
|
||||
$scope.builds_fetch = function () {
|
||||
let queryString = ''
|
||||
if ($scope.tenant) { queryString += '&tenant=' + $scope.tenant }
|
||||
if ($scope.pipeline) { queryString += '&pipeline=' + $scope.pipeline }
|
||||
if ($scope.job_name) { queryString += '&job_name=' + $scope.job_name }
|
||||
if ($scope.project) { queryString += '&project=' + $scope.project }
|
||||
if (queryString !== '') { queryString = '?' + queryString.substr(1) }
|
||||
$http.get(getSourceUrl('builds', $location) + queryString)
|
||||
.then(function success (result) {
|
||||
for (let buildPos = 0;
|
||||
buildPos < result.data.length;
|
||||
buildPos += 1) {
|
||||
let build = result.data[buildPos]
|
||||
if (build.node_name == null) {
|
||||
build.node_name = 'master'
|
||||
}
|
||||
/* Fix incorect url for post_failure job */
|
||||
if (build.log_url === build.job_name) {
|
||||
build.log_url = undefined
|
||||
}
|
||||
}
|
||||
$scope.builds = result.data
|
||||
})
|
||||
}
|
||||
$scope.builds_fetch()
|
||||
})
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 204 B After Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
@ -0,0 +1,929 @@
|
|||
/* global Image, jQuery */
|
||||
// jquery plugin for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2012 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
import RedImage from './images/red.png'
|
||||
import GreyImage from './images/grey.png'
|
||||
import GreenImage from './images/green.png'
|
||||
import BlackImage from './images/black.png'
|
||||
import LineImage from './images/line.png'
|
||||
import LineAngleImage from './images/line-angle.png'
|
||||
import LineTImage from './images/line-t.png';
|
||||
|
||||
(function ($) {
|
||||
function setCookie (name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/'
|
||||
}
|
||||
|
||||
function readCookie (name, defaultValue) {
|
||||
let nameEQ = name + '='
|
||||
let ca = document.cookie.split(';')
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i]
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1, c.length)
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
$.zuul = function (options) {
|
||||
options = $.extend({
|
||||
'enabled': true,
|
||||
'graphite_url': '',
|
||||
'source': 'status',
|
||||
'source_data': null,
|
||||
'msg_id': '#zuul_msg',
|
||||
'pipelines_id': '#zuul_pipelines',
|
||||
'queue_events_num': '#zuul_queue_events_num',
|
||||
'queue_management_events_num': '#zuul_queue_management_events_num',
|
||||
'queue_results_num': '#zuul_queue_results_num'
|
||||
}, options)
|
||||
|
||||
let collapsedExceptions = []
|
||||
let currentFilter = readCookie('zuul_filter_string', '')
|
||||
let changeSetInURL = window.location.href.split('#')[1]
|
||||
if (changeSetInURL) {
|
||||
currentFilter = changeSetInURL
|
||||
}
|
||||
let $jq
|
||||
|
||||
let xhr
|
||||
let zuulGraphUpdateCount = 0
|
||||
let zuulSparklineURLs = {}
|
||||
|
||||
function getSparklineURL (pipelineName) {
|
||||
if (options.graphite_url !== '') {
|
||||
if (!(pipelineName in zuulSparklineURLs)) {
|
||||
zuulSparklineURLs[pipelineName] = $.fn.graphite
|
||||
.geturl({
|
||||
url: options.graphite_url,
|
||||
from: '-8hours',
|
||||
width: 100,
|
||||
height: 26,
|
||||
margin: 0,
|
||||
hideLegend: true,
|
||||
hideAxes: true,
|
||||
hideGrid: true,
|
||||
target: [
|
||||
'color(stats.gauges.zuul.pipeline.' + pipelineName +
|
||||
".current_changes, '6b8182')"
|
||||
]
|
||||
})
|
||||
}
|
||||
return zuulSparklineURLs[pipelineName]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let format = {
|
||||
job: function (job) {
|
||||
let $jobLine = $('<span />')
|
||||
|
||||
if (job.result !== null) {
|
||||
$jobLine.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.report_url)
|
||||
.text(job.name)
|
||||
)
|
||||
} else if (job.url !== null) {
|
||||
$jobLine.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.url)
|
||||
.text(job.name)
|
||||
)
|
||||
} else {
|
||||
$jobLine.append(
|
||||
$('<span />')
|
||||
.addClass('zuul-job-name')
|
||||
.text(job.name)
|
||||
)
|
||||
}
|
||||
|
||||
$jobLine.append(this.job_status(job))
|
||||
|
||||
if (job.voting === false) {
|
||||
$jobLine.append(
|
||||
$(' <small />')
|
||||
.addClass('zuul-non-voting-desc')
|
||||
.text(' (non-voting)')
|
||||
)
|
||||
}
|
||||
|
||||
$jobLine.append($('<div style="clear: both"></div>'))
|
||||
return $jobLine
|
||||
},
|
||||
|
||||
job_status: function (job) {
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued'
|
||||
}
|
||||
|
||||
if (result === 'in progress') {
|
||||
return this.job_progress_bar(job.elapsed_time,
|
||||
job.remaining_time)
|
||||
} else {
|
||||
return this.status_label(result)
|
||||
}
|
||||
},
|
||||
|
||||
status_label: function (result) {
|
||||
let $status = $('<span />')
|
||||
$status.addClass('zuul-job-result label')
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$status.addClass('label-success')
|
||||
break
|
||||
case 'failure':
|
||||
$status.addClass('label-danger')
|
||||
break
|
||||
case 'unstable':
|
||||
$status.addClass('label-warning')
|
||||
break
|
||||
case 'skipped':
|
||||
$status.addClass('label-info')
|
||||
break
|
||||
// 'in progress' 'queued' 'lost' 'aborted' ...
|
||||
default:
|
||||
$status.addClass('label-default')
|
||||
}
|
||||
$status.text(result)
|
||||
return $status
|
||||
},
|
||||
|
||||
job_progress_bar: function (elapsedTime, remainingTime) {
|
||||
let progressPercent = 100 * (elapsedTime / (elapsedTime +
|
||||
remainingTime))
|
||||
let $barInner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
.attr('role', 'progressbar')
|
||||
.attr('aria-valuenow', 'progressbar')
|
||||
.attr('aria-valuemin', progressPercent)
|
||||
.attr('aria-valuemin', '0')
|
||||
.attr('aria-valuemax', '100')
|
||||
.css('width', progressPercent + '%')
|
||||
|
||||
let $barOutter = $('<div />')
|
||||
.addClass('progress zuul-job-result')
|
||||
.append($barInner)
|
||||
|
||||
return $barOutter
|
||||
},
|
||||
|
||||
enqueueTime: function (ms) {
|
||||
// Special format case for enqueue time to add style
|
||||
let hours = 60 * 60 * 1000
|
||||
let now = Date.now()
|
||||
let delta = now - ms
|
||||
let status = 'text-success'
|
||||
let text = this.time(delta, true)
|
||||
if (delta > (4 * hours)) {
|
||||
status = 'text-danger'
|
||||
} else if (delta > (2 * hours)) {
|
||||
status = 'text-warning'
|
||||
}
|
||||
return '<span class="' + status + '">' + text + '</span>'
|
||||
},
|
||||
|
||||
time: function (ms, words) {
|
||||
if (typeof (words) === 'undefined') {
|
||||
words = false
|
||||
}
|
||||
let seconds = (+ms) / 1000
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = Math.floor(minutes / 60)
|
||||
seconds = Math.floor(seconds % 60)
|
||||
minutes = Math.floor(minutes % 60)
|
||||
let r = ''
|
||||
if (words) {
|
||||
if (hours) {
|
||||
r += hours
|
||||
r += ' hr '
|
||||
}
|
||||
r += minutes + ' min'
|
||||
} else {
|
||||
if (hours < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += hours + ':'
|
||||
if (minutes < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += minutes + ':'
|
||||
if (seconds < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += seconds
|
||||
}
|
||||
return r
|
||||
},
|
||||
|
||||
changeTotalProgressBar: function (change) {
|
||||
let jobPercent = Math.floor(100 / change.jobs.length)
|
||||
let $barOutter = $('<div />')
|
||||
.addClass('progress zuul-change-total-result')
|
||||
|
||||
$.each(change.jobs, function (i, job) {
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued'
|
||||
}
|
||||
|
||||
if (result !== 'queued') {
|
||||
let $barInner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$barInner.addClass('progress-bar-success')
|
||||
break
|
||||
case 'lost':
|
||||
case 'failure':
|
||||
$barInner.addClass('progress-bar-danger')
|
||||
break
|
||||
case 'unstable':
|
||||
$barInner.addClass('progress-bar-warning')
|
||||
break
|
||||
case 'in progress':
|
||||
case 'queued':
|
||||
break
|
||||
}
|
||||
$barInner.attr('title', job.name)
|
||||
.css('width', jobPercent + '%')
|
||||
$barOutter.append($barInner)
|
||||
}
|
||||
})
|
||||
return $barOutter
|
||||
},
|
||||
|
||||
changeHeader: function (change) {
|
||||
let changeId = change.id || 'NA'
|
||||
|
||||
let $changeLink = $('<small />')
|
||||
if (change.url !== null) {
|
||||
let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
|
||||
if (githubId) {
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', changeId)
|
||||
.text('#' + githubId[1])
|
||||
)
|
||||
)
|
||||
} else if (/^[0-9a-f]{40}$/.test(changeId)) {
|
||||
let changeIdShort = changeId.slice(0, 7)
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', changeId)
|
||||
.text(changeIdShort)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).text(changeId)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (changeId.length === 40) {
|
||||
changeId = changeId.substr(0, 7)
|
||||
}
|
||||
$changeLink.text(changeId)
|
||||
}
|
||||
|
||||
let $changeProgressRowLeft = $('<div />')
|
||||
.addClass('col-xs-4')
|
||||
.append($changeLink)
|
||||
let $changeProgressRowRight = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append(this.changeTotalProgressBar(change))
|
||||
|
||||
let $changeProgressRow = $('<div />')
|
||||
.addClass('row')
|
||||
.append($changeProgressRowLeft)
|
||||
.append($changeProgressRowRight)
|
||||
|
||||
let $projectSpan = $('<span />')
|
||||
.addClass('change_project')
|
||||
.text(change.project)
|
||||
|
||||
let $left = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append($projectSpan, $changeProgressRow)
|
||||
|
||||
let remainingTime = this.time(change.remaining_time, true)
|
||||
let enqueueTime = this.enqueueTime(change.enqueue_time)
|
||||
let $remainingTime = $('<small />').addClass('time')
|
||||
.attr('title', 'Remaining Time').html(remainingTime)
|
||||
let $enqueueTime = $('<small />').addClass('time')
|
||||
.attr('title', 'Elapsed Time').html(enqueueTime)
|
||||
|
||||
let $right = $('<div />')
|
||||
if (change.live === true) {
|
||||
$right.addClass('col-xs-4 text-right')
|
||||
.append($remainingTime, $('<br />'), $enqueueTime)
|
||||
}
|
||||
|
||||
let $header = $('<div />')
|
||||
.addClass('row')
|
||||
.append($left, $right)
|
||||
return $header
|
||||
},
|
||||
|
||||
change_list: function (jobs) {
|
||||
let format = this
|
||||
let $list = $('<ul />')
|
||||
.addClass('list-group zuul-patchset-body')
|
||||
|
||||
$.each(jobs, function (i, job) {
|
||||
let $item = $('<li />')
|
||||
.addClass('list-group-item')
|
||||
.addClass('zuul-change-job')
|
||||
.append(format.job(job))
|
||||
$list.append($item)
|
||||
})
|
||||
|
||||
return $list
|
||||
},
|
||||
|
||||
changePanel: function (change) {
|
||||
let $header = $('<div />')
|
||||
.addClass('panel-heading zuul-patchset-header')
|
||||
.append(this.changeHeader(change))
|
||||
|
||||
let panelId = change.id ? change.id.replace(',', '_')
|
||||
: change.project.replace('/', '_') +
|
||||
'-' + change.enqueue_time
|
||||
let $panel = $('<div />')
|
||||
.attr('id', panelId)
|
||||
.addClass('panel panel-default zuul-change')
|
||||
.append($header)
|
||||
.append(this.change_list(change.jobs))
|
||||
|
||||
$header.click(this.toggle_patchset)
|
||||
return $panel
|
||||
},
|
||||
|
||||
change_status_icon: function (change) {
|
||||
let iconFile = GreenImage
|
||||
let iconTitle = 'Succeeding'
|
||||
|
||||
if (change.active !== true) {
|
||||
// Grey icon
|
||||
iconFile = GreyImage
|
||||
iconTitle = 'Waiting until closer to head of queue to' +
|
||||
' start jobs'
|
||||
} else if (change.live !== true) {
|
||||
// Grey icon
|
||||
iconFile = GreyImage
|
||||
iconTitle = 'Dependent change required for testing'
|
||||
} else if (change.failing_reasons &&
|
||||
change.failing_reasons.length > 0) {
|
||||
let reason = change.failing_reasons.join(', ')
|
||||
iconTitle = 'Failing because ' + reason
|
||||
if (reason.match(/merge conflict/)) {
|
||||
// Black icon
|
||||
iconFile = BlackImage
|
||||
} else {
|
||||
// Red icon
|
||||
iconFile = RedImage
|
||||
}
|
||||
}
|
||||
|
||||
let $icon = $('<img />')
|
||||
.attr('src', iconFile)
|
||||
.attr('title', iconTitle)
|
||||
.css('margin-top', '-6px')
|
||||
|
||||
return $icon
|
||||
},
|
||||
|
||||
change_with_status_tree: function (change, changeQueue) {
|
||||
let $changeRow = $('<tr />')
|
||||
|
||||
for (let i = 0; i < changeQueue._tree_columns; i++) {
|
||||
let $treeCell = $('<td />')
|
||||
.css('height', '100%')
|
||||
.css('padding', '0 0 10px 0')
|
||||
.css('margin', '0')
|
||||
.css('width', '16px')
|
||||
.css('min-width', '16px')
|
||||
.css('overflow', 'hidden')
|
||||
.css('vertical-align', 'top')
|
||||
|
||||
if (i < change._tree.length && change._tree[i] !== null) {
|
||||
$treeCell.css('background-image',
|
||||
'url(' + LineImage + ')')
|
||||
.css('background-repeat', 'repeat-y')
|
||||
}
|
||||
|
||||
if (i === change._tree_index) {
|
||||
$treeCell.append(
|
||||
this.change_status_icon(change))
|
||||
}
|
||||
if (change._tree_branches.indexOf(i) !== -1) {
|
||||
let $image = $('<img />')
|
||||
.css('vertical-align', 'baseline')
|
||||
if (change._tree_branches.indexOf(i) ===
|
||||
change._tree_branches.length - 1) {
|
||||
// Angle line
|
||||
$image.attr('src', LineAngleImage)
|
||||
} else {
|
||||
// T line
|
||||
$image.attr('src', LineTImage)
|
||||
}
|
||||
$treeCell.append($image)
|
||||
}
|
||||
$changeRow.append($treeCell)
|
||||
}
|
||||
|
||||
let changeWidth = 360 - 16 * changeQueue._tree_columns
|
||||
let $changeColumn = $('<td />')
|
||||
.css('width', changeWidth + 'px')
|
||||
.addClass('zuul-change-cell')
|
||||
.append(this.changePanel(change))
|
||||
|
||||
$changeRow.append($changeColumn)
|
||||
|
||||
let $changeTable = $('<table />')
|
||||
.addClass('zuul-change-box')
|
||||
.css('-moz-box-sizing', 'content-box')
|
||||
.css('box-sizing', 'content-box')
|
||||
.append($changeRow)
|
||||
|
||||
return $changeTable
|
||||
},
|
||||
|
||||
pipeline_sparkline: function (pipelineName) {
|
||||
if (options.graphite_url !== '') {
|
||||
let $sparkline = $('<img />')
|
||||
.addClass('pull-right')
|
||||
.attr('src', getSparklineURL(pipelineName))
|
||||
return $sparkline
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
pipeline_header: function (pipeline, count) {
|
||||
// Format the pipeline name, sparkline and description
|
||||
let $headerDiv = $('<div />')
|
||||
.addClass('zuul-pipeline-header')
|
||||
|
||||
let $heading = $('<h3 />')
|
||||
.css('vertical-align', 'middle')
|
||||
.text(pipeline.name)
|
||||
.append(
|
||||
$('<span />')
|
||||
.addClass('badge pull-right')
|
||||
.css('vertical-align', 'middle')
|
||||
.css('margin-top', '0.5em')
|
||||
.text(count)
|
||||
)
|
||||
.append(this.pipeline_sparkline(pipeline.name))
|
||||
|
||||
$headerDiv.append($heading)
|
||||
|
||||
if (typeof pipeline.description === 'string') {
|
||||
let descr = $('<small />')
|
||||
$.each(pipeline.description.split(/\r?\n\r?\n/),
|
||||
function (index, descrPart) {
|
||||
descr.append($('<p />').text(descrPart))
|
||||
})
|
||||
$headerDiv.append($('<p />').append(descr))
|
||||
}
|
||||
return $headerDiv
|
||||
},
|
||||
|
||||
pipeline: function (pipeline, count) {
|
||||
let format = this
|
||||
let $html = $('<div />')
|
||||
.addClass('zuul-pipeline col-md-4')
|
||||
.append(this.pipeline_header(pipeline, count))
|
||||
|
||||
$.each(pipeline.change_queues, function (queueIndex, changeQueue) {
|
||||
$.each(changeQueue.heads, function (headIndex, changes) {
|
||||
if (pipeline.change_queues.length > 1 && headIndex === 0) {
|
||||
let name = changeQueue.name
|
||||
let shortName = name
|
||||
if (shortName.length > 32) {
|
||||
shortName = shortName.substr(0, 32) + '...'
|
||||
}
|
||||
$html.append($('<p />')
|
||||
.text('Queue: ')
|
||||
.append(
|
||||
$('<abbr />')
|
||||
.attr('title', name)
|
||||
.text(shortName)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
$.each(changes, function (changeIndex, change) {
|
||||
let $changeBox =
|
||||
format.change_with_status_tree(
|
||||
change, changeQueue)
|
||||
$html.append($changeBox)
|
||||
format.display_patchset($changeBox)
|
||||
})
|
||||
})
|
||||
})
|
||||
return $html
|
||||
},
|
||||
|
||||
toggle_patchset: function (e) {
|
||||
// Toggle showing/hiding the patchset when the header is clicked.
|
||||
if (e.target.nodeName.toLowerCase() === 'a') {
|
||||
// Ignore clicks from gerrit patch set link
|
||||
return
|
||||
}
|
||||
|
||||
// Grab the patchset panel
|
||||
let $panel = $(e.target).parents('.zuul-change')
|
||||
let $body = $panel.children('.zuul-patchset-body')
|
||||
$body.toggle(200)
|
||||
let collapsedIndex = collapsedExceptions.indexOf(
|
||||
$panel.attr('id'))
|
||||
if (collapsedIndex === -1) {
|
||||
// Currently not an exception, add it to list
|
||||
collapsedExceptions.push($panel.attr('id'))
|
||||
} else {
|
||||
// Currently an except, remove from exceptions
|
||||
collapsedExceptions.splice(collapsedIndex, 1)
|
||||
}
|
||||
},
|
||||
|
||||
display_patchset: function ($changeBox, animate) {
|
||||
// Determine if to show or hide the patchset and/or the results
|
||||
// when loaded
|
||||
|
||||
// See if we should hide the body/results
|
||||
let $panel = $changeBox.find('.zuul-change')
|
||||
let panelChange = $panel.attr('id')
|
||||
let $body = $panel.children('.zuul-patchset-body')
|
||||
let expandByDefault = $('#expand_by_default')
|
||||
.prop('checked')
|
||||
|
||||
let collapsedIndex = collapsedExceptions
|
||||
.indexOf(panelChange)
|
||||
|
||||
if ((expandByDefault && collapsedIndex === -1) ||
|
||||
(!expandByDefault && collapsedIndex !== -1)) {
|
||||
// Expand by default, or is an exception
|
||||
$body.show(animate)
|
||||
} else {
|
||||
$body.hide(animate)
|
||||
}
|
||||
|
||||
// Check if we should hide the whole panel
|
||||
let panelProject = $panel.find('.change_project').text()
|
||||
.toLowerCase()
|
||||
|
||||
let panelPipeline = $changeBox
|
||||
.parents('.zuul-pipeline')
|
||||
.find('.zuul-pipeline-header > h3')
|
||||
.html()
|
||||
.toLowerCase()
|
||||
|
||||
if (currentFilter !== '') {
|
||||
let showPanel = false
|
||||
let filter = currentFilter.trim().split(/[\s,]+/)
|
||||
$.each(filter, function (index, filterVal) {
|
||||
if (filterVal !== '') {
|
||||
filterVal = filterVal.toLowerCase()
|
||||
if (panelProject.indexOf(filterVal) !== -1 ||
|
||||
panelPipeline.indexOf(filterVal) !== -1 ||
|
||||
panelChange.indexOf(filterVal) !== -1) {
|
||||
showPanel = true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (showPanel === true) {
|
||||
$changeBox.show(animate)
|
||||
} else {
|
||||
$changeBox.hide(animate)
|
||||
}
|
||||
} else {
|
||||
$changeBox.show(animate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app = {
|
||||
schedule: function (app) {
|
||||
app = app || this
|
||||
if (!options.enabled) {
|
||||
setTimeout(function () { app.schedule(app) }, 5000)
|
||||
return
|
||||
}
|
||||
app.update().always(function () {
|
||||
setTimeout(function () { app.schedule(app) }, 5000)
|
||||
})
|
||||
|
||||
// Only update graphs every minute
|
||||
if (zuulGraphUpdateCount > 11) {
|
||||
zuulGraphUpdateCount = 0
|
||||
$.zuul.update_sparklines()
|
||||
}
|
||||
},
|
||||
injest: function (data, $msg) {
|
||||
if ('message' in data) {
|
||||
$msg.removeClass('alert-danger')
|
||||
.addClass('alert-info')
|
||||
.text(data.message)
|
||||
.show()
|
||||
} else {
|
||||
$msg.empty()
|
||||
.hide()
|
||||
}
|
||||
|
||||
if ('zuul_version' in data) {
|
||||
$('#zuul-version-span').text(data.zuul_version)
|
||||
}
|
||||
if ('last_reconfigured' in data) {
|
||||
let lastReconfigured =
|
||||
new Date(data.last_reconfigured)
|
||||
$('#last-reconfigured-span').text(
|
||||
lastReconfigured.toString())
|
||||
}
|
||||
|
||||
let $pipelines = $(options.pipelines_id)
|
||||
$pipelines.html('')
|
||||
$.each(data.pipelines, function (i, pipeline) {
|
||||
let count = app.create_tree(pipeline)
|
||||
$pipelines.append(
|
||||
format.pipeline(pipeline, count))
|
||||
})
|
||||
|
||||
$(options.queue_events_num).text(
|
||||
data.trigger_event_queue
|
||||
? data.trigger_event_queue.length : '0'
|
||||
)
|
||||
$(options.queue_results_num).text(
|
||||
data.result_event_queue
|
||||
? data.result_event_queue.length : '0'
|
||||
)
|
||||
},
|
||||
/** @return {jQuery.Promise} */
|
||||
update: function () {
|
||||
// Cancel the previous update if it hasn't completed yet.
|
||||
if (xhr) {
|
||||
xhr.abort()
|
||||
}
|
||||
|
||||
this.emit('update-start')
|
||||
let app = this
|
||||
|
||||
let $msg = $(options.msg_id)
|
||||
if (options.source_data !== null) {
|
||||
app.injest(options.source_data, $msg)
|
||||
return
|
||||
}
|
||||
xhr = $.getJSON(options.source)
|
||||
.done(function (data) {
|
||||
app.injest(data, $msg)
|
||||
})
|
||||
.fail(function (jqXHR, statusText, errMsg) {
|
||||
if (statusText === 'abort') {
|
||||
return
|
||||
}
|
||||
$msg.text(options.source + ': ' + errMsg)
|
||||
.addClass('alert-danger')
|
||||
.removeClass('zuul-msg-wrap-off')
|
||||
.show()
|
||||
})
|
||||
.always(function () {
|
||||
xhr = undefined
|
||||
app.emit('update-end')
|
||||
})
|
||||
|
||||
return xhr
|
||||
},
|
||||
|
||||
update_sparklines: function () {
|
||||
$.each(zuulSparklineURLs, function (name, url) {
|
||||
let newimg = new Image()
|
||||
let parts = url.split('#')
|
||||
newimg.src = parts[0] + '#' + new Date().getTime()
|
||||
$(newimg).load(function () {
|
||||
zuulSparklineURLs[name] = newimg.src
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
controlForm: function () {
|
||||
// Build the filter form filling anything from cookies
|
||||
|
||||
let $controlForm = $('<form />')
|
||||
.attr('role', 'form')
|
||||
.addClass('form-inline')
|
||||
.submit(this.handleFilterChange)
|
||||
|
||||
$controlForm
|
||||
.append(this.filterFormGroup())
|
||||
.append(this.expandFormGroup())
|
||||
|
||||
return $controlForm
|
||||
},
|
||||
|
||||
filterFormGroup: function () {
|
||||
// Update the filter form with a clear button if required
|
||||
|
||||
let $label = $('<label />')
|
||||
.addClass('control-label')
|
||||
.attr('for', 'filter_string')
|
||||
.text('Filters')
|
||||
.css('padding-right', '0.5em')
|
||||
|
||||
let $input = $('<input />')
|
||||
.attr('type', 'text')
|
||||
.attr('id', 'filter_string')
|
||||
.addClass('form-control')
|
||||
.attr('title',
|
||||
'project(s), pipeline(s) or review(s) comma ' +
|
||||
'separated')
|
||||
.attr('value', currentFilter)
|
||||
|
||||
$input.change(this.handleFilterChange)
|
||||
|
||||
let $clearIcon = $('<span />')
|
||||
.addClass('form-control-feedback')
|
||||
.addClass('glyphicon glyphicon-remove-circle')
|
||||
.attr('id', 'filter_form_clear_box')
|
||||
.attr('title', 'clear filter')
|
||||
.css('cursor', 'pointer')
|
||||
|
||||
$clearIcon.click(function () {
|
||||
$('#filter_string').val('').change()
|
||||
})
|
||||
|
||||
if (currentFilter === '') {
|
||||
$clearIcon.hide()
|
||||
}
|
||||
|
||||
let $formGroup = $('<div />')
|
||||
.addClass('form-group has-feedback')
|
||||
.append($label, $input, $clearIcon)
|
||||
return $formGroup
|
||||
},
|
||||
|
||||
expandFormGroup: function () {
|
||||
let expandByDefault = (
|
||||
readCookie('zuul_expand_by_default', false) === 'true')
|
||||
|
||||
let $checkbox = $('<input />')
|
||||
.attr('type', 'checkbox')
|
||||
.attr('id', 'expand_by_default')
|
||||
.prop('checked', expandByDefault)
|
||||
.change(this.handleExpandByDefault)
|
||||
|
||||
let $label = $('<label />')
|
||||
.css('padding-left', '1em')
|
||||
.html('Expand by default: ')
|
||||
.append($checkbox)
|
||||
|
||||
let $formGroup = $('<div />')
|
||||
.addClass('checkbox')
|
||||
.append($label)
|
||||
return $formGroup
|
||||
},
|
||||
|
||||
handleFilterChange: function () {
|
||||
// Update the filter and save it to a cookie
|
||||
currentFilter = $('#filter_string').val()
|
||||
setCookie('zuul_filter_string', currentFilter)
|
||||
if (currentFilter === '') {
|
||||
$('#filter_form_clear_box').hide()
|
||||
} else {
|
||||
$('#filter_form_clear_box').show()
|
||||
}
|
||||
|
||||
$('.zuul-change-box').each(function (index, obj) {
|
||||
let $changeBox = $(obj)
|
||||
format.display_patchset($changeBox, 200)
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
handleExpandByDefault: function (e) {
|
||||
// Handle toggling expand by default
|
||||
setCookie('zuul_expand_by_default', e.target.checked)
|
||||
collapsedExceptions = []
|
||||
$('.zuul-change-box').each(function (index, obj) {
|
||||
let $changeBox = $(obj)
|
||||
format.display_patchset($changeBox, 200)
|
||||
})
|
||||
},
|
||||
|
||||
create_tree: function (pipeline) {
|
||||
let count = 0
|
||||
let pipelineMaxTreeColumns = 1
|
||||
$.each(pipeline.change_queues,
|
||||
function (changeQueueIndex, changeQueue) {
|
||||
let tree = []
|
||||
let maxTreeColumns = 1
|
||||
let changes = []
|
||||
let lastTreeLength = 0
|
||||
$.each(changeQueue.heads, function (headIndex, head) {
|
||||
$.each(head, function (changeIndex, change) {
|
||||
changes[change.id] = change
|
||||
change._tree_position = changeIndex
|
||||
})
|
||||
})
|
||||
$.each(changeQueue.heads, function (headIndex, head) {
|
||||
$.each(head, function (changeIndex, change) {
|
||||
if (change.live === true) {
|
||||
count += 1
|
||||
}
|
||||
let idx = tree.indexOf(change.id)
|
||||
if (idx > -1) {
|
||||
change._tree_index = idx
|
||||
// remove...
|
||||
tree[idx] = null
|
||||
while (tree[tree.length - 1] === null) {
|
||||
tree.pop()
|
||||
}
|
||||
} else {
|
||||
change._tree_index = 0
|
||||
}
|
||||
change._tree_branches = []
|
||||
change._tree = []
|
||||
if (typeof (change.items_behind) === 'undefined') {
|
||||
change.items_behind = []
|
||||
}
|
||||
change.items_behind.sort(function (a, b) {
|
||||
return (changes[b]._tree_position - changes[a]._tree_position)
|
||||
})
|
||||
$.each(change.items_behind, function (i, id) {
|
||||
tree.push(id)
|
||||
if (tree.length > lastTreeLength && lastTreeLength > 0) {
|
||||
change._tree_branches.push(tree.length - 1)
|
||||
}
|
||||
})
|
||||
if (tree.length > maxTreeColumns) {
|
||||
maxTreeColumns = tree.length
|
||||
}
|
||||
if (tree.length > pipelineMaxTreeColumns) {
|
||||
pipelineMaxTreeColumns = tree.length
|
||||
}
|
||||
change._tree = tree.slice(0) // make a copy
|
||||
lastTreeLength = tree.length
|
||||
})
|
||||
})
|
||||
changeQueue._tree_columns = maxTreeColumns
|
||||
})
|
||||
pipeline._tree_columns = pipelineMaxTreeColumns
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
$jq = $(app)
|
||||
return {
|
||||
options: options,
|
||||
format: format,
|
||||
app: app,
|
||||
jq: $jq
|
||||
}
|
||||
}
|
||||
}(jQuery))
|
|
@ -0,0 +1,25 @@
|
|||
// Main library entrypoint
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2018 Red Hat, Inc.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
import './status'
|
||||
import './stream'
|
||||
import './dashboard'
|
|
@ -0,0 +1,126 @@
|
|||
/* global jQuery, URL, DemoStatusBasic, DemoStatusOpenStack, DemoStatusTree, BuiltinConfig */
|
||||
// Client script for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2013 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'jquery-visibility/jquery-visibility'
|
||||
import 'graphitejs/jquery.graphite.js'
|
||||
import angular from 'angular'
|
||||
|
||||
import './styles/zuul.css'
|
||||
import './jquery.zuul'
|
||||
import { getSourceUrl } from './util'
|
||||
|
||||
/**
|
||||
* @return The $.zuul instance
|
||||
*/
|
||||
function zuulStart ($) {
|
||||
// Start the zuul app (expects default dom)
|
||||
|
||||
let $container, $indicator
|
||||
|
||||
let url = new URL(window.location)
|
||||
let params = {
|
||||
// graphite_url: 'http://graphite.openstack.org/render/'
|
||||
}
|
||||
|
||||
if (typeof BuiltinConfig !== 'undefined') {
|
||||
params['source'] = BuiltinConfig.api_endpoint + '/' + 'status'
|
||||
} else if (url.searchParams.has('source_url')) {
|
||||
params['source'] = url.searchParams.get('source_url') + '/' + 'status'
|
||||
} else if (url.searchParams.has('demo')) {
|
||||
let demo = url.searchParams.get('demo') || 'basic'
|
||||
if (demo === 'basic') {
|
||||
params['source_data'] = DemoStatusBasic
|
||||
} else if (demo === 'openstack') {
|
||||
params['source_data'] = DemoStatusOpenStack
|
||||
} else if (demo === 'tree') {
|
||||
params['source_data'] = DemoStatusTree
|
||||
}
|
||||
} else {
|
||||
params['source'] = getSourceUrl('status')
|
||||
}
|
||||
|
||||
let zuul = $.zuul(params)
|
||||
|
||||
zuul.jq.on('update-start', function () {
|
||||
$container.addClass('zuul-container-loading')
|
||||
$indicator.addClass('zuul-spinner-on')
|
||||
})
|
||||
|
||||
zuul.jq.on('update-end', function () {
|
||||
$container.removeClass('zuul-container-loading')
|
||||
setTimeout(function () {
|
||||
$indicator.removeClass('zuul-spinner-on')
|
||||
}, 500)
|
||||
})
|
||||
|
||||
zuul.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 () {
|
||||
// Fade in the content
|
||||
$container.addClass('zuul-container-ready')
|
||||
})
|
||||
})
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
$container = $('#zuul-container')
|
||||
$indicator = $('#zuul-spinner')
|
||||
$('#zuul_controls').append(zuul.app.controlForm())
|
||||
|
||||
zuul.app.schedule()
|
||||
|
||||
$(document).on({
|
||||
'show.visibility': function () {
|
||||
zuul.options.enabled = true
|
||||
zuul.app.update()
|
||||
},
|
||||
'hide.visibility': function () {
|
||||
zuul.options.enabled = false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return zuul
|
||||
}
|
||||
|
||||
if (module.hot) {
|
||||
// This doesn't fully work with our jquery plugin because $.zuul is already
|
||||
// instantiated. Leaving it here to show where a hook can happen if we can
|
||||
// figure out a way to live update it. When it's not there, an update to
|
||||
// jquery.zuul.js triggers a page reload.
|
||||
// module.hot.accept('./jquery.zuul', function() {
|
||||
// console.log('Accepting the updated module!');
|
||||
// })
|
||||
}
|
||||
|
||||
angular.module('zuulStatus', []).controller(
|
||||
'mainController', function ($scope, $http) {
|
||||
zuulStart(jQuery)
|
||||
}
|
||||
)
|
|
@ -0,0 +1,99 @@
|
|||
/* global URL, WebSocket, BuiltinConfig */
|
||||
// Client script for Zuul Log Streaming
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2017 BMW Car IT GmbH
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice for the JavaScript code in
|
||||
// this page.
|
||||
|
||||
import angular from 'angular'
|
||||
import './styles/stream.css'
|
||||
|
||||
function escapeLog (text) {
|
||||
const pattern = /[<>&"']/g
|
||||
|
||||
return text.replace(pattern, function (match) {
|
||||
return '&#' + match.charCodeAt(0) + ';'
|
||||
})
|
||||
}
|
||||
|
||||
function zuulStartStream () {
|
||||
let pageUpdateInMS = 250
|
||||
let receiveBuffer = ''
|
||||
|
||||
setInterval(function () {
|
||||
console.log('autoScroll')
|
||||
if (receiveBuffer !== '') {
|
||||
document.getElementById('zuulstreamcontent').innerHTML += receiveBuffer
|
||||
receiveBuffer = ''
|
||||
if (document.getElementById('autoscroll').checked) {
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
}
|
||||
}
|
||||
}, pageUpdateInMS)
|
||||
|
||||
let url = new URL(window.location)
|
||||
|
||||
let params = {
|
||||
uuid: url.searchParams.get('uuid')
|
||||
}
|
||||
document.getElementById('pagetitle').innerHTML = params['uuid']
|
||||
if (url.searchParams.has('logfile')) {
|
||||
params['logfile'] = url.searchParams.get('logfile')
|
||||
let logfileSuffix = `(${params['logfile']})`
|
||||
document.getElementById('pagetitle').innerHTML += logfileSuffix
|
||||
}
|
||||
if (typeof BuiltinConfig !== 'undefined') {
|
||||
params['websocket_url'] = BuiltinConfig.websocket_url
|
||||
} else if (url.searchParams.has('websocket_url')) {
|
||||
params['websocket_url'] = url.searchParams.get('websocket_url')
|
||||
} else {
|
||||
// Websocket doesn't accept relative urls so construct an
|
||||
// absolute one.
|
||||
let protocol = ''
|
||||
if (url['protocol'] === 'https:') {
|
||||
protocol = 'wss://'
|
||||
} else {
|
||||
protocol = 'ws://'
|
||||
}
|
||||
let path = url['pathname'].replace(/stream.html.*$/g, '') + 'console-stream'
|
||||
params['websocket_url'] = protocol + url['host'] + path
|
||||
}
|
||||
let ws = new WebSocket(params['websocket_url'])
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
console.log('onmessage')
|
||||
receiveBuffer = receiveBuffer + escapeLog(event.data)
|
||||
}
|
||||
|
||||
ws.onopen = function (event) {
|
||||
console.log('onopen')
|
||||
ws.send(JSON.stringify(params))
|
||||
}
|
||||
|
||||
ws.onclose = function (event) {
|
||||
console.log('onclose')
|
||||
receiveBuffer = receiveBuffer + '\n--- END OF STREAM ---\n'
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('zuulStream', []).controller(
|
||||
'mainController', function ($scope, $http) {
|
||||
window.onload = zuulStartStream()
|
||||
}
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
body#zuulstream {
|
||||
font-family: monospace;
|
||||
background-color: black;
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
#zuulstreamoverlay {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: darkgrey;
|
||||
color: black;
|
||||
}
|
||||
|
||||
pre#zuulstreamcontent {
|
||||
white-space: pre;
|
||||
margin: 0px 10px;
|
||||
}
|
|
@ -55,4 +55,4 @@
|
|||
.zuul-patchset-header {
|
||||
font-size: small;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
|
@ -13,15 +14,10 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Zuul Builds</title>
|
||||
<link rel="stylesheet" href="../static/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="../static/styles/zuul.css" />
|
||||
<script src="../static/js/jquery.min.js"></script>
|
||||
<script src="../static/js/angular.min.js"></script>
|
||||
<script src="../static/javascripts/zuul.angular.js"></script>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body ng-app="zuulBuilds" ng-controller="mainController"><div class="container-fluid">
|
||||
<nav class="navbar navbar-default">
|
|
@ -1,3 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
|
@ -13,15 +14,10 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Zuul Builds</title>
|
||||
<link rel="stylesheet" href="../static/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="../static/styles/zuul.css" />
|
||||
<script src="../static/js/jquery.min.js"></script>
|
||||
<script src="../static/js/angular.min.js"></script>
|
||||
<script src="../static/javascripts/zuul.angular.js"></script>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body ng-app="zuulJobs" ng-controller="mainController"><div class="container-fluid">
|
||||
<nav class="navbar navbar-default">
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
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.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body ng-app="zuulStatus" ng-controller="mainController">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="../" target="_self">Zuul Dashboard</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="status.html" target="_self">Status</a></li>
|
||||
<li><a href="jobs.html" target="_self">Jobs</a></li>
|
||||
<li><a href="builds.html" target="_self">Builds</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="zuul-container" id="zuul-container">
|
||||
<div style="display: none;" class="alert" id="zuul_msg"></div>
|
||||
<button class="btn pull-right zuul-spinner">
|
||||
updating <span class="glyphicon glyphicon-refresh"></span>
|
||||
</button>
|
||||
<p>Queue lengths:
|
||||
<span id="zuul_queue_events_num">0</span> events,
|
||||
<span id="zuul_queue_management_events_num">0</span> management events,
|
||||
<span id="zuul_queue_results_num">0</span> results.
|
||||
</p>
|
||||
<div id="zuul_controls"></div>
|
||||
<div id="zuul_pipelines" class="row"></div>
|
||||
<p>Zuul version: <span id="zuul-version-span"></span></p>
|
||||
<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body ng-app="zuulStream" ng-controller="mainController" id="zuulstream">
|
||||
<div id="zuulstreamoverlay">
|
||||
<form>
|
||||
<input type="checkbox" id="autoscroll" checked> autoscroll
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<pre id="zuulstreamcontent"></pre>
|
||||
</div></body>
|
||||
</html>
|
|
@ -1,3 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
|
@ -13,15 +14,10 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Zuul Tenants</title>
|
||||
<link rel="stylesheet" href="static/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/styles/zuul.css" />
|
||||
<script src="static/js/jquery.min.js"></script>
|
||||
<script src="static/js/angular.min.js"></script>
|
||||
<script src="static/javascripts/zuul.angular.js"></script>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body ng-app="zuulTenants" ng-controller="mainController"><div class="container-fluid">
|
||||
<nav class="navbar navbar-default">
|
||||
|
@ -30,7 +26,7 @@ under the License.
|
|||
<a class="navbar-brand">Zuul Dashboard</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="tenants.html">Tenants</a></li>
|
||||
<li class="active"><a href=".">Tenants</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
|
@ -0,0 +1,30 @@
|
|||
/* global ZUUL_API_URL */
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
// TODO(mordred) This should be encapsulated in an Angular Service singleton
|
||||
// that fetches the other things from the info endpoint.
|
||||
export function getSourceUrl (filename, $location) {
|
||||
if (typeof ZUUL_API_URL !== 'undefined') {
|
||||
return ZUUL_API_URL + '/' + filename
|
||||
} else {
|
||||
return filename
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function(env) {
|
||||
return require(`./web/config/webpack.${env}.js`)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2018 Red Hat, Inc
|
||||
#
|
||||
# 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.
|
||||
"""Hook for pbr to build javascript as part of tarball."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pbr.packaging
|
||||
|
||||
_old_from_git = pbr.packaging._from_git
|
||||
|
||||
|
||||
def _build_javascript():
|
||||
if subprocess.call(['which', 'yarn']) != 0:
|
||||
return
|
||||
if not os.path.exists('node_modules/.bin/webpack'):
|
||||
subprocess.check_call(['yarn', 'install', '-d'])
|
||||
if not os.path.exists('zuul/web/static/status.bundle.js'):
|
||||
subprocess.check_call(['npm', 'run', 'build:dist'])
|
||||
|
||||
|
||||
def _from_git(distribution):
|
||||
_build_javascript()
|
||||
return _old_from_git(distribution)
|
||||
|
||||
|
||||
def setup_hook(config):
|
||||
pbr.packaging._from_git = _from_git
|
|
@ -46,6 +46,9 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
|
|||
params['static_cache_expiry'] = get_default(self.config, 'web',
|
||||
'static_cache_expiry',
|
||||
3600)
|
||||
params['static_path'] = get_default(self.config,
|
||||
'web', 'static_path',
|
||||
None)
|
||||
params['gear_server'] = get_default(self.config, 'gearman', 'server')
|
||||
params['gear_port'] = get_default(self.config, 'gearman', 'port', 4730)
|
||||
params['ssl_key'] = get_default(self.config, 'gearman', 'ssl_key')
|
||||
|
|
|
@ -237,7 +237,8 @@ class ZuulWeb(object):
|
|||
ssl_key=None, ssl_cert=None, ssl_ca=None,
|
||||
static_cache_expiry=3600,
|
||||
connections=None,
|
||||
info=None):
|
||||
info=None,
|
||||
static_path=None):
|
||||
self.start_time = time.time()
|
||||
self.listen_address = listen_address
|
||||
self.listen_port = listen_port
|
||||
|
@ -246,6 +247,7 @@ class ZuulWeb(object):
|
|||
self.server = None
|
||||
self.static_cache_expiry = static_cache_expiry
|
||||
self.info = info
|
||||
self.static_path = static_path or STATIC_DIR
|
||||
# instanciate handlers
|
||||
self.rpc = zuul.rpcclient.RPCClient(gear_server, gear_port,
|
||||
ssl_key, ssl_cert, ssl_ca)
|
||||
|
@ -320,13 +322,22 @@ class ZuulWeb(object):
|
|||
]
|
||||
|
||||
static_routes = [
|
||||
StaticHandler(self, '/{tenant}/status.html'),
|
||||
StaticHandler(self, '/{tenant}/jobs.html'),
|
||||
StaticHandler(self, '/{tenant}/stream.html'),
|
||||
StaticHandler(self, '/tenants.html', 'index.html'),
|
||||
StaticHandler(self, '/', 'index.html'),
|
||||
StaticHandler(self, '/{tenant}/', 'status.html'),
|
||||
StaticHandler(self, '/', 'tenants.html'),
|
||||
]
|
||||
|
||||
for static_file in os.listdir(self.static_path):
|
||||
static_routes.append(
|
||||
StaticHandler(
|
||||
self, '/{{tenant}}/{static_file}'.format(
|
||||
static_file=static_file),
|
||||
static_file))
|
||||
static_routes.append(
|
||||
StaticHandler(
|
||||
self, '/{static_file}'.format(
|
||||
static_file=static_file),
|
||||
static_file))
|
||||
|
||||
for route in static_routes + self._plugin_routes:
|
||||
routes.append((route.method, route.path, route.handleRequest))
|
||||
|
||||
|
@ -343,7 +354,7 @@ class ZuulWeb(object):
|
|||
app = web.Application()
|
||||
for method, path, handler in routes:
|
||||
app.router.add_route(method, path, handler)
|
||||
app.router.add_static('/static', STATIC_DIR)
|
||||
app.router.add_static('/static', self.static_path)
|
||||
handler = app.make_handler(loop=self.event_loop)
|
||||
|
||||
# create the server
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
External requirements needs to be installed in these locations
|
||||
* /static/js/angular.min.js
|
||||
* /static/js/jquery.min.js
|
||||
* /static/js/jquery-visibility.min.js
|
||||
* /static/js/jquery.graphite.min.js
|
||||
* /static/bootstrap/css/bootstrap.min.css
|
||||
|
||||
|
||||
Use python2-rjsmin or another js minifier:
|
||||
```
|
||||
DEST_DIR=/var/www/html/static/
|
||||
mkdir -p $DEST_DIR/js
|
||||
echo "Fetching angular..."
|
||||
curl -L --silent https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js > $DEST_DIR/js/angular.min.js
|
||||
|
||||
echo "Fetching jquery..."
|
||||
curl -L --silent http://code.jquery.com/jquery.min.js > $DEST_DIR/js/jquery.min.js
|
||||
|
||||
echo "Fetching jquery-visibility..."
|
||||
curl -L --silent https://raw.githubusercontent.com/mathiasbynens/jquery-visibility/master/jquery-visibility.js > $DEST_DIR/js/jquery-visibility.js
|
||||
python2 -mrjsmin < $DEST_DIR/js/jquery-visibility.js > $DEST_DIR/js/jquery-visibility.min.js
|
||||
|
||||
echo "Fetching bootstrap..."
|
||||
curl -L --silent https://github.com/twbs/bootstrap/releases/download/v3.1.1/bootstrap-3.1.1-dist.zip > bootstrap.zip
|
||||
unzip -q -o bootstrap.zip -d $DEST_DIR/
|
||||
mv $DEST_DIR/bootstrap-3.1.1-dist $DEST_DIR/bootstrap
|
||||
rm -f bootstrap.zip
|
||||
|
||||
echo "Fetching jquery-graphite..."
|
||||
curl -L --silent https://github.com/prestontimmons/graphitejs/archive/master.zip > jquery-graphite.zip
|
||||
unzip -q -o jquery-graphite.zip -d $DEST_DIR/
|
||||
python2 -mrjsmin < $DEST_DIR/graphitejs-master/jquery.graphite.js > $DEST_DIR/js/jquery.graphite.min.js
|
||||
rm -Rf jquery-graphite.zip $DEST_DIR/graphitejs-master
|
||||
```
|
||||
|
||||
|
||||
Here is an example apache vhost configuration:
|
||||
<VirtualHost zuul-web.example.com:80>
|
||||
DocumentRoot /var/www/zuul-web
|
||||
|
||||
LogLevel warn
|
||||
|
||||
Alias "/static" "/var/www/zuul-web"
|
||||
AliasMatch "^/.*/(.*).html" "/var/www/zuul-web/$1.html"
|
||||
AliasMatch "^/.*.html" "/var/www/zuul-web/index.html"
|
||||
<Directory /var/www/zuul-web>
|
||||
Require all granted
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
# Console-stream needs a special proxy-pass for websocket
|
||||
ProxyPassMatch /(.*)/console-stream ws://localhost:9000/$1/console-stream nocanon retry=0
|
||||
|
||||
# Then only the json calls are sent to the zuul-web endpoints
|
||||
ProxyPassMatch ^/(.*.json)$ http://localhost:9000/$1 nocanon retry=0
|
||||
ProxyPassReverse / http://localhost:9000/
|
||||
</VirtualHost>
|
||||
|
||||
Then copy the zuul/web/static/ files and external requirements to
|
||||
/var/www/zuul-web
|
Before Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 286 B |
|
@ -1,945 +0,0 @@
|
|||
// jquery plugin for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2012 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
function set_cookie(name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/';
|
||||
}
|
||||
|
||||
function read_cookie(name, default_value) {
|
||||
var nameEQ = name + '=';
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0;i < ca.length;i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
$.zuul = function(options) {
|
||||
options = $.extend({
|
||||
'enabled': true,
|
||||
'graphite_url': '',
|
||||
'source': 'status',
|
||||
'msg_id': '#zuul_msg',
|
||||
'pipelines_id': '#zuul_pipelines',
|
||||
'queue_events_num': '#zuul_queue_events_num',
|
||||
'queue_management_events_num': '#zuul_queue_management_events_num',
|
||||
'queue_results_num': '#zuul_queue_results_num',
|
||||
}, options);
|
||||
|
||||
var collapsed_exceptions = [];
|
||||
var current_filter = read_cookie('zuul_filter_string', '');
|
||||
var change_set_in_url = window.location.href.split('#')[1];
|
||||
if (change_set_in_url) {
|
||||
current_filter = change_set_in_url;
|
||||
}
|
||||
var $jq;
|
||||
|
||||
var xhr,
|
||||
zuul_graph_update_count = 0,
|
||||
zuul_sparkline_urls = {};
|
||||
|
||||
function get_sparkline_url(pipeline_name) {
|
||||
if (options.graphite_url !== '') {
|
||||
if (!(pipeline_name in zuul_sparkline_urls)) {
|
||||
zuul_sparkline_urls[pipeline_name] = $.fn.graphite
|
||||
.geturl({
|
||||
url: options.graphite_url,
|
||||
from: "-8hours",
|
||||
width: 100,
|
||||
height: 26,
|
||||
margin: 0,
|
||||
hideLegend: true,
|
||||
hideAxes: true,
|
||||
hideGrid: true,
|
||||
target: [
|
||||
"color(stats.gauges.zuul.pipeline." + pipeline_name
|
||||
+ ".current_changes, '6b8182')"
|
||||
]
|
||||
});
|
||||
}
|
||||
return zuul_sparkline_urls[pipeline_name];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var format = {
|
||||
job: function(job) {
|
||||
var $job_line = $('<span />');
|
||||
|
||||
if (job.result !== null) {
|
||||
$job_line.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.report_url)
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
else if (job.url !== null) {
|
||||
$job_line.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.url)
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$job_line.append(
|
||||
$('<span />')
|
||||
.addClass('zuul-job-name')
|
||||
.text(job.name)
|
||||
);
|
||||
}
|
||||
|
||||
$job_line.append(this.job_status(job));
|
||||
|
||||
if (job.voting === false) {
|
||||
$job_line.append(
|
||||
$(' <small />')
|
||||
.addClass('zuul-non-voting-desc')
|
||||
.text(' (non-voting)')
|
||||
);
|
||||
}
|
||||
|
||||
$job_line.append($('<div style="clear: both"></div>'));
|
||||
return $job_line;
|
||||
},
|
||||
|
||||
job_status: function(job) {
|
||||
var result = job.result ? job.result.toLowerCase() : null;
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued';
|
||||
}
|
||||
|
||||
if (result === 'in progress') {
|
||||
return this.job_progress_bar(job.elapsed_time,
|
||||
job.remaining_time);
|
||||
}
|
||||
else {
|
||||
return this.status_label(result);
|
||||
}
|
||||
},
|
||||
|
||||
status_label: function(result) {
|
||||
var $status = $('<span />');
|
||||
$status.addClass('zuul-job-result label');
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$status.addClass('label-success');
|
||||
break;
|
||||
case 'failure':
|
||||
$status.addClass('label-danger');
|
||||
break;
|
||||
case 'unstable':
|
||||
$status.addClass('label-warning');
|
||||
break;
|
||||
case 'skipped':
|
||||
$status.addClass('label-info');
|
||||
break;
|
||||
// 'in progress' 'queued' 'lost' 'aborted' ...
|
||||
default:
|
||||
$status.addClass('label-default');
|
||||
}
|
||||
$status.text(result);
|
||||
return $status;
|
||||
},
|
||||
|
||||
job_progress_bar: function(elapsed_time, remaining_time) {
|
||||
var progress_percent = 100 * (elapsed_time / (elapsed_time +
|
||||
remaining_time));
|
||||
var $bar_inner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
.attr('role', 'progressbar')
|
||||
.attr('aria-valuenow', 'progressbar')
|
||||
.attr('aria-valuemin', progress_percent)
|
||||
.attr('aria-valuemin', '0')
|
||||
.attr('aria-valuemax', '100')
|
||||
.css('width', progress_percent + '%');
|
||||
|
||||
var $bar_outter = $('<div />')
|
||||
.addClass('progress zuul-job-result')
|
||||
.append($bar_inner);
|
||||
|
||||
return $bar_outter;
|
||||
},
|
||||
|
||||
enqueue_time: function(ms) {
|
||||
// Special format case for enqueue time to add style
|
||||
var hours = 60 * 60 * 1000;
|
||||
var now = Date.now();
|
||||
var delta = now - ms;
|
||||
var status = 'text-success';
|
||||
var text = this.time(delta, true);
|
||||
if (delta > (4 * hours)) {
|
||||
status = 'text-danger';
|
||||
} else if (delta > (2 * hours)) {
|
||||
status = 'text-warning';
|
||||
}
|
||||
return '<span class="' + status + '">' + text + '</span>';
|
||||
},
|
||||
|
||||
time: function(ms, words) {
|
||||
if (typeof(words) === 'undefined') {
|
||||
words = false;
|
||||
}
|
||||
var seconds = (+ms)/1000;
|
||||
var minutes = Math.floor(seconds/60);
|
||||
var hours = Math.floor(minutes/60);
|
||||
seconds = Math.floor(seconds % 60);
|
||||
minutes = Math.floor(minutes % 60);
|
||||
var r = '';
|
||||
if (words) {
|
||||
if (hours) {
|
||||
r += hours;
|
||||
r += ' hr ';
|
||||
}
|
||||
r += minutes + ' min';
|
||||
} else {
|
||||
if (hours < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += hours + ':';
|
||||
if (minutes < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += minutes + ':';
|
||||
if (seconds < 10) {
|
||||
r += '0';
|
||||
}
|
||||
r += seconds;
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
change_total_progress_bar: function(change) {
|
||||
var job_percent = Math.floor(100 / change.jobs.length);
|
||||
var $bar_outter = $('<div />')
|
||||
.addClass('progress zuul-change-total-result');
|
||||
|
||||
$.each(change.jobs, function (i, job) {
|
||||
var result = job.result ? job.result.toLowerCase() : null;
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued';
|
||||
}
|
||||
|
||||
if (result !== 'queued') {
|
||||
var $bar_inner = $('<div />')
|
||||
.addClass('progress-bar');
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$bar_inner.addClass('progress-bar-success');
|
||||
break;
|
||||
case 'lost':
|
||||
case 'failure':
|
||||
$bar_inner.addClass('progress-bar-danger');
|
||||
break;
|
||||
case 'unstable':
|
||||
$bar_inner.addClass('progress-bar-warning');
|
||||
break;
|
||||
case 'in progress':
|
||||
case 'queued':
|
||||
break;
|
||||
}
|
||||
$bar_inner.attr('title', job.name)
|
||||
.css('width', job_percent + '%');
|
||||
$bar_outter.append($bar_inner);
|
||||
}
|
||||
});
|
||||
return $bar_outter;
|
||||
},
|
||||
|
||||
change_header: function(change) {
|
||||
var change_id = change.id || 'NA';
|
||||
|
||||
var $change_link = $('<small />');
|
||||
if (change.url !== null) {
|
||||
var github_id = change_id.match(/^([0-9]+),([0-9a-f]{40})$/);
|
||||
if (github_id) {
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', change_id)
|
||||
.text('#' + github_id[1])
|
||||
)
|
||||
);
|
||||
} else if (/^[0-9a-f]{40}$/.test(change_id)) {
|
||||
var change_id_short = change_id.slice(0, 7);
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', change_id)
|
||||
.text(change_id_short)
|
||||
)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$change_link.append(
|
||||
$('<a />').attr('href', change.url).text(change_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (change_id.length === 40) {
|
||||
change_id = change_id.substr(0, 7);
|
||||
}
|
||||
$change_link.text(change_id);
|
||||
}
|
||||
|
||||
var $change_progress_row_left = $('<div />')
|
||||
.addClass('col-xs-4')
|
||||
.append($change_link);
|
||||
var $change_progress_row_right = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append(this.change_total_progress_bar(change));
|
||||
|
||||
var $change_progress_row = $('<div />')
|
||||
.addClass('row')
|
||||
.append($change_progress_row_left)
|
||||
.append($change_progress_row_right);
|
||||
|
||||
var $project_span = $('<span />')
|
||||
.addClass('change_project')
|
||||
.text(change.project);
|
||||
|
||||
var $left = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append($project_span, $change_progress_row);
|
||||
|
||||
var remaining_time = this.time(
|
||||
change.remaining_time, true);
|
||||
var enqueue_time = this.enqueue_time(
|
||||
change.enqueue_time);
|
||||
var $remaining_time = $('<small />').addClass('time')
|
||||
.attr('title', 'Remaining Time').html(remaining_time);
|
||||
var $enqueue_time = $('<small />').addClass('time')
|
||||
.attr('title', 'Elapsed Time').html(enqueue_time);
|
||||
|
||||
var $right = $('<div />');
|
||||
if (change.live === true) {
|
||||
$right.addClass('col-xs-4 text-right')
|
||||
.append($remaining_time, $('<br />'), $enqueue_time);
|
||||
}
|
||||
|
||||
var $header = $('<div />')
|
||||
.addClass('row')
|
||||
.append($left, $right);
|
||||
return $header;
|
||||
},
|
||||
|
||||
change_list: function(jobs) {
|
||||
var format = this;
|
||||
var $list = $('<ul />')
|
||||
.addClass('list-group zuul-patchset-body');
|
||||
|
||||
$.each(jobs, function (i, job) {
|
||||
var $item = $('<li />')
|
||||
.addClass('list-group-item')
|
||||
.addClass('zuul-change-job')
|
||||
.append(format.job(job));
|
||||
$list.append($item);
|
||||
});
|
||||
|
||||
return $list;
|
||||
},
|
||||
|
||||
change_panel: function (change) {
|
||||
var $header = $('<div />')
|
||||
.addClass('panel-heading zuul-patchset-header')
|
||||
.append(this.change_header(change));
|
||||
|
||||
var panel_id = change.id ? change.id.replace(',', '_')
|
||||
: change.project.replace('/', '_') +
|
||||
'-' + change.enqueue_time;
|
||||
var $panel = $('<div />')
|
||||
.attr('id', panel_id)
|
||||
.addClass('panel panel-default zuul-change')
|
||||
.append($header)
|
||||
.append(this.change_list(change.jobs));
|
||||
|
||||
$header.click(this.toggle_patchset);
|
||||
return $panel;
|
||||
},
|
||||
|
||||
change_status_icon: function(change) {
|
||||
var icon_name = 'green.png';
|
||||
var icon_title = 'Succeeding';
|
||||
|
||||
if (change.active !== true) {
|
||||
// Grey icon
|
||||
icon_name = 'grey.png';
|
||||
icon_title = 'Waiting until closer to head of queue to' +
|
||||
' start jobs';
|
||||
}
|
||||
else if (change.live !== true) {
|
||||
// Grey icon
|
||||
icon_name = 'grey.png';
|
||||
icon_title = 'Dependent change required for testing';
|
||||
}
|
||||
else if (change.failing_reasons &&
|
||||
change.failing_reasons.length > 0) {
|
||||
var reason = change.failing_reasons.join(', ');
|
||||
icon_title = 'Failing because ' + reason;
|
||||
if (reason.match(/merge conflict/)) {
|
||||
// Black icon
|
||||
icon_name = 'black.png';
|
||||
}
|
||||
else {
|
||||
// Red icon
|
||||
icon_name = 'red.png';
|
||||
}
|
||||
}
|
||||
|
||||
var $icon = $('<img />')
|
||||
.attr('src', '../static/images/' + icon_name)
|
||||
.attr('title', icon_title)
|
||||
.css('margin-top', '-6px');
|
||||
|
||||
return $icon;
|
||||
},
|
||||
|
||||
change_with_status_tree: function(change, change_queue) {
|
||||
var $change_row = $('<tr />');
|
||||
|
||||
for (var i = 0; i < change_queue._tree_columns; i++) {
|
||||
var $tree_cell = $('<td />')
|
||||
.css('height', '100%')
|
||||
.css('padding', '0 0 10px 0')
|
||||
.css('margin', '0')
|
||||
.css('width', '16px')
|
||||
.css('min-width', '16px')
|
||||
.css('overflow', 'hidden')
|
||||
.css('vertical-align', 'top');
|
||||
|
||||
if (i < change._tree.length && change._tree[i] !== null) {
|
||||
$tree_cell.css('background-image',
|
||||
'url(\'../static/images/line.png\')')
|
||||
.css('background-repeat', 'repeat-y');
|
||||
}
|
||||
|
||||
if (i === change._tree_index) {
|
||||
$tree_cell.append(
|
||||
this.change_status_icon(change));
|
||||
}
|
||||
if (change._tree_branches.indexOf(i) !== -1) {
|
||||
var $image = $('<img />')
|
||||
.css('vertical-align', 'baseline');
|
||||
if (change._tree_branches.indexOf(i) ===
|
||||
change._tree_branches.length - 1) {
|
||||
// Angle line
|
||||
$image.attr('src', '../static/images/line-angle.png');
|
||||
}
|
||||
else {
|
||||
// T line
|
||||
$image.attr('src', '../static/images/line-t.png');
|
||||
}
|
||||
$tree_cell.append($image);
|
||||
}
|
||||
$change_row.append($tree_cell);
|
||||
}
|
||||
|
||||
var change_width = 360 - 16*change_queue._tree_columns;
|
||||
var $change_column = $('<td />')
|
||||
.css('width', change_width + 'px')
|
||||
.addClass('zuul-change-cell')
|
||||
.append(this.change_panel(change));
|
||||
|
||||
$change_row.append($change_column);
|
||||
|
||||
var $change_table = $('<table />')
|
||||
.addClass('zuul-change-box')
|
||||
.css('-moz-box-sizing', 'content-box')
|
||||
.css('box-sizing', 'content-box')
|
||||
.append($change_row);
|
||||
|
||||
return $change_table;
|
||||
},
|
||||
|
||||
pipeline_sparkline: function(pipeline_name) {
|
||||
if (options.graphite_url !== '') {
|
||||
var $sparkline = $('<img />')
|
||||
.addClass('pull-right')
|
||||
.attr('src', get_sparkline_url(pipeline_name));
|
||||
return $sparkline;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
pipeline_header: function(pipeline, count) {
|
||||
// Format the pipeline name, sparkline and description
|
||||
var $header_div = $('<div />')
|
||||
.addClass('zuul-pipeline-header');
|
||||
|
||||
var $heading = $('<h3 />')
|
||||
.css('vertical-align', 'middle')
|
||||
.text(pipeline.name)
|
||||
.append(
|
||||
$('<span />')
|
||||
.addClass('badge pull-right')
|
||||
.css('vertical-align', 'middle')
|
||||
.css('margin-top', '0.5em')
|
||||
.text(count)
|
||||
)
|
||||
.append(this.pipeline_sparkline(pipeline.name));
|
||||
|
||||
$header_div.append($heading);
|
||||
|
||||
if (typeof pipeline.description === 'string') {
|
||||
var descr = $('<small />')
|
||||
$.each( pipeline.description.split(/\r?\n\r?\n/), function(index, descr_part){
|
||||
descr.append($('<p />').text(descr_part));
|
||||
});
|
||||
$header_div.append(
|
||||
$('<p />').append(descr)
|
||||
);
|
||||
}
|
||||
return $header_div;
|
||||
},
|
||||
|
||||
pipeline: function (pipeline, count) {
|
||||
var format = this;
|
||||
var $html = $('<div />')
|
||||
.addClass('zuul-pipeline col-md-4')
|
||||
.append(this.pipeline_header(pipeline, count));
|
||||
|
||||
$.each(pipeline.change_queues,
|
||||
function (queue_i, change_queue) {
|
||||
$.each(change_queue.heads, function (head_i, changes) {
|
||||
if (pipeline.change_queues.length > 1 &&
|
||||
head_i === 0) {
|
||||
var name = change_queue.name;
|
||||
var short_name = name;
|
||||
if (short_name.length > 32) {
|
||||
short_name = short_name.substr(0, 32) + '...';
|
||||
}
|
||||
$html.append(
|
||||
$('<p />')
|
||||
.text('Queue: ')
|
||||
.append(
|
||||
$('<abbr />')
|
||||
.attr('title', name)
|
||||
.text(short_name)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$.each(changes, function (change_i, change) {
|
||||
var $change_box =
|
||||
format.change_with_status_tree(
|
||||
change, change_queue);
|
||||
$html.append($change_box);
|
||||
format.display_patchset($change_box);
|
||||
});
|
||||
});
|
||||
});
|
||||
return $html;
|
||||
},
|
||||
|
||||
toggle_patchset: function(e) {
|
||||
// Toggle showing/hiding the patchset when the header is
|
||||
// clicked.
|
||||
|
||||
if (e.target.nodeName.toLowerCase() === 'a') {
|
||||
// Ignore clicks from gerrit patch set link
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab the patchset panel
|
||||
var $panel = $(e.target).parents('.zuul-change');
|
||||
var $body = $panel.children('.zuul-patchset-body');
|
||||
$body.toggle(200);
|
||||
var collapsed_index = collapsed_exceptions.indexOf(
|
||||
$panel.attr('id'));
|
||||
if (collapsed_index === -1 ) {
|
||||
// Currently not an exception, add it to list
|
||||
collapsed_exceptions.push($panel.attr('id'));
|
||||
}
|
||||
else {
|
||||
// Currently an except, remove from exceptions
|
||||
collapsed_exceptions.splice(collapsed_index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
display_patchset: function($change_box, animate) {
|
||||
// Determine if to show or hide the patchset and/or the results
|
||||
// when loaded
|
||||
|
||||
// See if we should hide the body/results
|
||||
var $panel = $change_box.find('.zuul-change');
|
||||
var panel_change = $panel.attr('id');
|
||||
var $body = $panel.children('.zuul-patchset-body');
|
||||
var expand_by_default = $('#expand_by_default')
|
||||
.prop('checked');
|
||||
|
||||
var collapsed_index = collapsed_exceptions
|
||||
.indexOf(panel_change);
|
||||
|
||||
if (expand_by_default && collapsed_index === -1 ||
|
||||
!expand_by_default && collapsed_index !== -1) {
|
||||
// Expand by default, or is an exception
|
||||
$body.show(animate);
|
||||
}
|
||||
else {
|
||||
$body.hide(animate);
|
||||
}
|
||||
|
||||
// Check if we should hide the whole panel
|
||||
var panel_project = $panel.find('.change_project').text()
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
var panel_pipeline = $change_box
|
||||
.parents('.zuul-pipeline')
|
||||
.find('.zuul-pipeline-header > h3')
|
||||
.html()
|
||||
.toLowerCase();
|
||||
|
||||
if (current_filter !== '') {
|
||||
var show_panel = false;
|
||||
var filter = current_filter.trim().split(/[\s,]+/);
|
||||
$.each(filter, function(index, f_val) {
|
||||
if (f_val !== '') {
|
||||
f_val = f_val.toLowerCase();
|
||||
if (panel_project.indexOf(f_val) !== -1 ||
|
||||
panel_pipeline.indexOf(f_val) !== -1 ||
|
||||
panel_change.indexOf(f_val) !== -1) {
|
||||
show_panel = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (show_panel === true) {
|
||||
$change_box.show(animate);
|
||||
}
|
||||
else {
|
||||
$change_box.hide(animate);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$change_box.show(animate);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var app = {
|
||||
schedule: function (app) {
|
||||
app = app || this;
|
||||
if (!options.enabled) {
|
||||
setTimeout(function() {app.schedule(app);}, 5000);
|
||||
return;
|
||||
}
|
||||
app.update().always(function () {
|
||||
setTimeout(function() {app.schedule(app);}, 5000);
|
||||
});
|
||||
|
||||
/* Only update graphs every minute */
|
||||
if (zuul_graph_update_count > 11) {
|
||||
zuul_graph_update_count = 0;
|
||||
zuul.update_sparklines();
|
||||
}
|
||||
},
|
||||
|
||||
/** @return {jQuery.Promise} */
|
||||
update: function () {
|
||||
// Cancel the previous update if it hasn't completed yet.
|
||||
if (xhr) {
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
this.emit('update-start');
|
||||
var app = this;
|
||||
|
||||
var $msg = $(options.msg_id);
|
||||
xhr = $.getJSON(options.source)
|
||||
.done(function (data) {
|
||||
if ('message' in data) {
|
||||
$msg.removeClass('alert-danger')
|
||||
.addClass('alert-info')
|
||||
.text(data.message)
|
||||
.show();
|
||||
} else {
|
||||
$msg.empty()
|
||||
.hide();
|
||||
}
|
||||
|
||||
if ('zuul_version' in data) {
|
||||
$('#zuul-version-span').text(data.zuul_version);
|
||||
}
|
||||
if ('last_reconfigured' in data) {
|
||||
var last_reconfigured =
|
||||
new Date(data.last_reconfigured);
|
||||
$('#last-reconfigured-span').text(
|
||||
last_reconfigured.toString());
|
||||
}
|
||||
|
||||
var $pipelines = $(options.pipelines_id);
|
||||
$pipelines.html('');
|
||||
$.each(data.pipelines, function (i, pipeline) {
|
||||
var count = app.create_tree(pipeline);
|
||||
$pipelines.append(
|
||||
format.pipeline(pipeline, count));
|
||||
});
|
||||
|
||||
$(options.queue_events_num).text(
|
||||
data.trigger_event_queue ?
|
||||
data.trigger_event_queue.length : '0'
|
||||
);
|
||||
$(options.queue_management_events_num).text(
|
||||
data.management_event_queue ?
|
||||
data.management_event_queue.length : '0'
|
||||
);
|
||||
$(options.queue_results_num).text(
|
||||
data.result_event_queue ?
|
||||
data.result_event_queue.length : '0'
|
||||
);
|
||||
})
|
||||
.fail(function (jqXHR, statusText, errMsg) {
|
||||
if (statusText === 'abort') {
|
||||
return;
|
||||
}
|
||||
$msg.text(options.source + ': ' + errMsg)
|
||||
.addClass('alert-danger')
|
||||
.removeClass('zuul-msg-wrap-off')
|
||||
.show();
|
||||
})
|
||||
.always(function () {
|
||||
xhr = undefined;
|
||||
app.emit('update-end');
|
||||
});
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
update_sparklines: function() {
|
||||
$.each(zuul_sparkline_urls, function(name, url) {
|
||||
var newimg = new Image();
|
||||
var parts = url.split('#');
|
||||
newimg.src = parts[0] + '#' + new Date().getTime();
|
||||
$(newimg).load(function () {
|
||||
zuul_sparkline_urls[name] = newimg.src;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
control_form: function() {
|
||||
// Build the filter form filling anything from cookies
|
||||
|
||||
var $control_form = $('<form />')
|
||||
.attr('role', 'form')
|
||||
.addClass('form-inline')
|
||||
.submit(this.handle_filter_change);
|
||||
|
||||
$control_form
|
||||
.append(this.filter_form_group())
|
||||
.append(this.expand_form_group());
|
||||
|
||||
return $control_form;
|
||||
},
|
||||
|
||||
filter_form_group: function() {
|
||||
// Update the filter form with a clear button if required
|
||||
|
||||
var $label = $('<label />')
|
||||
.addClass('control-label')
|
||||
.attr('for', 'filter_string')
|
||||
.text('Filters')
|
||||
.css('padding-right', '0.5em');
|
||||
|
||||
var $input = $('<input />')
|
||||
.attr('type', 'text')
|
||||
.attr('id', 'filter_string')
|
||||
.addClass('form-control')
|
||||
.attr('title',
|
||||
'project(s), pipeline(s) or review(s) comma ' +
|
||||
'separated')
|
||||
.attr('value', current_filter);
|
||||
|
||||
$input.change(this.handle_filter_change);
|
||||
|
||||
var $clear_icon = $('<span />')
|
||||
.addClass('form-control-feedback')
|
||||
.addClass('glyphicon glyphicon-remove-circle')
|
||||
.attr('id', 'filter_form_clear_box')
|
||||
.attr('title', 'clear filter')
|
||||
.css('cursor', 'pointer');
|
||||
|
||||
$clear_icon.click(function() {
|
||||
$('#filter_string').val('').change();
|
||||
});
|
||||
|
||||
if (current_filter === '') {
|
||||
$clear_icon.hide();
|
||||
}
|
||||
|
||||
var $form_group = $('<div />')
|
||||
.addClass('form-group has-feedback')
|
||||
.append($label, $input, $clear_icon);
|
||||
return $form_group;
|
||||
},
|
||||
|
||||
expand_form_group: function() {
|
||||
var expand_by_default = (
|
||||
read_cookie('zuul_expand_by_default', false) === 'true');
|
||||
|
||||
var $checkbox = $('<input />')
|
||||
.attr('type', 'checkbox')
|
||||
.attr('id', 'expand_by_default')
|
||||
.prop('checked', expand_by_default)
|
||||
.change(this.handle_expand_by_default);
|
||||
|
||||
var $label = $('<label />')
|
||||
.css('padding-left', '1em')
|
||||
.html('Expand by default: ')
|
||||
.append($checkbox);
|
||||
|
||||
var $form_group = $('<div />')
|
||||
.addClass('checkbox')
|
||||
.append($label);
|
||||
return $form_group;
|
||||
},
|
||||
|
||||
handle_filter_change: function() {
|
||||
// Update the filter and save it to a cookie
|
||||
current_filter = $('#filter_string').val();
|
||||
set_cookie('zuul_filter_string', current_filter);
|
||||
if (current_filter === '') {
|
||||
$('#filter_form_clear_box').hide();
|
||||
}
|
||||
else {
|
||||
$('#filter_form_clear_box').show();
|
||||
}
|
||||
|
||||
$('.zuul-change-box').each(function(index, obj) {
|
||||
var $change_box = $(obj);
|
||||
format.display_patchset($change_box, 200);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
handle_expand_by_default: function(e) {
|
||||
// Handle toggling expand by default
|
||||
set_cookie('zuul_expand_by_default', e.target.checked);
|
||||
collapsed_exceptions = [];
|
||||
$('.zuul-change-box').each(function(index, obj) {
|
||||
var $change_box = $(obj);
|
||||
format.display_patchset($change_box, 200);
|
||||
});
|
||||
},
|
||||
|
||||
create_tree: function(pipeline) {
|
||||
var count = 0;
|
||||
var pipeline_max_tree_columns = 1;
|
||||
$.each(pipeline.change_queues, function(change_queue_i,
|
||||
change_queue) {
|
||||
var tree = [];
|
||||
var max_tree_columns = 1;
|
||||
var changes = [];
|
||||
var last_tree_length = 0;
|
||||
$.each(change_queue.heads, function(head_i, head) {
|
||||
$.each(head, function(change_i, change) {
|
||||
changes[change.id] = change;
|
||||
change._tree_position = change_i;
|
||||
});
|
||||
});
|
||||
$.each(change_queue.heads, function(head_i, head) {
|
||||
$.each(head, function(change_i, change) {
|
||||
if (change.live === true) {
|
||||
count += 1;
|
||||
}
|
||||
var idx = tree.indexOf(change.id);
|
||||
if (idx > -1) {
|
||||
change._tree_index = idx;
|
||||
// remove...
|
||||
tree[idx] = null;
|
||||
while (tree[tree.length - 1] === null) {
|
||||
tree.pop();
|
||||
}
|
||||
} else {
|
||||
change._tree_index = 0;
|
||||
}
|
||||
change._tree_branches = [];
|
||||
change._tree = [];
|
||||
if (typeof(change.items_behind) === 'undefined') {
|
||||
change.items_behind = [];
|
||||
}
|
||||
change.items_behind.sort(function(a, b) {
|
||||
return (changes[b]._tree_position -
|
||||
changes[a]._tree_position);
|
||||
});
|
||||
$.each(change.items_behind, function(i, id) {
|
||||
tree.push(id);
|
||||
if (tree.length>last_tree_length &&
|
||||
last_tree_length > 0) {
|
||||
change._tree_branches.push(
|
||||
tree.length - 1);
|
||||
}
|
||||
});
|
||||
if (tree.length > max_tree_columns) {
|
||||
max_tree_columns = tree.length;
|
||||
}
|
||||
if (tree.length > pipeline_max_tree_columns) {
|
||||
pipeline_max_tree_columns = tree.length;
|
||||
}
|
||||
change._tree = tree.slice(0); // make a copy
|
||||
last_tree_length = tree.length;
|
||||
});
|
||||
});
|
||||
change_queue._tree_columns = max_tree_columns;
|
||||
});
|
||||
pipeline._tree_columns = pipeline_max_tree_columns;
|
||||
return count;
|
||||
},
|
||||
};
|
||||
|
||||
$jq = $(app);
|
||||
return {
|
||||
options: options,
|
||||
format: format,
|
||||
app: app,
|
||||
jq: $jq
|
||||
};
|
||||
};
|
||||
}(jQuery));
|
|
@ -1,99 +0,0 @@
|
|||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
angular.module('zuulTenants', []).controller(
|
||||
'mainController', function($scope, $http)
|
||||
{
|
||||
$scope.tenants = undefined;
|
||||
$scope.tenants_fetch = function() {
|
||||
$http.get("tenants")
|
||||
.then(function success(result) {
|
||||
$scope.tenants = result.data;
|
||||
});
|
||||
}
|
||||
$scope.tenants_fetch();
|
||||
});
|
||||
|
||||
angular.module('zuulJobs', []).controller(
|
||||
'mainController', function($scope, $http)
|
||||
{
|
||||
$scope.jobs = undefined;
|
||||
$scope.jobs_fetch = function() {
|
||||
$http.get("jobs")
|
||||
.then(function success(result) {
|
||||
$scope.jobs = result.data;
|
||||
});
|
||||
}
|
||||
$scope.jobs_fetch();
|
||||
});
|
||||
|
||||
angular.module('zuulBuilds', [], function($locationProvider) {
|
||||
$locationProvider.html5Mode({
|
||||
enabled: true,
|
||||
requireBase: false
|
||||
});
|
||||
}).controller('mainController', function($scope, $http, $location)
|
||||
{
|
||||
$scope.rowClass = function(build) {
|
||||
if (build.result == "SUCCESS") {
|
||||
return "success";
|
||||
} else {
|
||||
return "warning";
|
||||
}
|
||||
};
|
||||
var query_args = $location.search();
|
||||
var url = $location.url();
|
||||
var tenant_start = url.lastIndexOf(
|
||||
'/', url.lastIndexOf('/builds.html') - 1) + 1;
|
||||
var tenant_length = url.lastIndexOf('/builds.html') - tenant_start;
|
||||
$scope.tenant = url.substr(tenant_start, tenant_length);
|
||||
$scope.builds = undefined;
|
||||
if (query_args["pipeline"]) {$scope.pipeline = query_args["pipeline"];
|
||||
} else {$scope.pipeline = "";}
|
||||
if (query_args["job_name"]) {$scope.job_name = query_args["job_name"];
|
||||
} else {$scope.job_name = "";}
|
||||
if (query_args["project"]) {$scope.project = query_args["project"];
|
||||
} else {$scope.project = "";}
|
||||
$scope.builds_fetch = function() {
|
||||
query_string = "";
|
||||
if ($scope.tenant) {query_string += "&tenant="+$scope.tenant;}
|
||||
if ($scope.pipeline) {query_string += "&pipeline="+$scope.pipeline;}
|
||||
if ($scope.job_name) {query_string += "&job_name="+$scope.job_name;}
|
||||
if ($scope.project) {query_string += "&project="+$scope.project;}
|
||||
if (query_string != "") {query_string = "?" + query_string.substr(1);}
|
||||
$http.get("builds" + query_string)
|
||||
.then(function success(result) {
|
||||
for (build_pos = 0;
|
||||
build_pos < result.data.length;
|
||||
build_pos += 1) {
|
||||
build = result.data[build_pos]
|
||||
if (build.node_name == null) {
|
||||
build.node_name = 'master'
|
||||
}
|
||||
/* Fix incorect url for post_failure job */
|
||||
if (build.log_url == build.job_name) {
|
||||
build.log_url = undefined;
|
||||
}
|
||||
}
|
||||
$scope.builds = result.data;
|
||||
});
|
||||
}
|
||||
$scope.builds_fetch()
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
// Client script for Zuul status page
|
||||
//
|
||||
// @licstart The following is the entire license notice for the
|
||||
// JavaScript code in this page.
|
||||
//
|
||||
// Copyright 2013 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// @licend The above is the entire license notice
|
||||
// for the JavaScript code in this page.
|
||||
|
||||
/*exported zuul_build_dom, zuul_start */
|
||||
|
||||
function zuul_build_dom($, container) {
|
||||
// Build a default-looking DOM
|
||||
var default_layout = '<div class="container">'
|
||||
+ '<div class="zuul-container" id="zuul-container">'
|
||||
+ '<div style="display: none;" class="alert" id="zuul_msg"></div>'
|
||||
+ '<button class="btn pull-right zuul-spinner">updating <span class="glyphicon glyphicon-refresh"></span></button>'
|
||||
+ '<p>Queue lengths: <span id="zuul_queue_events_num">0</span> events, <span id="zuul_queue_management_events_num">0</span> management events, <span id="zuul_queue_results_num">0</span> results.</p>'
|
||||
+ '<div id="zuul_controls"></div>'
|
||||
+ '<div id="zuul_pipelines" class="row"></div>'
|
||||
+ '<p>Zuul version: <span id="zuul-version-span"></span></p>'
|
||||
+ '<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>'
|
||||
+ '</div></div>';
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
var $container = $(container);
|
||||
$container.html(default_layout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The $.zuul instance
|
||||
*/
|
||||
function zuul_start($) {
|
||||
// Start the zuul app (expects default dom)
|
||||
|
||||
var $container, $indicator;
|
||||
var demo = location.search.match(/[?&]demo=([^?&]*)/),
|
||||
source_url = location.search.match(/[?&]source_url=([^?&]*)/),
|
||||
source = demo ? './status-' + (demo[1] || 'basic') + '.json-sample' :
|
||||
'status';
|
||||
source = source_url ? source_url[1] : source;
|
||||
|
||||
var zuul = $.zuul({
|
||||
source: source,
|
||||
//graphite_url: 'http://graphite.openstack.org/render/'
|
||||
});
|
||||
|
||||
zuul.jq.on('update-start', function () {
|
||||
$container.addClass('zuul-container-loading');
|
||||
$indicator.addClass('zuul-spinner-on');
|
||||
});
|
||||
|
||||
zuul.jq.on('update-end', function () {
|
||||
$container.removeClass('zuul-container-loading');
|
||||
setTimeout(function () {
|
||||
$indicator.removeClass('zuul-spinner-on');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
zuul.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 () {
|
||||
// Fade in the content
|
||||
$container.addClass('zuul-container-ready');
|
||||
});
|
||||
});
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
$container = $('#zuul-container');
|
||||
$indicator = $('#zuul-spinner');
|
||||
$('#zuul_controls').append(zuul.app.control_form());
|
||||
|
||||
zuul.app.schedule();
|
||||
|
||||
$(document).on({
|
||||
'show.visibility': function () {
|
||||
zuul.options.enabled = true;
|
||||
zuul.app.update();
|
||||
},
|
||||
'hide.visibility': function () {
|
||||
zuul.options.enabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return zuul;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<!--
|
||||
Copyright 2013 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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Zuul Status</title>
|
||||
<link rel="stylesheet" href="../static/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="../static/styles/zuul.css" />
|
||||
<script src="../static/js/jquery.min.js"></script>
|
||||
<script src="../static/js/jquery-visibility.min.js"></script>
|
||||
<script src="../static/js/jquery.graphite.min.js"></script>
|
||||
<script src="../static/javascripts/jquery.zuul.js"></script>
|
||||
<script src="../static/javascripts/zuul.app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="../" target="_self">Zuul Dashboard</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="status.html" target="_self">Status</a></li>
|
||||
<li><a href="jobs.html" target="_self">Jobs</a></li>
|
||||
<li><a href="builds.html" target="_self">Builds</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="zuul_container"></div>
|
||||
<script>
|
||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache 2.0
|
||||
zuul_build_dom(jQuery, '#zuul_container');
|
||||
zuul_start(jQuery);
|
||||
// @license-end
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,114 +0,0 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
|
||||
body {
|
||||
font-family: monospace;
|
||||
background-color: black;
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: darkgrey;
|
||||
color: black;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function escapeLog(text) {
|
||||
var pattern = /[<>&"']/g;
|
||||
|
||||
return text.replace(pattern, function(match) {
|
||||
return '&#' + match.charCodeAt(0) + ';';
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
pageUpdateInMS = 250;
|
||||
var receiveBuffer = "";
|
||||
var websocket_url = null
|
||||
|
||||
setInterval(function() {
|
||||
console.log("autoScroll");
|
||||
if (receiveBuffer != "") {
|
||||
document.getElementById('pagecontent').innerHTML += receiveBuffer;
|
||||
receiveBuffer = "";
|
||||
if (document.getElementById('autoscroll').checked) {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
}, pageUpdateInMS);
|
||||
|
||||
var url = new URL(window.location);
|
||||
|
||||
var params = {
|
||||
uuid: url.searchParams.get('uuid')
|
||||
}
|
||||
document.getElementById('pagetitle').innerHTML = params['uuid'];
|
||||
if (url.searchParams.has('logfile')) {
|
||||
params['logfile'] = url.searchParams.get('logfile');
|
||||
var logfile_suffix = "(" + params['logfile'] + ")";
|
||||
document.getElementById('pagetitle').innerHTML += logfile_suffix;
|
||||
}
|
||||
if (url.searchParams.has('websocket_url')) {
|
||||
params['websocket_url'] = url.searchParams.get('websocket_url');
|
||||
} else {
|
||||
// Websocket doesn't accept relative urls so construct an
|
||||
// absolute one.
|
||||
var protocol = '';
|
||||
if (url['protocol'] == 'https:') {
|
||||
protocol = 'wss://';
|
||||
} else {
|
||||
protocol = 'ws://';
|
||||
}
|
||||
path = url['pathname'].replace(/stream.html.*$/g, '') + 'console-stream';
|
||||
params['websocket_url'] = protocol + url['host'] + path;
|
||||
}
|
||||
var ws = new WebSocket(params['websocket_url']);
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
console.log("onmessage");
|
||||
receiveBuffer = receiveBuffer + escapeLog(event.data);
|
||||
};
|
||||
|
||||
ws.onopen = function(event) {
|
||||
console.log("onopen");
|
||||
ws.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log("onclose");
|
||||
receiveBuffer = receiveBuffer + "\n--- END OF STREAM ---\n";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<title id="pagetitle"></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="overlay">
|
||||
<form>
|
||||
<input type="checkbox" id="autoscroll" checked> autoscroll
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<pre id="pagecontent"></pre>
|
||||
|
||||
</body>
|
||||
</html>
|