From 4a781a7f8699f5b483f79b1bdface0ba2ba92428 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 25 Jul 2017 07:28:04 -0400 Subject: [PATCH] Use yarn and webpack to manage zuul-web javascript yarn drives package and dependency management. webpack handles bundling, minification and transpiling down to browser-acceptable javascript but allows for more modern javascript like import statements. There are some really neat things in the webpack dev server. CSS changes, for instance, get applied immediately without a refresh. Other things, like the jquery plugin do need a refresh, but it's handled just on a file changing. As a followup, we can also consider turning the majority of the status page into a webpack library that other people can depend on as a mechanism for direct use. Things like that haven't been touched because allowing folks to poke at the existing known status page without too many changes using the tools seems like a good way for people to learn/understand the stack. Move things so that the built content gets put into zuul/web/static so that the built-in static serving from zuul-web will/can serve the files. Update MANIFEST.in so that if npm run build:dist is run before the python setup.py sdist, the built html/javascript content will be included in the source tarball. Add a pbr hook so that if yarn is installed, javascript content will be built before the tarball. Add a zuul job with a success url that contains a source_url pointing to the live v3 data. This adds a framework for verifying that we can serve the web app urls and their dependencies for all of the various ways we want to support folks hosting zuul-web. It includes a very simple reverse proxy server for approximating what we do in openstack to "white label" the Zuul service -- that is, hide the multitenancy aspect and present the single tenant at the site root. We can run similar tests without the proxy to ensure the default, multi-tenant view works as well. Add babel transpiling enabling use of ES6 features ECMAScript6 has a bunch of nice things, like block scoped variables, const, template strings and classes. Babel is a javascript transpiler which webpack can use to allow us to write using modern javascript but the resulting code to still work on older browsers. Use the babel-plugin-angularjs-annotate so that angular's dependency injection doesn't get borked by babel's transpiling things (which causes variables to otherwise be renamed in a way that causes angular to not find them) While we're at it, replace our use of var with let (let is the new block-scoped version of var) and toss in some use of const and template strings for good measure. Add StandardJS eslint config for linting JavaScript Standard Style is a code style similar to pep8/flake8. It's being added here not because of the pep8 part, but because the pyflakes equivalent can catch real errors. This uses the babel-eslint parser since we're using Babel to transpile already. This auto-formats the existing code with: npm run format Rather than using StandardJS directly through the 'standard' package, use the standardjs eslint plugin so that we can ignore the camelCase rule (and any other rule that might emerge in the future) Many of under_score/camelCase were fixed in a previous version of the patch. Since the prevailing zuul style is camelCase methods anyway, those fixes were left. That warning has now been disabled. Other things, such as == vs. === and ensuring template strings are in backticks are fixed. Ignore indentation errors for now - we'll fix them at the end of this stack and then remove the exclusion. Add a 'format' npm run target that will run the eslint command with --fix for ease of fixing reported issues. Add a 'lint' npm run target and a 'lint' environment that runs with linting turned to errors. The next patch makes the lint environment more broadly useful. When we run lint, also run the BundleAnalyzerPlugin and set the success-url to the report. Add an angular controller for status and stream page Wrap the status and stream page construction with an angular controller so that all the javascripts can be bundled in a single file. Building the files locally is wonderful and all, but what we really want is to make a tarball that has the built code so that it can be deployed. Put it in the root source dir so that it can be used with the zuul fetch-javascript-tarball role. Also, replace the custom npm job with the new build-javascript-content job which naturally grabs the content we want. Make a 'main.js' file that imports the other three so that we just have a single bundle. Then, add a 'vendor' entry in the common webpack file and use the CommonsChunkPlugin to extract dependencies into their own bundle. A second CommonsChunkPlugin entry pulls out a little bit of metadata that would otherwise cause the main and vendor chunks to change even with no source change. Then add chunkhash into the filename. This way the files themselves can be aggressively cached. This all follows recommendations from https://webpack.js.org/guides/caching/ https://webpack.js.org/guides/code-splitting/ and https://webpack.js.org/guides/output-management/ Change-Id: I2e1230783fe57f1bc3b7818460463df1e659936b Co-Authored-By: Tristan Cacqueray Co-Authored-By: James E. Blair --- .babelrc | 8 + .eslintrc | 10 + .gitignore | 3 + .zuul.yaml | 55 +- MANIFEST.in | 1 + TESTING.rst | 17 + doc/source/admin/components.rst | 5 + doc/source/developer/index.rst | 1 + doc/source/developer/javascript.rst | 223 + etc/status/README.rst | 27 - etc/status/fetch-dependencies.sh | 23 - etc/status/public_html/index.html | 39 - etc/status/public_html/jquery.zuul.js | 945 --- etc/status/public_html/styles/zuul.css | 58 - etc/status/public_html/zuul.app.js | 108 - package.json | 60 + playbooks/tox/post.yaml | 3 + playbooks/tox/pre.yaml | 5 + playbooks/tox/run.yaml | 5 + setup.cfg | 4 + test-requirements.txt | 1 + tests/base.py | 71 + tests/unit/test_streaming.py | 3 +- tests/unit/test_web.py | 3 + tests/unit/test_web_urls.py | 101 + tools/install-js-repos-rpm.sh | 256 + tools/install-js-tools.sh | 35 + {etc/status => web}/.gitignore | 0 {etc/status => web}/.jshintignore | 0 {etc/status => web}/.jshintrc | 0 web/config/webpack.common.js | 137 + web/config/webpack.dev.js | 36 + web/config/webpack.lint.js | 32 + web/config/webpack.prod.js | 49 + web/dashboard.js | 115 + .../public_html => web}/images/black.png | Bin .../public_html => web}/images/green.png | Bin .../public_html => web}/images/grey.png | Bin .../public_html => web}/images/line-angle.png | Bin .../public_html => web}/images/line-t.png | Bin .../public_html => web}/images/line.png | Bin .../status/public_html => web}/images/red.png | Bin web/jquery.zuul.js | 929 +++ web/main.js | 25 + .../status-basic.json | 0 .../status-openstack.json | 0 .../status-tree.json | 0 web/status.js | 126 + web/stream.js | 99 + web/styles/stream.css | 18 + {zuul/web/static => web}/styles/zuul.css | 2 +- .../builds.html => web/templates/builds.ejs | 10 +- .../jobs.html => web/templates/jobs.ejs | 10 +- web/templates/status.ejs | 52 + web/templates/stream.ejs | 17 + .../index.html => web/templates/tenants.ejs | 12 +- web/util.js | 30 + webpack.config.js | 3 + yarn.lock | 6019 +++++++++++++++++ zuul/_setup_hook.py | 39 + zuul/cmd/web.py | 3 + zuul/web/__init__.py | 25 +- zuul/web/static/README | 61 - zuul/web/static/images/black.png | Bin 267 -> 0 bytes zuul/web/static/images/green.png | Bin 283 -> 0 bytes zuul/web/static/images/grey.png | Bin 282 -> 0 bytes zuul/web/static/images/line-angle.png | Bin 262 -> 0 bytes zuul/web/static/images/line-t.png | Bin 204 -> 0 bytes zuul/web/static/images/line.png | Bin 183 -> 0 bytes zuul/web/static/images/red.png | Bin 286 -> 0 bytes zuul/web/static/javascripts/jquery.zuul.js | 945 --- zuul/web/static/javascripts/zuul.angular.js | 99 - zuul/web/static/javascripts/zuul.app.js | 108 - zuul/web/static/status.html | 51 - zuul/web/static/stream.html | 114 - 75 files changed, 8625 insertions(+), 2611 deletions(-) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 doc/source/developer/javascript.rst delete mode 100644 etc/status/README.rst delete mode 100755 etc/status/fetch-dependencies.sh delete mode 100644 etc/status/public_html/index.html delete mode 100644 etc/status/public_html/jquery.zuul.js delete mode 100644 etc/status/public_html/styles/zuul.css delete mode 100644 etc/status/public_html/zuul.app.js create mode 100644 package.json create mode 100644 playbooks/tox/post.yaml create mode 100644 playbooks/tox/pre.yaml create mode 100644 playbooks/tox/run.yaml create mode 100644 tests/unit/test_web_urls.py create mode 100755 tools/install-js-repos-rpm.sh create mode 100755 tools/install-js-tools.sh rename {etc/status => web}/.gitignore (100%) rename {etc/status => web}/.jshintignore (100%) rename {etc/status => web}/.jshintrc (100%) create mode 100644 web/config/webpack.common.js create mode 100644 web/config/webpack.dev.js create mode 100644 web/config/webpack.lint.js create mode 100644 web/config/webpack.prod.js create mode 100644 web/dashboard.js rename {etc/status/public_html => web}/images/black.png (100%) rename {etc/status/public_html => web}/images/green.png (100%) rename {etc/status/public_html => web}/images/grey.png (100%) rename {etc/status/public_html => web}/images/line-angle.png (100%) rename {etc/status/public_html => web}/images/line-t.png (100%) rename {etc/status/public_html => web}/images/line.png (100%) rename {etc/status/public_html => web}/images/red.png (100%) create mode 100644 web/jquery.zuul.js create mode 100644 web/main.js rename etc/status/public_html/status-basic.json-sample => web/status-basic.json (100%) rename etc/status/public_html/status-openstack.json-sample => web/status-openstack.json (100%) rename etc/status/public_html/status-tree.json-sample => web/status-tree.json (100%) create mode 100644 web/status.js create mode 100644 web/stream.js create mode 100644 web/styles/stream.css rename {zuul/web/static => web}/styles/zuul.css (99%) rename zuul/web/static/builds.html => web/templates/builds.ejs (87%) rename zuul/web/static/jobs.html => web/templates/jobs.ejs (81%) create mode 100644 web/templates/status.ejs create mode 100644 web/templates/stream.ejs rename zuul/web/static/index.html => web/templates/tenants.ejs (79%) create mode 100644 web/util.js create mode 100644 webpack.config.js create mode 100644 yarn.lock create mode 100644 zuul/_setup_hook.py delete mode 100644 zuul/web/static/README delete mode 100644 zuul/web/static/images/black.png delete mode 100644 zuul/web/static/images/green.png delete mode 100644 zuul/web/static/images/grey.png delete mode 100644 zuul/web/static/images/line-angle.png delete mode 100644 zuul/web/static/images/line-t.png delete mode 100644 zuul/web/static/images/line.png delete mode 100644 zuul/web/static/images/red.png delete mode 100644 zuul/web/static/javascripts/jquery.zuul.js delete mode 100644 zuul/web/static/javascripts/zuul.angular.js delete mode 100644 zuul/web/static/javascripts/zuul.app.js delete mode 100644 zuul/web/static/status.html delete mode 100644 zuul/web/static/stream.html 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 = $('