diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..6df7c0c926 --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ['env'], + "plugins": [[ + "angularjs-annotate", { + "explicitOnly": false + } + ]] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..136e54daba --- /dev/null +++ b/.eslintrc @@ -0,0 +1,10 @@ +parser: babel-eslint +plugins: + - standard +rules: + camelcase: off + indent: + - off + - 2 +extends: + - ./node_modules/eslint-config-standard/eslintrc.json diff --git a/.gitignore b/.gitignore index d7e2fac657..82b3898933 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ zuul/versioninfo dist/ cover/ htmlcov/ +zuul/web/static +node_modules +yarn-error.log diff --git a/.zuul.yaml b/.zuul.yaml index 8b5ccb9253..0ddfa2552d 100644 --- a/.zuul.yaml +++ b/.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 diff --git a/MANIFEST.in b/MANIFEST.in index 74fc557861..429cf108e8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include AUTHORS include ChangeLog +include zuul/web/static/* exclude .gitignore exclude .gitreview diff --git a/TESTING.rst b/TESTING.rst index 0786ebf408..289af5c2bc 100644 --- a/TESTING.rst +++ b/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 ------------- diff --git a/doc/source/admin/components.rst b/doc/source/admin/components.rst index b555abc884..acf0aad6d4 100644 --- a/doc/source/admin/components.rst +++ b/doc/source/admin/components.rst @@ -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 diff --git a/doc/source/developer/index.rst b/doc/source/developer/index.rst index 360dcd5e33..69e9499ac1 100644 --- a/doc/source/developer/index.rst +++ b/doc/source/developer/index.rst @@ -16,3 +16,4 @@ Zuul, though advanced users may find it interesting. testing docs ansible + javascript diff --git a/doc/source/developer/javascript.rst b/doc/source/developer/javascript.rst new file mode 100644 index 0000000000..9eca55ec42 --- /dev/null +++ b/doc/source/developer/javascript.rst @@ -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 diff --git a/etc/status/README.rst b/etc/status/README.rst deleted file mode 100644 index 762b49ce42..0000000000 --- a/etc/status/README.rst +++ /dev/null @@ -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 -``
``. - -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. diff --git a/etc/status/fetch-dependencies.sh b/etc/status/fetch-dependencies.sh deleted file mode 100755 index ccaf74ca8c..0000000000 --- a/etc/status/fetch-dependencies.sh +++ /dev/null @@ -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 diff --git a/etc/status/public_html/index.html b/etc/status/public_html/index.html deleted file mode 100644 index cc3d40a00f..0000000000 --- a/etc/status/public_html/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Zuul Status - - - - -
- - - - - - - - diff --git a/etc/status/public_html/jquery.zuul.js b/etc/status/public_html/jquery.zuul.js deleted file mode 100644 index ac8a302817..0000000000 --- a/etc/status/public_html/jquery.zuul.js +++ /dev/null @@ -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 = $(''); - - if (job.result !== null) { - $job_line.append( - $('') - .addClass('zuul-job-name') - .attr('href', job.report_url) - .text(job.name) - ); - } - else if (job.url !== null) { - $job_line.append( - $('') - .addClass('zuul-job-name') - .attr('href', job.url) - .text(job.name) - ); - } - else { - $job_line.append( - $('') - .addClass('zuul-job-name') - .text(job.name) - ); - } - - $job_line.append(this.job_status(job)); - - if (job.voting === false) { - $job_line.append( - $(' ') - .addClass('zuul-non-voting-desc') - .text(' (non-voting)') - ); - } - - $job_line.append($('
')); - 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 = $(''); - $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 = $('
') - .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 = $('
') - .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 '' + text + ''; - }, - - 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 = $('
') - .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 = $('
') - .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 = $(''); - if (change.url !== null) { - var github_id = change_id.match(/^([0-9]+),([0-9a-f]{40})$/); - if (github_id) { - $change_link.append( - $('').attr('href', change.url).append( - $('') - .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( - $('').attr('href', change.url).append( - $('') - .attr('title', change_id) - .text(change_id_short) - ) - ); - } - else { - $change_link.append( - $('').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 = $('
') - .addClass('col-xs-4') - .append($change_link); - var $change_progress_row_right = $('
') - .addClass('col-xs-8') - .append(this.change_total_progress_bar(change)); - - var $change_progress_row = $('
') - .addClass('row') - .append($change_progress_row_left) - .append($change_progress_row_right); - - var $project_span = $('') - .addClass('change_project') - .text(change.project); - - var $left = $('
') - .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 = $('').addClass('time') - .attr('title', 'Remaining Time').html(remaining_time); - var $enqueue_time = $('').addClass('time') - .attr('title', 'Elapsed Time').html(enqueue_time); - - var $right = $('
'); - if (change.live === true) { - $right.addClass('col-xs-4 text-right') - .append($remaining_time, $('
'), $enqueue_time); - } - - var $header = $('
') - .addClass('row') - .append($left, $right); - return $header; - }, - - change_list: function(jobs) { - var format = this; - var $list = $('
    ') - .addClass('list-group zuul-patchset-body'); - - $.each(jobs, function (i, job) { - var $item = $('
  • ') - .addClass('list-group-item') - .addClass('zuul-change-job') - .append(format.job(job)); - $list.append($item); - }); - - return $list; - }, - - change_panel: function (change) { - var $header = $('
    ') - .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 = $('
    ') - .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 = $('') - .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 = $(''); - - for (var i = 0; i < change_queue._tree_columns; i++) { - var $tree_cell = $('') - .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 = $('') - .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 = $('') - .css('width', change_width + 'px') - .addClass('zuul-change-cell') - .append(this.change_panel(change)); - - $change_row.append($change_column); - - var $change_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 = $('') - .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 = $('
    ') - .addClass('zuul-pipeline-header'); - - var $heading = $('

    ') - .css('vertical-align', 'middle') - .text(pipeline.name) - .append( - $('') - .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 = $('') - $.each( pipeline.description.split(/\r?\n\r?\n/), function(index, descr_part){ - descr.append($('

    ').text(descr_part)); - }); - $header_div.append( - $('

    ').append(descr) - ); - } - return $header_div; - }, - - pipeline: function (pipeline, count) { - var format = this; - var $html = $('

    ') - .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( - $('

    ') - .text('Queue: ') - .append( - $('') - .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 = $('
    ') - .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 = $('

    ') - .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 = $('') - .attr('type', 'checkbox') - .attr('id', 'expand_by_default') - .prop('checked', expand_by_default) - .change(this.handle_expand_by_default); - - var $label = $('

    ') + + for (let i = 0; i < changeQueue._tree_columns; i++) { + let $treeCell = $('
    ') + .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 = $('') + .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 = $('') + .css('width', changeWidth + 'px') + .addClass('zuul-change-cell') + .append(this.changePanel(change)) + + $changeRow.append($changeColumn) + + let $changeTable = $('') + .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 = $('') + .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 = $('
    ') + .addClass('zuul-pipeline-header') + + let $heading = $('

    ') + .css('vertical-align', 'middle') + .text(pipeline.name) + .append( + $('') + .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 = $('') + $.each(pipeline.description.split(/\r?\n\r?\n/), + function (index, descrPart) { + descr.append($('

    ').text(descrPart)) + }) + $headerDiv.append($('

    ').append(descr)) + } + return $headerDiv + }, + + pipeline: function (pipeline, count) { + let format = this + let $html = $('

    ') + .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($('

    ') + .text('Queue: ') + .append( + $('') + .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 = $('') + .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 = $('

    ') + .addClass('form-group has-feedback') + .append($label, $input, $clearIcon) + return $formGroup + }, + + expandFormGroup: function () { + let expandByDefault = ( + readCookie('zuul_expand_by_default', false) === 'true') + + let $checkbox = $('') + .attr('type', 'checkbox') + .attr('id', 'expand_by_default') + .prop('checked', expandByDefault) + .change(this.handleExpandByDefault) + + let $label = $('

    '); - - for (var i = 0; i < change_queue._tree_columns; i++) { - var $tree_cell = $('
    ') - .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 = $('') - .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 = $('') - .css('width', change_width + 'px') - .addClass('zuul-change-cell') - .append(this.change_panel(change)); - - $change_row.append($change_column); - - var $change_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 = $('') - .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 = $('
    ') - .addClass('zuul-pipeline-header'); - - var $heading = $('

    ') - .css('vertical-align', 'middle') - .text(pipeline.name) - .append( - $('') - .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 = $('') - $.each( pipeline.description.split(/\r?\n\r?\n/), function(index, descr_part){ - descr.append($('

    ').text(descr_part)); - }); - $header_div.append( - $('

    ').append(descr) - ); - } - return $header_div; - }, - - pipeline: function (pipeline, count) { - var format = this; - var $html = $('

    ') - .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( - $('

    ') - .text('Queue: ') - .append( - $('') - .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 = $('
    ') - .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 = $('

    ') - .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 = $('') - .attr('type', 'checkbox') - .attr('id', 'expand_by_default') - .prop('checked', expand_by_default) - .change(this.handle_expand_by_default); - - var $label = $('