Revert "Revert "web: rewrite interface in react""
This reverts commit 3dba813c643ec8f4b3323c2a09c6aecf8ad4d338. Change-Id: I233797a9b4e3485491c49675da2c2efbdba59449
8
.babelrc
@ -1,8 +0,0 @@
|
||||
{
|
||||
"presets": ['env'],
|
||||
"plugins": [[
|
||||
"angularjs-annotate", {
|
||||
"explicitOnly": false
|
||||
}
|
||||
]]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
parser: babel-eslint
|
||||
plugins:
|
||||
- standard
|
||||
rules:
|
||||
camelcase: off
|
||||
extends:
|
||||
- ./node_modules/eslint-config-standard/eslintrc.json
|
41
.zuul.yaml
@ -42,27 +42,20 @@
|
||||
- job:
|
||||
name: zuul-build-dashboard
|
||||
parent: build-javascript-content
|
||||
success-url: 'npm/html/status.html'
|
||||
success-url: 'npm/html/'
|
||||
files:
|
||||
- package.json
|
||||
- tsconfig.json
|
||||
- tslint.json
|
||||
- web/.*
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
vars:
|
||||
javascript_content_dir: zuul/web/static
|
||||
javascript_content_dir: "../zuul/web/static"
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
zuul_api_url: https://zuul.openstack.org
|
||||
run: playbooks/dashboard/run.yaml
|
||||
|
||||
- job:
|
||||
name: zuul-build-dashboard-multi-tenant
|
||||
parent: zuul-build-dashboard
|
||||
success-url: 'npm/html/tenants.html'
|
||||
post-run: playbooks/dashboard/multi.yaml
|
||||
vars:
|
||||
zuul_api_url: https://softwarefactory-project.io/zuul
|
||||
javascript_copy_links: false
|
||||
|
||||
- project:
|
||||
check:
|
||||
@ -82,15 +75,15 @@
|
||||
- zuul-build-dashboard-multi-tenant
|
||||
- nodejs-npm-run-lint:
|
||||
vars:
|
||||
node_version: 8
|
||||
node_version: 8
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
- nodejs-npm-run-test:
|
||||
vars:
|
||||
node_version: 8
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
success-url: 'npm/reports/bundle.html'
|
||||
files:
|
||||
- package.json
|
||||
- tsconfig.json
|
||||
- tslint.json
|
||||
- web/.*
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- zuul-stream-functional
|
||||
- zuul-tox-remote
|
||||
- pbrx-build-container-images:
|
||||
@ -114,15 +107,15 @@
|
||||
- zuul-build-dashboard
|
||||
- nodejs-npm-run-lint:
|
||||
vars:
|
||||
node_version: 8
|
||||
node_version: 8
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
- nodejs-npm-run-test:
|
||||
vars:
|
||||
node_version: 8
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
success-url: 'npm/reports/bundle.html'
|
||||
files:
|
||||
- package.json
|
||||
- tsconfig.json
|
||||
- tslint.json
|
||||
- web/.*
|
||||
- webpack.config.js
|
||||
- yarn.lock
|
||||
- zuul-stream-functional
|
||||
- zuul-tox-remote
|
||||
- pbrx-build-container-images:
|
||||
@ -134,7 +127,9 @@
|
||||
- publish-openstack-python-branch-tarball
|
||||
- publish-openstack-javascript-content:
|
||||
vars:
|
||||
node_version: 8
|
||||
node_version: 8
|
||||
zuul_work_dir: "{{ zuul.project.src_dir }}/web"
|
||||
create_tarball_directory: build
|
||||
- openstackzuul-pbrx-push-container-images:
|
||||
vars:
|
||||
pbrx_prefix: zuul
|
||||
|
@ -1,6 +1,7 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
include zuul/web/static/*
|
||||
include zuul/web/static/static/*/*
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
@ -38,11 +38,15 @@ As of zuul v3, a running zookeeper is required to execute tests.
|
||||
|
||||
*Install javascript dependencies*::
|
||||
|
||||
pushd web
|
||||
yarn install
|
||||
popd
|
||||
|
||||
*Build javascript assets*::
|
||||
|
||||
npm run build:dev
|
||||
pushd web
|
||||
yarn build
|
||||
popd
|
||||
|
||||
Run The Tests
|
||||
-------------
|
||||
|
@ -95,7 +95,7 @@ Static External
|
||||
Sub-URL
|
||||
Serve a Zuul dashboard from a location below the root URL as part of
|
||||
presenting integration with other application.
|
||||
https://softwarefactory-project.io/zuul3/ is an example of a Zuul dashboard
|
||||
https://softwarefactory-project.io/zuul/ is an example of a Zuul dashboard
|
||||
that is being served from a Sub-URL.
|
||||
|
||||
None of those make any sense for simple non-production oriented deployments, so
|
||||
@ -121,18 +121,71 @@ simplest reverse-proxy case is::
|
||||
Static Offload
|
||||
--------------
|
||||
|
||||
To have the Reverse Proxy serve the static html/javscript assets instead of
|
||||
To have the Reverse Proxy serve the static html/javascript assets instead of
|
||||
proxying them to the REST layer, register the location where you unpacked
|
||||
the web application as the document root and add a simple rewrite rule::
|
||||
the web application as the document root and add rewrite rules::
|
||||
|
||||
DocumentRoot /var/lib/html
|
||||
<Directory /var/lib/html>
|
||||
<Directory /usr/share/zuul>
|
||||
Require all granted
|
||||
</Directory>
|
||||
RewriteEngine on
|
||||
RewriteRule ^/t/.*/(.*)$ /$1 [L]
|
||||
RewriteRule ^/api/tenant/(.*)/console-stream ws://localhost:9000/api/tenant/$1/console-stream [P]
|
||||
RewriteRule ^/api/(.*)$ http://localhost:9000/api/$1 [P]
|
||||
Alias / /usr/share/zuul/
|
||||
<Location />
|
||||
RewriteEngine on
|
||||
RewriteBase /
|
||||
# Rewrite api to the zuul-web endpoint
|
||||
RewriteRule api/tenant/(.*)/console-stream ws://localhost:9000/api/tenant/$1/console-stream [P,L]
|
||||
RewriteRule api/(.*)$ http://localhost:9000/api/$1 [P,L]
|
||||
# Backward compatible rewrite
|
||||
RewriteRule t/(.*)/(.*).html(.*) /t/$1/$2$3 [R=301,L,NE]
|
||||
|
||||
# Don't rewrite files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
</Location>
|
||||
|
||||
|
||||
Sub directory serving
|
||||
---------------------
|
||||
|
||||
The web application needs to be rebuild to update the internal location of
|
||||
the static files. Set the homepage setting in the package.json to an
|
||||
absolute path or url. For example, to deploy the web interface through a
|
||||
'/zuul/' sub directory:
|
||||
|
||||
.. note::
|
||||
|
||||
The web dashboard source code and package.json are located in the ``web``
|
||||
directory. All the yarn commands need to be executed from the ``web``
|
||||
directory.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sed -e 's#"homepage": "/"#"homepage": "/zuul/"#' -i package.json
|
||||
yarn build
|
||||
|
||||
Then assuming the web application is unpacked in /usr/share/zuul,
|
||||
add the following rewrite rules::
|
||||
|
||||
<Directory /usr/share/zuul>
|
||||
Require all granted
|
||||
</Directory>
|
||||
Alias /zuul /usr/share/zuul/
|
||||
<Location /zuul>
|
||||
RewriteEngine on
|
||||
RewriteBase /zuul
|
||||
# Rewrite api to the zuul-web endpoint
|
||||
RewriteRule api/tenant/(.*)/console-stream ws://localhost:9000/api/tenant/$1/console-stream [P,L]
|
||||
RewriteRule api/(.*)$ http://localhost:9000/api/$1 [P,L]
|
||||
# Backward compatible rewrite
|
||||
RewriteRule t/(.*)/(.*).html(.*) /t/$1/$2$3 [R=301,L,NE]
|
||||
|
||||
# Don't rewrite files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /zuul/index.html [L]
|
||||
</Location>
|
||||
|
||||
|
||||
White Labeled Tenant
|
||||
--------------------
|
||||
@ -147,14 +200,27 @@ rule to ensure connection webhooks don't try to get put into the tenant scope.
|
||||
|
||||
Assuming the zuul tenant name is "example", the rewrite rules are::
|
||||
|
||||
DocumentRoot /var/lib/html
|
||||
<Directory /var/lib/html>
|
||||
<Directory /usr/share/zuul>
|
||||
Require all granted
|
||||
</Directory>
|
||||
RewriteEngine on
|
||||
RewriteRule ^/api/connection/(.*)$ http://localhost:9000/api/connection/$1 [P]
|
||||
RewriteRule ^/api/console-stream ws://localhost:9000/api/tenant/example/console-stream [P]
|
||||
RewriteRule ^/api/(.*)$ http://localhost:9000/api/tenant/example/$1 [P]
|
||||
Alias / /usr/share/zuul/
|
||||
<Location />
|
||||
RewriteEngine on
|
||||
RewriteBase /
|
||||
# Rewrite api to the zuul-web endpoint
|
||||
RewriteRule api/connection/(.*)$ http://localhost:9000/api/connection/$1 [P,L]
|
||||
RewriteRule api/console-stream ws://localhost:9000/api/tenant/example/console-stream [P,L]
|
||||
RewriteRule api/(.*)$ http://localhost:9000/api/tenant/example/$1 [P,L]
|
||||
# Backward compatible rewrite
|
||||
RewriteRule t/(.*)/(.*).html(.*) /t/$1/$2$3 [R=301,L,NE]
|
||||
|
||||
# Don't rewrite files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
</Location>
|
||||
|
||||
|
||||
|
||||
Static External
|
||||
---------------
|
||||
@ -165,9 +231,8 @@ Static External
|
||||
dynamic url rewrite rules only works for white-labeled deployments.
|
||||
|
||||
In order to serve the zuul dashboard code from an external static location,
|
||||
``ZUUL_API_URL`` must be set at javascript build time by passing the
|
||||
``--define`` flag to the ``npm build:dist`` command.
|
||||
``REACT_APP_ZUUl_API`` must be set at javascript build time:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm build:dist -- --define "ZUUL_API_URL='http://zuul-web.example.com'"
|
||||
REACT_APP_ZUUL_API='http://zuul-web.example.com' yarn build
|
||||
|
@ -6,9 +6,15 @@ 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 dashboard is written in `Typescript`_ and `Angular`_ and is
|
||||
managed by `yarn`_ and `webpack`_ which in turn both assume a functioning
|
||||
and recent `nodejs`_ installation.
|
||||
The web dashboard is written in `React`_ and `Patternfly`_ and is
|
||||
managed by `create-react-app`_ and `yarn`_ which in turn both assume a
|
||||
functioning and recent `nodejs`_ installation.
|
||||
|
||||
.. note::
|
||||
|
||||
The web dashboard source code and package.json are located in the ``web``
|
||||
directory. All the yarn commands need to be executed from the ``web``
|
||||
directory.
|
||||
|
||||
For the impatient who don't want deal with javascript toolchains
|
||||
----------------------------------------------------------------
|
||||
@ -98,28 +104,27 @@ conflicts is to first resolve the conflicts, if any, in ``package.json``. Then:
|
||||
Which causes yarn to discard the ``yarn.lock`` file, recalculate the
|
||||
dependencies and write new content.
|
||||
|
||||
webpack asset management
|
||||
------------------------
|
||||
React Components
|
||||
----------------
|
||||
|
||||
`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.
|
||||
Each page is a React Component. For instance the status.html page code is
|
||||
``web/src/pages/status.jsx``.
|
||||
|
||||
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``.
|
||||
Mapping of pages/urls to components can be found in the route list in
|
||||
``web/src/routes.js``.
|
||||
|
||||
Angular Components
|
||||
------------------
|
||||
Here are some useful documentation about the different libraries:
|
||||
|
||||
Each page has an `Angular Component`_ associated with it. For instance, the
|
||||
``status.html`` page has code in ``web/status/status.component.ts`` and the
|
||||
relevant HTML can be found in ``web/status/status.component.html``.
|
||||
- https://reactjs.org/docs/getting-started.html
|
||||
- https://reacttraining.com/react-router/web/guides/philosophy
|
||||
- https://react-bootstrap.github.io/components/forms/
|
||||
- https://redux.js.org/introduction/coreconcepts
|
||||
- https://www.patternfly.org/pattern-library/
|
||||
- https://rawgit.com/patternfly/patternfly-react/gh-pages/
|
||||
|
||||
Mapping of pages/urls to components can be found in the routing module in
|
||||
``web/app-routing.module.ts``.
|
||||
The gh-pages are built from storybook present in the patternfly-react
|
||||
repository. Sometime the 'View Info' is not enough and using grep in the
|
||||
repository may yield better documentation.
|
||||
|
||||
Development
|
||||
-----------
|
||||
@ -128,68 +133,43 @@ Building the code can be done with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run build
|
||||
yarn build
|
||||
|
||||
zuul-web has a ``static`` route defined which serves files from
|
||||
``zuul/web/static``. ``npm run build`` will put the build output files
|
||||
``zuul/web/static``. ``yarn 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:
|
||||
Development server that handles things like reloading and
|
||||
hot-updating of code can be started with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run build:dev
|
||||
yarn start
|
||||
|
||||
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:
|
||||
will build the code and launch the dev server on `localhost:3000`. Fake
|
||||
api response needs to be set in the ``web/public/api`` directory::
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start
|
||||
mkdir public/api/
|
||||
for route in info status jobs builds; do
|
||||
curl -o public/api/${route} https://zuul.openstack.org/api/${route}
|
||||
done
|
||||
|
||||
will build the code and launch the dev server on `localhost:8080`. It will
|
||||
be configured to use the API endpoint from OpenStack's Zuul. The
|
||||
``webpack-dev-server`` watches for changes to the files and
|
||||
re-compiles/refresh as needed.
|
||||
To use an existing zuul api, uses the REACT_APP_ZUUl_API environment
|
||||
variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:multi
|
||||
# Use openstack zuul's api:
|
||||
yarn start:openstack
|
||||
|
||||
will do the same but will be pointed at the SoftwareFactory Project Zuul, which
|
||||
is multi-tenant.
|
||||
# Use software-factory multi-tenant zuul's api:
|
||||
yarn start:multi
|
||||
|
||||
Arbitrary command line options will be passed through after a ``--`` such as:
|
||||
# Use a custom zuul:
|
||||
REACT_APP_ZUUL_API="https://zuul.example.com/api/" yarn start
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start -- --open-file='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:basic
|
||||
|
||||
Run status against `openstack` built-in demo data
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:openstack
|
||||
|
||||
Run status against `tree` built-in demo data.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm run start:tree
|
||||
|
||||
Additional run commands can be added in `package.json` in the ``scripts``
|
||||
section.
|
||||
|
||||
Deploying
|
||||
---------
|
||||
@ -199,31 +179,16 @@ 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
|
||||
``yarn build`` has been done before ``pip install .`` or
|
||||
``python setup.py sdist``, all the files will be where they need to be.
|
||||
As long as `yarn`_ is installed, the installation of zuul will run
|
||||
``npm run build`` appropriately.
|
||||
|
||||
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 build`` appropriately.
|
||||
|
||||
.. _yarn: https://yarnpkg.com/en/
|
||||
.. _nodejs: https://nodejs.org/
|
||||
.. _webpack: https://webpack.js.org/
|
||||
.. _devtool: https://webpack.js.org/configuration/devtool/#devtool
|
||||
.. _nodeenv: https://pypi.org/project/nodeenv
|
||||
.. _Angular: https://angular.io
|
||||
.. _Angular Component: https://angular.io/guide/architecture-components
|
||||
.. _Typescript: https://www.typescriptlang.org/
|
||||
.. _React: https://reactjs.org/
|
||||
.. _Patternfly: https://www.patternfly.org/
|
||||
.. _create-react-app: https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md
|
||||
|
86
package.json
@ -1,86 +0,0 @@
|
||||
{
|
||||
"name": "@zuul-ci/dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "Web Dashboard for Zuul",
|
||||
"main": "web/main.ts",
|
||||
"repository": "https://git.zuul-ci.org/zuul",
|
||||
"author": "OpenStack Infra",
|
||||
"license": "Apache-2.0",
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/common": "^6.0.3",
|
||||
"@angular/compiler": "^6.0.3",
|
||||
"@angular/core": "^6.0.3",
|
||||
"@angular/forms": "^6.0.3",
|
||||
"@angular/platform-browser": "^6.0.3",
|
||||
"@angular/platform-browser-dynamic": "^6.0.3",
|
||||
"@angular/router": "^6.0.3",
|
||||
"bootstrap": "3.1.1",
|
||||
"core-js": "^2.5.3",
|
||||
"graphitejs": "https://github.com/prestontimmons/graphitejs/archive/master.tar.gz",
|
||||
"jquery": "^3.3.1",
|
||||
"jquery-visibility": "https://github.com/mathiasbynens/jquery-visibility/archive/master.tar.gz",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.2.0",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"zone.js": "^0.8.26"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:dist",
|
||||
"build:dev": "webpack --env=dev",
|
||||
"build:dist": "webpack --env=prod",
|
||||
"build:dist-with-depends": "yarn install && npm run build:dist",
|
||||
"format": "tslint --project tsconfig.json -c tslint.json --fix",
|
||||
"lint": "webpack --env=lint",
|
||||
"start": "webpack-dev-server --env=dev --define ZUUL_API_URL=\"'https://zuul.openstack.org'\" --open-page='status.html'",
|
||||
"start:multi": "webpack-dev-server --env=dev --define ZUUL_API_URL=\"'https://softwarefactory-project.io/zuul'\" --open-page='tenants.html'",
|
||||
"start:basic": "webpack-dev-server --env=dev --open-page='status.html?demo=basic'",
|
||||
"start:openstack": "webpack-dev-server --env=dev --open-page='status.html?demo=openstack'",
|
||||
"start:tree": "webpack-dev-server --env=dev --open-page='status.html?demo=tree'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/angular": "^1.6.43",
|
||||
"@types/bootstrap": "^4.0.0",
|
||||
"@types/core-js": "^0.9.46",
|
||||
"@types/jquery": "^3.3.1",
|
||||
"@types/node": "^9.4.7",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-angularjs-annotate": "^0.8.2",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"clean-webpack-plugin": "^0.1.16",
|
||||
"codelyzer": "^4.2.1",
|
||||
"css-loader": "^0.28.10",
|
||||
"eslint": ">=3.19.0",
|
||||
"eslint-config-standard": "^11.0.0-beta.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^6.0.0",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"file-loader": "^1.1.11",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.0.0",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
"style-loader": "^0.20.3",
|
||||
"ts-loader": "^4.1.0",
|
||||
"ts-node": "^5.0.1",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-angular": "^1.1.1",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"typescript": "2.7.2",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^4.4.0",
|
||||
"webpack-archive-plugin": "^3.0.0",
|
||||
"webpack-bundle-analyzer": "^2.9.1",
|
||||
"webpack-cli": "^2.0.11",
|
||||
"webpack-dev-server": "^3.0.0",
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
|
||||
- name: Make tenant subdir
|
||||
file:
|
||||
state: directory
|
||||
dest: '{{ zuul.project.src_dir }}/{{ javascript_content_dir }}/t'
|
||||
|
||||
- name: Copy the html/javascript content into subdirs
|
||||
shell: |
|
||||
CONTENT_DIR="{{ zuul.project.src_dir }}/{{ javascript_content_dir }}"
|
||||
mkdir $CONTENT_DIR/t/{{ item }}
|
||||
for f in $(find $CONTENT_DIR -type f -mindepth 1 -maxdepth 1) ; do
|
||||
cp $f $CONTENT_DIR/t/{{ item }}
|
||||
done
|
||||
with_items:
|
||||
- local
|
||||
- ansible
|
||||
- ansible-dev
|
||||
- openstack.org
|
||||
- rdoproject.org
|
@ -1,18 +1,19 @@
|
||||
- hosts: all
|
||||
pre_tasks:
|
||||
- name: Update homepage for sub directory deployment
|
||||
replace:
|
||||
path: '{{ zuul.project.src_dir }}/web/package.json'
|
||||
regexp: '"homepage": "/"'
|
||||
replace: '"homepage": "./"'
|
||||
# NOTE: using "./" is not enough to support html5 links, even with
|
||||
# rewrite rules for unknown files, accessing 'job/devstack' will make
|
||||
# the dashboard load static files from 'job/static/...'
|
||||
# This works for the preview dashboard that can only be loaded from the
|
||||
# npm/html directory anyway.
|
||||
roles:
|
||||
- revoke-sudo
|
||||
- set-zuul-log-path-fact
|
||||
# Both sets of quotes are required.
|
||||
# The "" quotes are for the shell to protect the '' quotes.
|
||||
# We need the '' quotes because defines here are essentially
|
||||
# direct string substitutions. Therefore:
|
||||
# --define "ZUUL_API_URL='https://zuul.openstack.org'"
|
||||
# with the javascript code:
|
||||
# return ZUUL_API_URL
|
||||
# results in
|
||||
# return 'https://zuul.openstack.org'
|
||||
# in the compiled javascript.
|
||||
- role: npm
|
||||
npm_command: >-
|
||||
build:dist --
|
||||
--define "ZUUL_API_URL='{{ zuul_api_url }}'"
|
||||
npm_command: build
|
||||
environment:
|
||||
REACT_APP_ZUUL_API: "{{ zuul_api_url }}/api/"
|
||||
|
11
releasenotes/notes/react-zuul-a593c7627ca22b37.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The Zuul web dashboard has been rewritten in React.
|
||||
upgrade:
|
||||
- |
|
||||
The Zuul web dashboard is now a single index.html and static offload
|
||||
server needs new rewrite rules. Check the install instruction for backward
|
||||
compatible rewrite rules. Note that serving the web dashboard from a
|
||||
sub-directory requires the application to be rebuilt using the desired
|
||||
homepage location.
|
@ -245,9 +245,9 @@ class TestWeb(BaseTestWeb):
|
||||
self.assertEqual(1, data[0]['queue'])
|
||||
|
||||
def test_web_bad_url(self):
|
||||
# do we 404 correctly
|
||||
# do we redirect to index.html
|
||||
resp = self.get_url("status/foo")
|
||||
self.assertEqual(404, resp.status_code)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_web_find_change(self):
|
||||
# can we filter by change id
|
||||
|
@ -51,10 +51,8 @@ class TestWebURLs(ZuulTestCase):
|
||||
]:
|
||||
for item in page.find_all(tag):
|
||||
suburl = item.get(attr)
|
||||
# Skip empty urls. Also skip the navbar relative link for now.
|
||||
# TODO(mordred) Remove when we have the top navbar link sorted.
|
||||
if suburl is None or suburl == "../":
|
||||
continue
|
||||
if suburl.startswith('/'):
|
||||
suburl = suburl[1:]
|
||||
link = urllib.parse.urljoin(url, suburl)
|
||||
self._get(self.port, link)
|
||||
|
||||
@ -66,7 +64,8 @@ class TestDirect(TestWebURLs):
|
||||
self.port = self.web.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/t/tenant-one/status.html')
|
||||
self._crawl('/')
|
||||
self._crawl('/t/tenant-one/status')
|
||||
|
||||
|
||||
class TestWhiteLabel(TestWebURLs):
|
||||
@ -81,7 +80,8 @@ class TestWhiteLabel(TestWebURLs):
|
||||
self.port = self.proxy.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/status.html')
|
||||
self._crawl('/')
|
||||
self._crawl('/status')
|
||||
|
||||
|
||||
class TestWhiteLabelAPI(TestWebURLs):
|
||||
@ -108,11 +108,11 @@ class TestSuburl(TestWebURLs):
|
||||
def setUp(self):
|
||||
super(TestSuburl, self).setUp()
|
||||
rules = [
|
||||
('^/zuul3/(.*)$', 'http://localhost:{}/\\1'.format(
|
||||
('^/zuul/(.*)$', 'http://localhost:{}/\\1'.format(
|
||||
self.web.port)),
|
||||
]
|
||||
self.proxy = self.useFixture(WebProxyFixture(rules))
|
||||
self.port = self.proxy.port
|
||||
|
||||
def test_status_page(self):
|
||||
self._crawl('/zuul3/t/tenant-one/status.html')
|
||||
self._crawl('/zuul/')
|
||||
|
6
tools/pip.sh
Normal file → Executable file
@ -32,7 +32,9 @@ then
|
||||
fi
|
||||
if [[ ! -f zuul/web/static/status.html ]]
|
||||
then
|
||||
yarn install
|
||||
npm run build:dev
|
||||
pushd web/
|
||||
yarn install
|
||||
yarn build
|
||||
popd
|
||||
fi
|
||||
pip install $*
|
||||
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./zuul/web/static/",
|
||||
"noImplicitAny": false,
|
||||
"target": "es6",
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": false
|
||||
},
|
||||
"include": [
|
||||
"webpack.config.ts",
|
||||
"web/**/*.ts"
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint-angular"],
|
||||
"rules": {
|
||||
"array-type": [true, "array-simple"],
|
||||
"semicolon": [true, "never"]
|
||||
}
|
||||
}
|
23
web/.eslintrc
Normal file
@ -0,0 +1,23 @@
|
||||
parser: babel-eslint
|
||||
plugins:
|
||||
- standard
|
||||
- jest
|
||||
rules:
|
||||
no-console: off
|
||||
semi: [error, never]
|
||||
quotes: [error, single]
|
||||
lines-between-class-members: error
|
||||
react/prop-types: error
|
||||
react/jsx-key: error
|
||||
react/no-did-mount-set-state: error
|
||||
react/no-did-update-set-state: error
|
||||
react/no-deprecated: error
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:react/recommended
|
||||
settings:
|
||||
react:
|
||||
version: 16.4
|
||||
env:
|
||||
jest/globals: true
|
||||
browser: true
|
1
web/.gitignore
vendored
@ -1 +0,0 @@
|
||||
public_html/lib
|
@ -1 +0,0 @@
|
||||
public_html/lib
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"bitwise": true,
|
||||
"eqeqeq": true,
|
||||
"forin": true,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": true,
|
||||
"nonew": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
|
||||
"strict": false,
|
||||
"laxbreak": true,
|
||||
"browser": true,
|
||||
|
||||
"predef": [
|
||||
"jQuery",
|
||||
"zuul"
|
||||
]
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
// Routing information for Zuul dashboard pages
|
||||
//
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { NgModule, isDevMode } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
import BuildsComponent from './builds/builds.component'
|
||||
import JobsComponent from './jobs/jobs.component'
|
||||
import StatusComponent from './status/status.component'
|
||||
import StreamComponent from './stream/stream.component'
|
||||
import TenantsComponent from './tenants/tenants.component'
|
||||
|
||||
// Have all routes go to builds.html for now
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
path: 't/:tenant/builds.html',
|
||||
component: BuildsComponent
|
||||
},
|
||||
{
|
||||
path: 'builds.html',
|
||||
component: BuildsComponent
|
||||
},
|
||||
{
|
||||
path: 't/:tenant/status.html',
|
||||
component: StatusComponent
|
||||
},
|
||||
{
|
||||
path: 'status.html',
|
||||
component: StatusComponent
|
||||
},
|
||||
{
|
||||
path: 't/:tenant/jobs.html',
|
||||
component: JobsComponent
|
||||
},
|
||||
{
|
||||
path: 'jobs.html',
|
||||
component: JobsComponent
|
||||
},
|
||||
{
|
||||
path: 'stream.html',
|
||||
component: StreamComponent
|
||||
},
|
||||
{
|
||||
path: 't/:tenant/stream.html',
|
||||
component: StreamComponent
|
||||
},
|
||||
{
|
||||
path: 'tenants.html',
|
||||
component: TenantsComponent
|
||||
},
|
||||
{
|
||||
path: 't/tenants.html',
|
||||
component: TenantsComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
component: StatusComponent
|
||||
}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(
|
||||
appRoutes,
|
||||
// Enable router tracing in devel mode. This prints router decisions
|
||||
// to the console.log.
|
||||
{ enableTracing: isDevMode() }
|
||||
)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
@ -1,28 +0,0 @@
|
||||
// Main dashboard component
|
||||
//
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'zuul-dashboard',
|
||||
template: `
|
||||
<navigation></navigation>
|
||||
<div class="container-fluid">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AppComponent {}
|
@ -1,73 +0,0 @@
|
||||
// Entrypoint for Zuul dashboard pages
|
||||
//
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import './styles/zuul.css'
|
||||
|
||||
import { APP_BASE_HREF } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
import { CoreModule } from './core/core.module'
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { getAppBaseHref } from './zuul/zuul.service'
|
||||
|
||||
import BuildsComponent from './builds/builds.component'
|
||||
import NavigationComponent from './navigation/navigation.component'
|
||||
import JobsComponent from './jobs/jobs.component'
|
||||
import StatusComponent from './status/status.component'
|
||||
import StreamComponent from './stream/stream.component'
|
||||
import TenantsComponent from './tenants/tenants.component'
|
||||
import ZuulService from './zuul/zuul.service'
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
CoreModule.forRoot({}),
|
||||
AppRoutingModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
BuildsComponent,
|
||||
NavigationComponent,
|
||||
JobsComponent,
|
||||
StatusComponent,
|
||||
StreamComponent,
|
||||
TenantsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
BuildsComponent,
|
||||
NavigationComponent,
|
||||
JobsComponent,
|
||||
StatusComponent,
|
||||
StreamComponent,
|
||||
TenantsComponent
|
||||
],
|
||||
providers: [
|
||||
ZuulService,
|
||||
{provide: APP_BASE_HREF, useValue: getAppBaseHref()}
|
||||
],
|
||||
bootstrap: [
|
||||
AppComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
@ -1,28 +0,0 @@
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
export default class Build {
|
||||
|
||||
constructor(
|
||||
public job_name: string,
|
||||
public result: string,
|
||||
public project: string,
|
||||
public pipeline: string,
|
||||
public ref_url: string,
|
||||
public duration: number,
|
||||
public start_time: string,
|
||||
public log_url?: string,
|
||||
) {}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<div class="container-fluid">
|
||||
<span style="float: right; margin-top: 7px;">
|
||||
<form class="form-inline" #buildsForm="ngForm">
|
||||
<div class="form-group">
|
||||
<label for="pipeline">Pipeline:</label>
|
||||
<input class="form-control" id="pipeline"
|
||||
[(ngModel)]="pipeline" name="pipeline" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="job_name">Job:</label>
|
||||
<input class="form-control" id="job_name"
|
||||
[(ngModel)]="job_name" name="job_name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project">Project:</label>
|
||||
<input class="form-control" id="project"
|
||||
[(ngModel)]="project" name="project">
|
||||
</div>
|
||||
<button type="submit" class="btn" (click)='buildsFetch()'>
|
||||
Refresh
|
||||
</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
<table class="table table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job</th>
|
||||
<th>Project</th>
|
||||
<th>Branch</th>
|
||||
<th>Pipeline</th>
|
||||
<th>Change</th>
|
||||
<th>Duration</th>
|
||||
<th>Log url</th>
|
||||
<th>Start time</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let build of builds" [class]="getRowClass(build)">
|
||||
<td>{{ build.job_name }}</td>
|
||||
<td>{{ build.project }}</td>
|
||||
<td>{{ build.branch }}</td>
|
||||
<td>{{ build.pipeline }}</td>
|
||||
<td><a href="{{ build.ref_url }}" target="_self">change</a></td>
|
||||
<td>{{ build.duration }} seconds</td>
|
||||
<td><a *ngIf="build.log_url" href="{{ build.log_url }}" target="_self">logs</a></td>
|
||||
<td>{{ build.start_time }}</td>
|
||||
<td>{{ build.result }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@ -1,78 +0,0 @@
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import 'rxjs/add/operator/map'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
import Build from './build'
|
||||
|
||||
|
||||
@Component({
|
||||
template: require('./builds.component.html')
|
||||
})
|
||||
export default class BuildsComponent implements OnInit {
|
||||
builds: Build[]
|
||||
pipeline: string
|
||||
job_name: string
|
||||
project: string
|
||||
|
||||
constructor(
|
||||
private http: HttpClient, private route: ActivatedRoute,
|
||||
private zuul: ZuulService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
|
||||
|
||||
this.pipeline = this.route.snapshot.queryParamMap.get('pipeline')
|
||||
this.job_name = this.route.snapshot.queryParamMap.get('job_name')
|
||||
this.project = this.route.snapshot.queryParamMap.get('project')
|
||||
|
||||
this.buildsFetch()
|
||||
}
|
||||
|
||||
buildsFetch(): void {
|
||||
let params = new HttpParams()
|
||||
if (this.pipeline) { params = params.set('pipeline', this.pipeline) }
|
||||
if (this.job_name) { params = params.set('job_name', this.job_name) }
|
||||
if (this.project) { params = params.set('project', this.project) }
|
||||
|
||||
const remoteLocation = this.zuul.getSourceUrl('builds')
|
||||
if (remoteLocation) {
|
||||
this.http.get<Build[]>(remoteLocation, {params: params})
|
||||
.subscribe(builds => {
|
||||
for (const build of builds) {
|
||||
/* Fix incorect url for post_failure job */
|
||||
/* TODO(mordred) Maybe let's fix this server side? */
|
||||
if (build.log_url === build.job_name) {
|
||||
build.log_url = undefined
|
||||
}
|
||||
}
|
||||
this.builds = builds
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getRowClass(build: Build): string {
|
||||
if (build.result === 'SUCCESS') {
|
||||
return 'success'
|
||||
} else {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title id='pagetitle'><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<zuul-dashboard></zuul-dashboard>
|
||||
</body></html>
|
@ -1,178 +0,0 @@
|
||||
{
|
||||
"last_reconfigured": 1389381756000,
|
||||
"message": "Example info message",
|
||||
"pipelines": [
|
||||
{
|
||||
"name": "test",
|
||||
"description": "Lint and unit tests",
|
||||
"change_queues": [
|
||||
{
|
||||
"name": "some-jobs@worker301.ci-example.org",
|
||||
"heads": [
|
||||
[
|
||||
{
|
||||
"id": "10101,1",
|
||||
"url": "#!/review.example.org/r/10101",
|
||||
"project": "openstack/infra/zuul",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "zuul-merge",
|
||||
"url": "#!/jenkins.example.org/job/zuul-merge/201",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-lint",
|
||||
"url": "#!/jenkins.example.org/job/zuul-lint/201",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-test",
|
||||
"url": "#!/jenkins.example.org/job/zuul-test/201",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": "10103,1",
|
||||
"url": "#!/review.example.org/r/10103",
|
||||
"project": "google/gerrit",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "gerrit-merge",
|
||||
"url": "#!/jenkins.example.org/job/gerrit-merge/203",
|
||||
"result": "SUCCESS",
|
||||
"voting": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "other-jobs@worker301.ci-example.org",
|
||||
"heads": [
|
||||
[
|
||||
{
|
||||
"id": "10102,1",
|
||||
"url": "#!/review.example.org/r/10102",
|
||||
"project": "google/gerrit",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "gerrit-merge",
|
||||
"url": "#!/jenkins.example.org/job/gerrit-merge/202",
|
||||
"result": "UNSTABLE",
|
||||
"voting": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": "10104,1",
|
||||
"url": "#!/review.example.org/r/10104",
|
||||
"project": "openstack/infra/zuul",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "zuul-merge",
|
||||
"url": "#!/jenkins.example.org/job/zuul-merge/204",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-lint",
|
||||
"url": "#!/jenkins.example.org/job/zuul-lint/204",
|
||||
"result": "FAILURE",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-test",
|
||||
"url": "#!/jenkins.example.org/job/zuul-test/204",
|
||||
"result": null,
|
||||
"voting": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gate-and-submit",
|
||||
"change_queues": []
|
||||
},
|
||||
{
|
||||
"name": "postmerge",
|
||||
"change_queues": [
|
||||
{
|
||||
"name": "some-jobs@worker301.ci-example.org",
|
||||
"heads": [
|
||||
[
|
||||
{
|
||||
"id": "7f1d65cb0f663c907698f915da01c008c7ef4748",
|
||||
"url": "#!/review.example.org/r/10100",
|
||||
"project": "openstack/infra/zuul",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "zuul-lint",
|
||||
"url": "#!/jenkins.example.org/job/zuul-lint/200",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-test",
|
||||
"url": "#!/jenkins.example.org/job/zuul-test/200",
|
||||
"result": "FAILURE",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-regression-python2",
|
||||
"url": "#!/jenkins.example.org/job/zuul-regression-python2/200",
|
||||
"result": "SUCCESS",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-regression-python3",
|
||||
"url": "#!/jenkins.example.org/job/zuul-regression-python3/200",
|
||||
"result": "FAILURE",
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-performance-python2",
|
||||
"url": null,
|
||||
"result": null,
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-performance-python3",
|
||||
"url": null,
|
||||
"result": null,
|
||||
"voting": true
|
||||
},
|
||||
{
|
||||
"name": "zuul-docs-publish",
|
||||
"url": null,
|
||||
"result": null,
|
||||
"voting": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"trigger_event_queue": {
|
||||
"length": 0
|
||||
},
|
||||
"result_event_queue": {
|
||||
"length": 0
|
||||
},
|
||||
"zuul_version": "2.0.0.19"
|
||||
}
|
@ -1,312 +0,0 @@
|
||||
{
|
||||
"last_reconfigured": 1389381756000,
|
||||
"pipelines": [
|
||||
{
|
||||
"name": "check",
|
||||
"description": "Newly uploaded patchsets enter this pipeline to receive an initial +/-1 Verified vote from Jenkins.",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/tripleo-image-elements"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Changes that have been approved by core developers are enqueued in order in this pipeline, and .",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-detackforge/reddwarf-integration"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/moniker"
|
||||
},
|
||||
{
|
||||
"heads": [
|
||||
[
|
||||
{
|
||||
"url": "https://review.openstack.org/26292",
|
||||
"project": "openstack/quantum",
|
||||
"jobs": [
|
||||
{
|
||||
"url": "https://jenkins.openstack.org/job/gate-quantum-docs/5501/consoleFull",
|
||||
"voting": true,
|
||||
"result": "SUCCESS",
|
||||
"name": "gate-quantum-docs"
|
||||
},
|
||||
{
|
||||
"url": "https://jenkins.openstack.org/job/gate-quantum-pep8/6254/consoleFull",
|
||||
"voting": true,
|
||||
"result": "SUCCESS",
|
||||
"name": "gate-quantum-pep8"
|
||||
},
|
||||
{
|
||||
"url": "https://jenkins.openstack.org/job/gate-quantum-python26/5876/",
|
||||
"voting": true,
|
||||
"result": null,
|
||||
"name": "gate-quantum-python26"
|
||||
},
|
||||
{
|
||||
"url": "https://jenkins.openstack.org/job/gate-quantum-python27/5887/",
|
||||
"voting": true,
|
||||
"result": null,
|
||||
"name": "gate-quantum-python27"
|
||||
},
|
||||
{
|
||||
"url": "https://jenkins.openstack.org/job/gate-tempest-devstack-vm-quantum/17548/",
|
||||
"voting": true,
|
||||
"result": null,
|
||||
"name": "gate-tempest-devstack-vm-quantum"
|
||||
}
|
||||
],
|
||||
"id": "26292,1"
|
||||
}
|
||||
]
|
||||
],
|
||||
"name": "openstack-dev/devstack, openstack-infra/devstack-gate, openstack/cinder, openstack/glance, openstack/horizon, openstack/keystone, openstack/nova, openstack/python-cinderclient, openstack/python-glanceclient, openstack/python-keystoneclient, openstack/python-novaclient, openstack/python-quantumclient, openstack/quantum, openstack/swift, openstack/tempest, z/tempest"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/ceilometer"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-openstack"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-cinder"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/marconi"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/config"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/tripleo-image-elements"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/kwapi"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/python-reddwarfclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/python-savannaclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/python-monikerclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/packstack"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/oslo.config"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/jenkins-job-builder"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-horizon"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/heat-cfntools"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/oslo-incubator"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/os-config-applier"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/requirements"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-glance"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/gearman-plugin"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-keystone"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-nova"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/climate"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/python-swiftclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/python-ceilometerclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/git-review"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/bufunfa"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/puppet-swift"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/statusbot"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/openstack-planet"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/python-openstackclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/diskimage-builder"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/gerritlib"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/zuul"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/reddwarf"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-dev/hacking"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/python-heatclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/python-libraclient"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/reviewday"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/jeepyb"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/heat"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/libra"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/gerrit"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/healthnmon"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-infra/gerritbot"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-dev/pbr"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "stackforge/savanna"
|
||||
},
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack/openstack-manuals"
|
||||
}
|
||||
],
|
||||
"name": "gate"
|
||||
},
|
||||
{
|
||||
"description": "This pipeline runs jobs that operate after each change is merged.",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-dev/hacking, openstack-dev/openstack-qa, openstack-dev/pbr, openstack-infra/config, openstack-infra/gearman-plugin, openstack-infra/gerrit, openstack-infra/gerritbot, openstack-infra/git-review, openstack-infra/jenkins-job-builder, openstack-infra/nose-html-output, openstack-infra/reviewday, openstack-infra/statusbot, openstack-infra/zuul, openstack/api-site, openstack/ceilometer, openstack/cinder, openstack/compute-api, openstack/glance, openstack/heat, openstack/heat-cfntools, openstack/horizon, openstack/identity-api, openstack/image-api, openstack/keystone, openstack/netconn-api, openstack/nova, openstack/object-api, openstack/openstack-manuals, openstack/oslo-incubator, openstack/oslo.config, openstack/python-ceilometerclient, openstack/python-cinderclient, openstack/python-glanceclient, openstack/python-heatclient, openstack/python-keystoneclient, openstack/python-novaclient, openstack/python-openstackclient, openstack/python-quantumclient, openstack/python-swiftclient, openstack/quantum, openstack/requirements, openstack/swift, openstack/volume-api, stackforge/bufunfa, stackforge/diskimage-builder, stackforge/moniker, stackforge/os-config-applier, stackforge/python-monikerclient, stackforge/python-savannaclient, stackforge/reddwarf, stackforge/savanna, stackforge/tripleo-image-elements"
|
||||
}
|
||||
],
|
||||
"name": "post"
|
||||
},
|
||||
{
|
||||
"description": "This pipeline runs jobs on projects in response to pre-release tags.",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-dev/hacking, openstack-dev/pbr, openstack-infra/gerritbot, openstack-infra/gerritlib, openstack-infra/git-review, openstack-infra/jeepyb, openstack-infra/jenkins-job-builder, openstack-infra/nose-html-output, openstack-infra/reviewday, openstack-infra/statusbot, openstack-infra/zuul, openstack/ceilometer, openstack/cinder, openstack/glance, openstack/heat, openstack/heat-cfntools, openstack/horizon, openstack/keystone, openstack/nova, openstack/oslo.config, openstack/python-ceilometerclient, openstack/python-cinderclient, openstack/python-glanceclient, openstack/python-heatclient, openstack/python-keystoneclient, openstack/python-novaclient, openstack/python-openstackclient, openstack/python-quantumclient, openstack/python-swiftclient, openstack/quantum, openstack/swift, stackforge/moniker, stackforge/python-monikerclient, stackforge/python-reddwarfclient, stackforge/python-savannaclient, stackforge/savanna"
|
||||
}
|
||||
],
|
||||
"name": "pre-release"
|
||||
},
|
||||
{
|
||||
"description": "When a commit is tagged as a release, this pipeline runs jobs that publish archives and documentation.",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": "openstack-dev/hacking, openstack-dev/openstack-qa, openstack-dev/pbr, openstack-infra/gerritbot, openstack-infra/gerritlib, openstack-infra/git-review, openstack-infra/jeepyb, openstack-infra/jenkins-job-builder, openstack-infra/nose-html-output, openstack-infra/reviewday, openstack-infra/statusbot, openstack-infra/zuul, openstack/ceilometer, openstack/cinder, openstack/glance, openstack/heat, openstack/heat-cfntools, openstack/horizon, openstack/keystone, openstack/nova, openstack/oslo-incubator, openstack/oslo.config, openstack/python-ceilometerclient, openstack/python-cinderclient, openstack/python-glanceclient, openstack/python-heatclient, openstack/python-keystoneclient, openstack/python-novaclient, openstack/python-openstackclient, openstack/python-quantumclient, openstack/python-swiftclient, openstack/quantum, openstack/swift, stackforge/moniker, stackforge/python-monikerclient, stackforge/python-reddwarfclient, stackforge/python-savannaclient, stackforge/savanna"
|
||||
}
|
||||
],
|
||||
"name": "release"
|
||||
},
|
||||
{
|
||||
"description": "This pipeline is used for silently testing new jobs.",
|
||||
"change_queues": [
|
||||
{
|
||||
"heads": [],
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"name": "silent"
|
||||
}
|
||||
],
|
||||
"trigger_event_queue": {
|
||||
"length": 0
|
||||
},
|
||||
"result_event_queue": {
|
||||
"length": 0
|
||||
},
|
||||
"zuul_version": "2.0.0.19"
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
entry: './web/main.ts',
|
||||
resolve: {
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
// path.resolve(__dirname winds up relative to the config dir
|
||||
path: path.resolve(__dirname, '../../zuul/web/static'),
|
||||
publicPath: ''
|
||||
},
|
||||
// Some folks prefer "cheaper" source-map for dev and one that is more
|
||||
// expensive to build for prod. Debugging without the full source-map sucks,
|
||||
// so define it here in common.
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
runtimeChunk: true,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test: /node_modules/,
|
||||
name: 'vendor',
|
||||
chunks: 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
new webpack.ProvidePlugin({
|
||||
$: 'jquery/dist/jquery',
|
||||
jQuery: 'jquery/dist/jquery',
|
||||
}),
|
||||
// Each of the entries below lists a specific 'chunk' which is one of the
|
||||
// entry items from above. We can collapse this to just do one single
|
||||
// output file.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'web/config/main.ejs',
|
||||
title: 'Zuul Status'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'status.html',
|
||||
template: 'web/config/main.ejs',
|
||||
title: 'Zuul Status'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Builds',
|
||||
template: 'web/config/main.ejs',
|
||||
filename: 'builds.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Jobs',
|
||||
template: 'web/config/main.ejs',
|
||||
filename: 'jobs.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Tenants',
|
||||
template: 'web/config/main.ejs',
|
||||
filename: 'tenants.html'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Zuul Console Stream',
|
||||
template: 'web/config/main.ejs',
|
||||
filename: 'stream.html'
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// disable type checker - we will use it in fork plugin
|
||||
transpileOnly: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
'babel-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
// The majority of the rules below are all about getting bootstrap copied
|
||||
// appropriately.
|
||||
{
|
||||
test: /\.woff(2)?(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/font-woff'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: 'image/svg+xml'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: ['html-loader'],
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
mode: 'development',
|
||||
// Enable Hot Module Replacement for devServer
|
||||
devServer: {
|
||||
hot: true,
|
||||
contentBase: path.resolve(__dirname, './zuul/web/static'),
|
||||
publicPath: '/'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// We only need to bundle the demo files when we're running locally
|
||||
new webpack.ProvidePlugin({
|
||||
DemoStatusBasic: '../config/status-basic.json',
|
||||
DemoStatusOpenStack: '../config/status-openstack.json',
|
||||
DemoStatusTree: '../config/status-tree.json'
|
||||
}),
|
||||
]
|
||||
})
|
@ -1,47 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
const BundleAnalyzer = require('webpack-bundle-analyzer');
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'tslint-loader',
|
||||
options: {
|
||||
emitErrors: true,
|
||||
typeCheck: false,
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
'eslint-loader'
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new BundleAnalyzer.BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: '../../../reports/bundle.html',
|
||||
generateStatsFile: true,
|
||||
openAnalyzer: false,
|
||||
statsFilename: '../../../reports/stats.json',
|
||||
}),
|
||||
]
|
||||
})
|
@ -1,32 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const Merge = require('webpack-merge');
|
||||
const CommonConfig = require('./webpack.common.js');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const ArchivePlugin = require('webpack-archive-plugin');
|
||||
|
||||
module.exports = Merge(CommonConfig, {
|
||||
mode: 'production',
|
||||
output: {
|
||||
filename: '[name].[chunkhash].js',
|
||||
// path.resolve(__dirname winds up relative to the config dir
|
||||
path: path.resolve(__dirname, '../../zuul/web/static'),
|
||||
publicPath: ''
|
||||
},
|
||||
optimization: {
|
||||
minimize: true
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(
|
||||
['zuul/web/static'], { root: path.resolve(__dirname, '../..')}),
|
||||
// Keeps the vendor bundle from changing needlessly.
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
new ArchivePlugin({
|
||||
output: path.resolve(__dirname, '../../zuul-web'),
|
||||
format: [
|
||||
'tar',
|
||||
],
|
||||
ext: 'tgz'
|
||||
})
|
||||
]
|
||||
})
|
@ -1,24 +0,0 @@
|
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'
|
||||
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
import { ZuulService } from '../zuul/zuul.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule ],
|
||||
providers: [ ZuulService ]
|
||||
})
|
||||
export class CoreModule {
|
||||
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||
if (parentModule) {
|
||||
throw new Error(
|
||||
'CoreModule is already loaded. Import it in the AppModule only')
|
||||
}
|
||||
}
|
||||
|
||||
static forRoot(config: {}): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: CoreModule,
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 286 B |
@ -1,17 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
export default class JobDetails {
|
||||
source_context: string
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import JobDetails from './details'
|
||||
|
||||
export default class Job {
|
||||
expanded: boolean
|
||||
details: JobDetails
|
||||
name: string
|
||||
|
||||
constructor() {
|
||||
this.expanded = false
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<div class="container-fluid">
|
||||
<table class="table table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Last builds</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let job of jobs">
|
||||
<tr>
|
||||
<td>{{ job.name }}</td>
|
||||
<td>{{ job.description }}</td>
|
||||
<td><a [routerLink]="['../builds.html']"
|
||||
[queryParams]="{job_name: job.name}" target="_self">
|
||||
builds</a></td>
|
||||
</tr>
|
||||
<tr *ngIf="job.expanded">
|
||||
<td colspan="3">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" *ngFor="let detail of job.details">
|
||||
<!-- TODO: make clickable link to cgit files ? -->
|
||||
{{ detail.source_context }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -1,54 +0,0 @@
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
import JobDetails from './details'
|
||||
import Job from './job'
|
||||
|
||||
@Component({
|
||||
template: require('./jobs.component.html')
|
||||
})
|
||||
export default class JobsComponent implements OnInit {
|
||||
|
||||
jobs: Job[]
|
||||
|
||||
constructor(
|
||||
private http: HttpClient, private route: ActivatedRoute,
|
||||
private zuul: ZuulService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
|
||||
this.jobsFetch()
|
||||
}
|
||||
|
||||
jobsFetch(): void {
|
||||
const remoteLocation = this.zuul.getSourceUrl('jobs')
|
||||
if (remoteLocation) {
|
||||
this.http.get<Job[]>(remoteLocation)
|
||||
.subscribe(jobs => this.injestJobs(jobs))
|
||||
}
|
||||
}
|
||||
|
||||
injestJobs(jobs: Job[]): void {
|
||||
for (const job of jobs) {
|
||||
job.expanded = false
|
||||
}
|
||||
this.jobs = jobs
|
||||
}
|
||||
}
|
22
web/main.ts
@ -1,22 +0,0 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import 'zone.js'
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
import { AppModule } from './app.module'
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
@ -1,30 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a *ngIf="zuul.info && !zuul.info.whiteLabel" class="navbar-brand" [routerLink]="dashboardLink" target="_self">Zuul Dashboard</a>
|
||||
<span *ngIf="zuul.info && zuul.info.whiteLabel" class="navbar-brand">Zuul Dashboard</span>
|
||||
</div>
|
||||
<ul class="nav navbar-nav" *ngIf="zuul.info && zuul.info.tenant !== ''">
|
||||
<li routerLinkActive="active" *ngFor="let route of zuul.navbarRoutes">
|
||||
<a [routerLink]="route.url">
|
||||
{{ route.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
@ -1,34 +0,0 @@
|
||||
// Copyright 2018 Red Hat, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { OnInit, Component } from '@angular/core'
|
||||
import { Router, ResolveEnd } from '@angular/router'
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
template: require('./navigation.component.html')
|
||||
})
|
||||
export default class NavigationComponent implements OnInit {
|
||||
dashboardLink: string
|
||||
|
||||
constructor(private router: Router, private zuul: ZuulService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.dashboardLink = '/t/tenants.html'
|
||||
}
|
||||
}
|
40
web/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@zuul-ci/dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "Web Dashboard for Zuul",
|
||||
"repository": "https://git.zuul-ci.org/zuul",
|
||||
"author": "Zuul Developers",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"lodash": "^4.17.10",
|
||||
"patternfly-react": "^2.13.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts": "1.1.4",
|
||||
"redux": "<4.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sockette": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^5.3.0",
|
||||
"eslint-plugin-jest": "^21.21.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"eslint-plugin-standard": "^3.1.0",
|
||||
"yarn": "^1.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start:openstack": "REACT_APP_ZUUL_API='https://zuul.openstack.org/api/' react-scripts start",
|
||||
"start:multi": "REACT_APP_ZUUL_API='https://softwarefactory-project.io/zuul/api/' react-scripts start",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .js --ext .jsx src"
|
||||
}
|
||||
}
|
BIN
web/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
17
web/public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="layout-pf layout-pf-fixed">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<title>Zuul</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
15
web/public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Zuul",
|
||||
"name": "Zuul Dashboard",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "48x48 32x32",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
162
web/src/App.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// The App is the parent component of every pages. Each page content is
|
||||
// rendered by the Route object according to the current location.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { matchPath, withRouter } from 'react-router'
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { Masthead } from 'patternfly-react'
|
||||
|
||||
import logo from './images/logo.png'
|
||||
import { routes } from './routes'
|
||||
import { setTenantAction } from './reducers'
|
||||
|
||||
|
||||
class App extends React.Component {
|
||||
static propTypes = {
|
||||
info: PropTypes.object,
|
||||
tenant: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.menu = routes()
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
const { location } = this.props
|
||||
const activeItem = this.menu.find(
|
||||
item => location.pathname === item.to
|
||||
)
|
||||
return (
|
||||
<ul className='nav navbar-nav navbar-primary'>
|
||||
{this.menu.filter(item => item.title).map(item => (
|
||||
<li key={item.to} className={item === activeItem ? 'active' : ''}>
|
||||
<Link to={this.props.tenant.linkPrefix + item.to}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { tenant } = this.props
|
||||
const allRoutes = []
|
||||
this.menu
|
||||
// Do not include '/tenants' route in white-label setup
|
||||
.filter(item =>
|
||||
(tenant.whiteLabel && !item.globalRoute) || !tenant.whiteLabel)
|
||||
.forEach((item, index) => {
|
||||
allRoutes.push(
|
||||
<Route
|
||||
key={index}
|
||||
path={item.globalRoute ? item.to : tenant.routePrefix + item.to}
|
||||
component={item.component}
|
||||
exact
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Switch>
|
||||
{allRoutes}
|
||||
<Redirect from='*' to={tenant.defaultRoute} key='default-route' />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// This method is called when info property is updated
|
||||
const { tenant, info } = this.props
|
||||
if (info.capabilities) {
|
||||
let tenantName, whiteLabel
|
||||
|
||||
if (info.tenant) {
|
||||
// White label
|
||||
whiteLabel = true
|
||||
tenantName = info.tenant
|
||||
} else if (!info.tenant) {
|
||||
// Multi tenant, look for tenant name in url
|
||||
whiteLabel = false
|
||||
|
||||
const match = matchPath(
|
||||
this.props.location.pathname, {path: '/t/:tenant'})
|
||||
|
||||
if (match) {
|
||||
tenantName = match.params.tenant
|
||||
} else {
|
||||
tenantName = ''
|
||||
}
|
||||
}
|
||||
// Set tenant only if it changed to prevent DidUpdate loop
|
||||
if (typeof tenant.name === 'undefined' || tenant.name !== tenantName) {
|
||||
this.props.dispatch(setTenantAction(tenantName, whiteLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tenant } = this.props
|
||||
if (typeof tenant.name === 'undefined') {
|
||||
return (<h2>Loading...</h2>)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Masthead
|
||||
iconImg={logo}
|
||||
navToggle
|
||||
thin
|
||||
>
|
||||
<div className='collapse navbar-collapse'>
|
||||
{tenant.name && this.renderMenu()}
|
||||
<ul className='nav navbar-nav navbar-utility'>
|
||||
<li>
|
||||
<a href='https://zuul-ci.org/docs'
|
||||
rel='noopener noreferrer' target='_blank'>
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
{tenant.name && (
|
||||
<li>
|
||||
<Link to={tenant.defaultRoute}>
|
||||
<strong>Tenant</strong> {tenant.name}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Masthead>
|
||||
<div className='container-fluid container-cards-pf'>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// This connect the info state from the store to the info property of the App.
|
||||
export default withRouter(connect(
|
||||
state => ({
|
||||
info: state.info,
|
||||
tenant: state.tenant
|
||||
})
|
||||
)(App))
|
102
web/src/App.test.jsx
Normal file
@ -0,0 +1,102 @@
|
||||
/* global Promise, expect, jest, it, location */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-dom/test-utils'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Link, BrowserRouter as Router } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import { createZuulStore, fetchInfoAction } from './reducers'
|
||||
import App from './App'
|
||||
import TenantsPage from './pages/Tenants'
|
||||
import StatusPage from './pages/Status'
|
||||
import * as api from './api'
|
||||
|
||||
api.fetchInfo = jest.fn()
|
||||
api.fetchTenants = jest.fn()
|
||||
api.fetchStatus = jest.fn()
|
||||
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div')
|
||||
const store = createZuulStore()
|
||||
ReactDOM.render(<Provider store={store}><Router><App /></Router></Provider>,
|
||||
div)
|
||||
ReactDOM.unmountComponentAtNode(div)
|
||||
})
|
||||
|
||||
it('renders multi tenant', () => {
|
||||
api.fetchInfo.mockImplementation(
|
||||
() => Promise.resolve({data: {
|
||||
info: {capabilities: {}}
|
||||
}})
|
||||
)
|
||||
api.fetchTenants.mockImplementation(
|
||||
() => Promise.resolve({data: [{name: 'openstack'}]})
|
||||
)
|
||||
const store = createZuulStore()
|
||||
const application = ReactTestUtils.renderIntoDocument(
|
||||
<Provider store={store}><Router><App /></Router></Provider>
|
||||
)
|
||||
store.dispatch(fetchInfoAction()).then(() => {
|
||||
// Link should be tenant scoped
|
||||
const topMenuLinks = ReactTestUtils.scryRenderedComponentsWithType(
|
||||
application, Link)
|
||||
expect(topMenuLinks[0].props.to).toEqual('/t/openstack/status')
|
||||
expect(topMenuLinks[1].props.to).toEqual('/t/openstack/jobs')
|
||||
// Location should be /tenants
|
||||
expect(location.pathname).toEqual('/tenants')
|
||||
// Info should tell multi tenants
|
||||
expect(store.getState().info.tenant).toEqual(undefined)
|
||||
// Tenants list has been rendered
|
||||
expect(ReactTestUtils.findRenderedComponentWithType(
|
||||
application, TenantsPage)).not.toEqual(null)
|
||||
// Fetch tenants has been called
|
||||
expect(api.fetchTenants).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders single tenant', () => {
|
||||
api.fetchInfo.mockImplementation(
|
||||
() => Promise.resolve({data: {
|
||||
info: {capabilities: {}, tenant: 'openstack'}
|
||||
}})
|
||||
)
|
||||
api.fetchStatus.mockImplementation(
|
||||
() => Promise.resolve({data: {pipelines: []}})
|
||||
)
|
||||
const store = createZuulStore()
|
||||
const application = ReactTestUtils.renderIntoDocument(
|
||||
<Provider store={store}><Router><App /></Router></Provider>
|
||||
)
|
||||
|
||||
store.dispatch(fetchInfoAction()).then(() => {
|
||||
// Link should be white-label scoped
|
||||
const topMenuLinks = ReactTestUtils.scryRenderedComponentsWithType(
|
||||
application, Link)
|
||||
expect(topMenuLinks[0].props.to).toEqual('/status')
|
||||
expect(topMenuLinks[1].props.to).toEqual('/jobs')
|
||||
// Location should be /status
|
||||
expect(location.pathname).toEqual('/status')
|
||||
// Info should tell white label tenant openstack
|
||||
expect(store.getState().info.tenant).toEqual('openstack')
|
||||
// Status page has been rendered
|
||||
expect(ReactTestUtils.findRenderedComponentWithType(
|
||||
application, StatusPage)).not.toEqual(null)
|
||||
// Fetch status has been called
|
||||
expect(api.fetchStatus).toBeCalled()
|
||||
})
|
||||
})
|
133
web/src/api.js
Normal file
@ -0,0 +1,133 @@
|
||||
/* global process, window */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Axios from 'axios'
|
||||
|
||||
function getHomepageUrl (url) {
|
||||
//
|
||||
// Discover serving location from href.
|
||||
//
|
||||
// This is only needed for sub-directory serving.
|
||||
// Serving the application from '/' may simply default to '/'
|
||||
//
|
||||
// Note that this is not enough for sub-directory serving,
|
||||
// The static files location also needs to be adapted with the 'homepage'
|
||||
// settings of the package.json file.
|
||||
//
|
||||
// This homepage url is used for the Router and Link resolution logic
|
||||
//
|
||||
let baseUrl
|
||||
if (url) {
|
||||
baseUrl = url
|
||||
} else {
|
||||
baseUrl = window.location.href
|
||||
}
|
||||
// Get dirname of the current url
|
||||
baseUrl = baseUrl.replace(/\\/g, '/').replace(/\/[^/]*$/, '/')
|
||||
|
||||
// Remove any query strings
|
||||
if (baseUrl.includes('?')) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.lastIndexOf('?'))
|
||||
}
|
||||
// Remove any hash anchor
|
||||
if (baseUrl.includes('/#')) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.lastIndexOf('/#') + 1)
|
||||
}
|
||||
|
||||
// Remove known sub-path
|
||||
const subDir = [
|
||||
'/build/',
|
||||
'/job/',
|
||||
'/project/',
|
||||
'/stream/',
|
||||
]
|
||||
subDir.forEach(path => {
|
||||
if (baseUrl.includes(path)) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.lastIndexOf(path) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove tenant scope
|
||||
if (baseUrl.includes('/t/')) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.lastIndexOf('/t/') + 1)
|
||||
}
|
||||
if (! baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl + '/'
|
||||
}
|
||||
// console.log('Homepage url is ', baseUrl)
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
function getZuulUrl () {
|
||||
// Return the zuul root api absolute url
|
||||
const ZUUL_API = process.env.REACT_APP_ZUUL_API
|
||||
let apiUrl
|
||||
|
||||
if (ZUUL_API) {
|
||||
// Api url set at build time, use it
|
||||
apiUrl = ZUUL_API
|
||||
} else {
|
||||
// Api url is relative to homepage path
|
||||
apiUrl = getHomepageUrl () + 'api/'
|
||||
}
|
||||
if (! apiUrl.endsWith('/')) {
|
||||
apiUrl = apiUrl + '/'
|
||||
}
|
||||
if (! apiUrl.endsWith('/api/')) {
|
||||
apiUrl = apiUrl + 'api/'
|
||||
}
|
||||
// console.log('Api url is ', apiUrl)
|
||||
return apiUrl
|
||||
}
|
||||
const apiUrl = getZuulUrl()
|
||||
|
||||
|
||||
function getStreamUrl (apiPrefix) {
|
||||
const streamUrl = (apiUrl + apiPrefix)
|
||||
.replace(/(http)(s)?:\/\//, 'ws$2://') + 'console-stream'
|
||||
// console.log('Stream url is ', streamUrl)
|
||||
return streamUrl
|
||||
}
|
||||
|
||||
// Direct APIs
|
||||
function fetchInfo () {
|
||||
return Axios.get(apiUrl + 'info')
|
||||
}
|
||||
function fetchTenants () {
|
||||
return Axios.get(apiUrl + 'tenants')
|
||||
}
|
||||
function fetchStatus (apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'status')
|
||||
}
|
||||
function fetchBuilds (apiPrefix, queryString) {
|
||||
let path = 'builds'
|
||||
if (queryString) {
|
||||
path += '?' + queryString.slice(1)
|
||||
}
|
||||
return Axios.get(apiUrl + apiPrefix + path)
|
||||
}
|
||||
function fetchJobs (apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'jobs')
|
||||
}
|
||||
|
||||
export {
|
||||
getHomepageUrl,
|
||||
getStreamUrl,
|
||||
fetchStatus,
|
||||
fetchBuilds,
|
||||
fetchJobs,
|
||||
fetchTenants,
|
||||
fetchInfo
|
||||
}
|
237
web/src/containers/TableFilters.jsx
Normal file
@ -0,0 +1,237 @@
|
||||
/* global URLSearchParams */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Boiler plate code to manage table filtering
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Filter, FormControl, Toolbar } from 'patternfly-react'
|
||||
|
||||
|
||||
class TableFilters extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object
|
||||
}
|
||||
|
||||
getFilterFromUrl = () => {
|
||||
const urlParams = new URLSearchParams(this.props.location.search)
|
||||
let activeFilters = []
|
||||
this.filterTypes.forEach(item => {
|
||||
urlParams.getAll(item.id).forEach(param => {
|
||||
activeFilters.push({
|
||||
label: item.title + ': ' + param,
|
||||
key: item.id,
|
||||
value: param})
|
||||
})
|
||||
})
|
||||
this.setState({activeFilters: activeFilters})
|
||||
return activeFilters
|
||||
}
|
||||
|
||||
updateUrl (activeFilters) {
|
||||
let path = this.props.location.pathname
|
||||
if (activeFilters.length > 0) {
|
||||
path += '?'
|
||||
activeFilters.forEach((item, idx) => {
|
||||
if (idx > 0) {
|
||||
path += '&'
|
||||
}
|
||||
path += (
|
||||
encodeURIComponent(item.key)
|
||||
+ '=' +
|
||||
encodeURIComponent(item.value)
|
||||
)
|
||||
})
|
||||
}
|
||||
window.history.pushState({path: path}, '', path)
|
||||
}
|
||||
|
||||
filterAdded = (field, value) => {
|
||||
let filterText = ''
|
||||
if (field.title) {
|
||||
filterText = field.title
|
||||
} else {
|
||||
filterText = field
|
||||
}
|
||||
filterText += ': '
|
||||
|
||||
if (value.filterCategory) {
|
||||
filterText +=
|
||||
(value.filterCategory.title || value.filterCategory) +
|
||||
'-' +
|
||||
(value.filterValue.title || value.filterValue)
|
||||
} else if (value.title) {
|
||||
filterText += value.title
|
||||
} else {
|
||||
filterText += value
|
||||
}
|
||||
|
||||
let activeFilters = [...this.state.activeFilters, {
|
||||
label: filterText,
|
||||
key: field.id,
|
||||
value: value
|
||||
}]
|
||||
this.setState({ activeFilters: activeFilters })
|
||||
this.updateData(activeFilters)
|
||||
this.updateUrl(activeFilters)
|
||||
}
|
||||
|
||||
selectFilterType = filterType => {
|
||||
const { currentFilterType } = this.state
|
||||
if (currentFilterType !== filterType) {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
currentValue: '',
|
||||
currentFilterType: filterType,
|
||||
filterCategory:
|
||||
filterType.filterType === 'complex-select'
|
||||
? undefined
|
||||
: prevState.filterCategory,
|
||||
categoryValue:
|
||||
filterType.filterType === 'complex-select'
|
||||
? ''
|
||||
: prevState.categoryValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
filterValueSelected = filterValue => {
|
||||
const { currentFilterType, currentValue } = this.state
|
||||
|
||||
if (filterValue !== currentValue) {
|
||||
this.setState({ currentValue: filterValue })
|
||||
if (filterValue) {
|
||||
this.filterAdded(currentFilterType, filterValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterCategorySelected = category => {
|
||||
const { filterCategory } = this.state
|
||||
if (filterCategory !== category) {
|
||||
this.setState({ filterCategory: category, currentValue: '' })
|
||||
}
|
||||
}
|
||||
|
||||
categoryValueSelected = value => {
|
||||
const { currentValue, currentFilterType, filterCategory } = this.state
|
||||
|
||||
if (filterCategory && currentValue !== value) {
|
||||
this.setState({ currentValue: value })
|
||||
if (value) {
|
||||
let filterValue = {
|
||||
filterCategory: filterCategory,
|
||||
filterValue: value
|
||||
}
|
||||
this.filterAdded(currentFilterType, filterValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentValue = event => {
|
||||
this.setState({ currentValue: event.target.value })
|
||||
}
|
||||
|
||||
onValueKeyPress = keyEvent => {
|
||||
const { currentValue, currentFilterType } = this.state
|
||||
|
||||
if (keyEvent.key === 'Enter' && currentValue && currentValue.length > 0) {
|
||||
this.setState({ currentValue: '' })
|
||||
this.filterAdded(currentFilterType, currentValue)
|
||||
keyEvent.stopPropagation()
|
||||
keyEvent.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
removeFilter = filter => {
|
||||
const { activeFilters } = this.state
|
||||
|
||||
let index = activeFilters.indexOf(filter)
|
||||
if (index > -1) {
|
||||
let updated = [
|
||||
...activeFilters.slice(0, index),
|
||||
...activeFilters.slice(index + 1)
|
||||
]
|
||||
this.setState({ activeFilters: updated })
|
||||
this.updateData(updated)
|
||||
this.updateUrl(updated)
|
||||
}
|
||||
}
|
||||
|
||||
clearFilters = () => {
|
||||
this.setState({ activeFilters: [] })
|
||||
this.updateData()
|
||||
this.updateUrl([])
|
||||
}
|
||||
|
||||
renderFilterInput() {
|
||||
const { currentFilterType, currentValue } = this.state
|
||||
if (!currentFilterType) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<FormControl
|
||||
type={currentFilterType.filterType}
|
||||
value={currentValue}
|
||||
placeholder={currentFilterType.placeholder}
|
||||
onChange={e => this.updateCurrentValue(e)}
|
||||
onKeyPress={e => this.onValueKeyPress(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderFilter = () => {
|
||||
const { currentFilterType, activeFilters } = this.state
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ width: 300 }}>
|
||||
<Filter>
|
||||
<Filter.TypeSelector
|
||||
filterTypes={this.filterTypes}
|
||||
currentFilterType={currentFilterType}
|
||||
onFilterTypeSelected={this.selectFilterType}
|
||||
/>
|
||||
{this.renderFilterInput()}
|
||||
</Filter>
|
||||
</div>
|
||||
{activeFilters && activeFilters.length > 0 && (
|
||||
<Toolbar.Results>
|
||||
<Filter.ActiveLabel>{'Active Filters:'}</Filter.ActiveLabel>
|
||||
<Filter.List>
|
||||
{activeFilters.map((item, index) => {
|
||||
return (
|
||||
<Filter.Item
|
||||
key={index}
|
||||
onRemove={this.removeFilter}
|
||||
filterData={item}
|
||||
>
|
||||
{item.label}
|
||||
</Filter.Item>
|
||||
)
|
||||
})}
|
||||
</Filter.List>
|
||||
<Button onClick={e => {
|
||||
e.preventDefault()
|
||||
this.clearFilters()
|
||||
}}>Clear All Filters</Button>
|
||||
</Toolbar.Results>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TableFilters
|
99
web/src/containers/status/Change.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import LineAngleImage from '../../images/line-angle.png'
|
||||
import LineTImage from '../../images/line-t.png'
|
||||
import ChangePanel from './ChangePanel'
|
||||
|
||||
|
||||
class Change extends React.Component {
|
||||
static propTypes = {
|
||||
change: PropTypes.object.isRequired,
|
||||
queue: PropTypes.object.isRequired,
|
||||
expanded: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
renderStatusIcon (change) {
|
||||
let iconGlyph = 'pficon pficon-ok'
|
||||
let iconTitle = 'Succeeding'
|
||||
if (change.active !== true) {
|
||||
iconGlyph = 'pficon pficon-pending'
|
||||
iconTitle = 'Waiting until closer to head of queue to' +
|
||||
' start jobs'
|
||||
} else if (change.live !== true) {
|
||||
iconGlyph = 'pficon pficon-info'
|
||||
iconTitle = 'Dependent change required for testing'
|
||||
} else if (change.failing_reasons &&
|
||||
change.failing_reasons.length > 0) {
|
||||
let reason = change.failing_reasons.join(', ')
|
||||
iconTitle = 'Failing because ' + reason
|
||||
if (reason.match(/merge conflict/)) {
|
||||
iconGlyph = 'pficon pficon-error-circle-o zuul-build-merge-conflict'
|
||||
} else {
|
||||
iconGlyph = 'pficon pficon-error-circle-o'
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span className={'zuul-build-status ' + iconGlyph}
|
||||
title={iconTitle} />
|
||||
)
|
||||
}
|
||||
|
||||
renderLineImg (change, i) {
|
||||
let image = LineTImage
|
||||
if (change._tree_branches.indexOf(i) === change._tree_branches.length - 1) {
|
||||
// Angle line
|
||||
image = LineAngleImage
|
||||
}
|
||||
return <img alt="Line" src={image} style={{verticalAlign: 'baseline'}} />
|
||||
}
|
||||
|
||||
render () {
|
||||
const { change, queue, expanded } = this.props
|
||||
let row = []
|
||||
let i
|
||||
for (i = 0; i < queue._tree_columns; i++) {
|
||||
let className = ''
|
||||
if (i < change._tree.length && change._tree[i] !== null) {
|
||||
className = ' zuul-change-row-line'
|
||||
}
|
||||
row.push(
|
||||
<td key={i} className={'zuul-change-row' + className}>
|
||||
{i === change._tree_index ? this.renderStatusIcon(change) : ''}
|
||||
{change._tree_branches.indexOf(i) !== -1 ? (
|
||||
this.renderLineImg(change, i)) : ''}
|
||||
</td>)
|
||||
}
|
||||
let changeWidth = 360 - 16 * queue._tree_columns
|
||||
row.push(
|
||||
<td key={i + 1}
|
||||
className="zuul-change-cell"
|
||||
style={{width: changeWidth + 'px'}}>
|
||||
<ChangePanel change={change} globalExpanded={expanded} />
|
||||
</td>
|
||||
)
|
||||
return (
|
||||
<table className="zuul-change-box" style={{boxSizing: 'content-box'}}>
|
||||
<tbody>
|
||||
<tr>{row}</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Change
|
317
web/src/containers/status/ChangePanel.jsx
Normal file
@ -0,0 +1,317 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
|
||||
class ChangePanel extends React.Component {
|
||||
static propTypes = {
|
||||
globalExpanded: PropTypes.bool.isRequired,
|
||||
change: PropTypes.object.isRequired,
|
||||
tenant: PropTypes.object
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
expanded: false
|
||||
}
|
||||
this.onClick = this.onClick.bind(this)
|
||||
this.clicked = false
|
||||
}
|
||||
|
||||
onClick () {
|
||||
let expanded = this.state.expanded
|
||||
if (!this.clicked) {
|
||||
expanded = this.props.globalExpanded
|
||||
}
|
||||
this.clicked = true
|
||||
this.setState({ expanded: !expanded })
|
||||
}
|
||||
|
||||
time (ms, words) {
|
||||
if (typeof (words) === 'undefined') {
|
||||
words = false
|
||||
}
|
||||
let seconds = (+ms) / 1000
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = Math.floor(minutes / 60)
|
||||
seconds = Math.floor(seconds % 60)
|
||||
minutes = Math.floor(minutes % 60)
|
||||
let r = ''
|
||||
if (words) {
|
||||
if (hours) {
|
||||
r += hours
|
||||
r += ' hr '
|
||||
}
|
||||
r += minutes + ' min'
|
||||
} else {
|
||||
if (hours < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += hours + ':'
|
||||
if (minutes < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += minutes + ':'
|
||||
if (seconds < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += seconds
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
enqueueTime (ms) {
|
||||
// Special format case for enqueue time to add style
|
||||
let hours = 60 * 60 * 1000
|
||||
let now = Date.now()
|
||||
let delta = now - ms
|
||||
let status = 'text-success'
|
||||
let text = this.time(delta, true)
|
||||
if (delta > (4 * hours)) {
|
||||
status = 'text-danger'
|
||||
} else if (delta > (2 * hours)) {
|
||||
status = 'text-warning'
|
||||
}
|
||||
return <span className={status}>{text}</span>
|
||||
}
|
||||
|
||||
renderChangeLink (change) {
|
||||
let changeId = change.id || 'NA'
|
||||
let changeTitle = changeId
|
||||
let changeText = ''
|
||||
if (change.url !== null) {
|
||||
let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
|
||||
if (githubId) {
|
||||
changeTitle = githubId
|
||||
changeText = '#' + githubId[1]
|
||||
} else if (/^[0-9a-f]{40}$/.test(changeId)) {
|
||||
changeText = changeId.slice(0, 7)
|
||||
}
|
||||
} else if (changeId.length === 40) {
|
||||
changeText = changeId.slice(0, 7)
|
||||
}
|
||||
return (
|
||||
<small>
|
||||
<a href={change.url}>
|
||||
{changeText !== '' ? (
|
||||
<abbr title={changeTitle}>{changeText}</abbr>) : changeTitle}
|
||||
</a>
|
||||
</small>)
|
||||
}
|
||||
|
||||
renderProgressBar (change) {
|
||||
let jobPercent = Math.floor(100 / change.jobs.length)
|
||||
return (
|
||||
<div className='progress zuul-change-total-result'>
|
||||
{change.jobs.map((job, idx) => {
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued'
|
||||
}
|
||||
if (result !== 'queued') {
|
||||
let className = ''
|
||||
switch (result) {
|
||||
case 'success':
|
||||
className = ' progress-bar-success'
|
||||
break
|
||||
case 'lost':
|
||||
case 'failure':
|
||||
className = ' progress-bar-danger'
|
||||
break
|
||||
case 'unstable':
|
||||
className = ' progress-bar-warning'
|
||||
break
|
||||
case 'in progress':
|
||||
case 'queued':
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return <div className={'progress-bar' + className}
|
||||
key={idx}
|
||||
title={job.name}
|
||||
style={{width: jobPercent + '%'}}/>
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderTimer (change) {
|
||||
let remainingTime
|
||||
if (change.remaining_time === null) {
|
||||
remainingTime = 'unknown'
|
||||
} else {
|
||||
remainingTime = this.time(change.remaining_time, true)
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<small title='Remaining Time' className='time'>
|
||||
{remainingTime}
|
||||
</small>
|
||||
<br />
|
||||
<small title='Elapsed Time' className='time'>
|
||||
{this.enqueueTime(change.enqueue_time)}
|
||||
</small>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
renderJobProgressBar (elapsedTime, remainingTime) {
|
||||
let progressPercent = 100 * (elapsedTime / (elapsedTime +
|
||||
remainingTime))
|
||||
return (
|
||||
<div className='progress zuul-job-result'>
|
||||
<div className='progress-bar'
|
||||
role='progressbar'
|
||||
aria-valuenow={progressPercent}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
style={{'width': progressPercent + '%'}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderJobStatusLabel (result) {
|
||||
let className
|
||||
switch (result) {
|
||||
case 'success':
|
||||
className = 'label-success'
|
||||
break
|
||||
case 'failure':
|
||||
className = 'label-danger'
|
||||
break
|
||||
case 'unstable':
|
||||
className = 'label-warning'
|
||||
break
|
||||
case 'skipped':
|
||||
className = 'label-info'
|
||||
break
|
||||
// 'in progress' 'queued' 'lost' 'aborted' ...
|
||||
default:
|
||||
className = 'label-default'
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={'zuul-job-result label ' + className}>{result}</span>
|
||||
)
|
||||
}
|
||||
|
||||
renderJob (job) {
|
||||
const { tenant } = this.props
|
||||
let name = ''
|
||||
if (job.result !== null) {
|
||||
name = <a className='zuul-job-name' href={job.report_url}>{job.name}</a>
|
||||
} else if (job.url !== null) {
|
||||
let url = job.url
|
||||
if (job.url.match('stream.html')) {
|
||||
const buildUuid = job.url.split('?')[1].split('&')[0].split('=')[1]
|
||||
const to = (
|
||||
tenant.linkPrefix + '/stream/' + buildUuid + '?logfile=console.log'
|
||||
)
|
||||
name = <Link to={to}>{job.name}</Link>
|
||||
} else {
|
||||
name = <a className='zuul-job-name' href={url}>{job.name}</a>
|
||||
}
|
||||
} else {
|
||||
name = <span className='zuul-job-name'>{job.name}</span>
|
||||
}
|
||||
let resultBar
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
if (job.url === null) {
|
||||
result = 'queued'
|
||||
} else if (job.paused !== null && job.paused) {
|
||||
result = 'paused'
|
||||
} else {
|
||||
result = 'in progress'
|
||||
}
|
||||
}
|
||||
if (result === 'in progress') {
|
||||
resultBar = this.renderJobProgressBar(
|
||||
job.elapsed_time, job.remaining_time)
|
||||
} else {
|
||||
resultBar = this.renderJobStatusLabel(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{name}
|
||||
{resultBar}
|
||||
{job.voting === false ? (
|
||||
<small className='zuul-non-voting-desc'> (non-voting)</small>) : ''}
|
||||
<div style={{clear: 'both'}} />
|
||||
</span>)
|
||||
}
|
||||
|
||||
renderJobList (jobs) {
|
||||
return (
|
||||
<ul className='list-group zuul-patchset-body'>
|
||||
{jobs.map((job, idx) => (
|
||||
<li key={idx} className='list-group-item zuul-change-job'>
|
||||
{this.renderJob(job)}
|
||||
</li>
|
||||
))}
|
||||
</ul>)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { expanded } = this.state
|
||||
const { change, globalExpanded } = this.props
|
||||
let expand = globalExpanded
|
||||
if (this.clicked) {
|
||||
expand = expanded
|
||||
}
|
||||
const header = (
|
||||
<div className='panel panel-default zuul-change' onClick={this.onClick}>
|
||||
<div className='panel-heading zuul-patchset-header'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-8'>
|
||||
<span className='change_project'>{change.project}</span>
|
||||
<div className='row'>
|
||||
<div className='col-xs-4'>
|
||||
{this.renderChangeLink(change)}
|
||||
</div>
|
||||
<div className='col-xs-8'>
|
||||
{this.renderProgressBar(change)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{change.live === true ? (
|
||||
<div className='col-xs-4 text-right'>
|
||||
{this.renderTimer(change)}
|
||||
</div>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
{expand ? this.renderJobList(change.jobs) : ''}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<React.Fragment>
|
||||
{header}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(ChangePanel)
|
64
web/src/containers/status/ChangePanel.test.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
/* global expect, jest, it */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-dom/test-utils'
|
||||
import { Link, BrowserRouter as Router } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import { createZuulStore, setTenantAction } from '../../reducers'
|
||||
import ChangePanel from './ChangePanel'
|
||||
|
||||
|
||||
const fakeChange = {
|
||||
project: 'org-project',
|
||||
jobs: [{
|
||||
name: 'job-name',
|
||||
url: 'stream.html?build=42',
|
||||
result: null
|
||||
}]
|
||||
}
|
||||
|
||||
it('change panel render multi tenant links', () => {
|
||||
const store = createZuulStore()
|
||||
store.dispatch(setTenantAction('tenant-one', false))
|
||||
const application = ReactTestUtils.renderIntoDocument(
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<ChangePanel change={fakeChange} globalExpanded={true} />
|
||||
</Router>
|
||||
</Provider>
|
||||
)
|
||||
const jobLink = ReactTestUtils.findRenderedComponentWithType(
|
||||
application, Link)
|
||||
expect(jobLink.props.to).toEqual(
|
||||
'/t/tenant-one/stream/42?logfile=console.log')
|
||||
})
|
||||
|
||||
it('change panel render white-label tenant links', () => {
|
||||
const store = createZuulStore()
|
||||
store.dispatch(setTenantAction('tenant-one', true))
|
||||
const application = ReactTestUtils.renderIntoDocument(
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<ChangePanel change={fakeChange} globalExpanded={true} />
|
||||
</Router>
|
||||
</Provider>
|
||||
)
|
||||
const jobLink = ReactTestUtils.findRenderedComponentWithType(
|
||||
application, Link)
|
||||
expect(jobLink.props.to).toEqual(
|
||||
'/stream/42?logfile=console.log')
|
||||
})
|
54
web/src/containers/status/ChangeQueue.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Change from './Change'
|
||||
|
||||
|
||||
class ChangeQueue extends React.Component {
|
||||
static propTypes = {
|
||||
pipeline: PropTypes.string.isRequired,
|
||||
queue: PropTypes.object.isRequired,
|
||||
expanded: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { queue, pipeline, expanded } = this.props
|
||||
let shortName = queue.name
|
||||
if (shortName.length > 32) {
|
||||
shortName = shortName.substr(0, 32) + '...'
|
||||
}
|
||||
let changesList = []
|
||||
queue.heads.forEach((changes, changeIdx) => {
|
||||
changes.forEach((change, idx) => {
|
||||
changesList.push(
|
||||
<Change
|
||||
change={change}
|
||||
queue={queue}
|
||||
expanded={expanded}
|
||||
key={changeIdx.toString() + idx}
|
||||
/>)
|
||||
})
|
||||
})
|
||||
return (
|
||||
<div className="change-queue" data-zuul-pipeline={pipeline}>
|
||||
<p>Queue: <abbr title={queue.name}>{shortName}</abbr></p>
|
||||
{changesList}
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangeQueue
|
136
web/src/containers/status/Pipeline.jsx
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Badge } from 'patternfly-react'
|
||||
|
||||
import ChangeQueue from './ChangeQueue'
|
||||
|
||||
|
||||
class Pipeline extends React.Component {
|
||||
static propTypes = {
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
pipeline: PropTypes.object.isRequired,
|
||||
filter: PropTypes.string
|
||||
}
|
||||
|
||||
createTree (pipeline) {
|
||||
let count = 0
|
||||
let pipelineMaxTreeColumns = 1
|
||||
pipeline.change_queues.forEach(changeQueue => {
|
||||
let tree = []
|
||||
let maxTreeColumns = 1
|
||||
let changes = []
|
||||
let lastTreeLength = 0
|
||||
changeQueue.heads.forEach(head => {
|
||||
head.forEach((change, changeIndex) => {
|
||||
changes[change.id] = change
|
||||
change._tree_position = changeIndex
|
||||
})
|
||||
})
|
||||
changeQueue.heads.forEach(head => {
|
||||
head.forEach(change => {
|
||||
if (change.live === true) {
|
||||
count += 1
|
||||
}
|
||||
let idx = tree.indexOf(change.id)
|
||||
if (idx > -1) {
|
||||
change._tree_index = idx
|
||||
// remove...
|
||||
tree[idx] = null
|
||||
while (tree[tree.length - 1] === null) {
|
||||
tree.pop()
|
||||
}
|
||||
} else {
|
||||
change._tree_index = 0
|
||||
}
|
||||
change._tree_branches = []
|
||||
change._tree = []
|
||||
if (typeof (change.items_behind) === 'undefined') {
|
||||
change.items_behind = []
|
||||
}
|
||||
change.items_behind.sort(function (a, b) {
|
||||
return (changes[b]._tree_position - changes[a]._tree_position)
|
||||
})
|
||||
change.items_behind.forEach(id => {
|
||||
tree.push(id)
|
||||
if (tree.length > lastTreeLength && lastTreeLength > 0) {
|
||||
change._tree_branches.push(tree.length - 1)
|
||||
}
|
||||
})
|
||||
if (tree.length > maxTreeColumns) {
|
||||
maxTreeColumns = tree.length
|
||||
}
|
||||
if (tree.length > pipelineMaxTreeColumns) {
|
||||
pipelineMaxTreeColumns = tree.length
|
||||
}
|
||||
change._tree = tree.slice(0) // make a copy
|
||||
lastTreeLength = tree.length
|
||||
})
|
||||
})
|
||||
changeQueue._tree_columns = maxTreeColumns
|
||||
})
|
||||
pipeline._tree_columns = pipelineMaxTreeColumns
|
||||
return count
|
||||
}
|
||||
|
||||
filterQueue(queue, filter) {
|
||||
let found = false
|
||||
queue.heads.forEach(changes => {
|
||||
changes.forEach(change => {
|
||||
if ((change.project && change.project.indexOf(filter) !== -1) ||
|
||||
(change.id && change.id.indexOf(filter) !== -1)) {
|
||||
found = true
|
||||
return
|
||||
}
|
||||
})
|
||||
if (found) {
|
||||
return
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
render () {
|
||||
const { pipeline, filter, expanded } = this.props
|
||||
const count = this.createTree(pipeline)
|
||||
return (
|
||||
<div className="zuul-pipeline col-md-4">
|
||||
<div className="zuul-pipeline-header">
|
||||
<h3>{pipeline.name} <Badge>{count}</Badge></h3>
|
||||
{pipeline.description ? (
|
||||
<small>
|
||||
<p>{pipeline.description.split(/\r?\n\r?\n/)}</p>
|
||||
</small>) : ''}
|
||||
</div>
|
||||
{pipeline.change_queues.filter(item => item.heads.length > 0)
|
||||
.filter(item => (!filter || (
|
||||
pipeline.name.indexOf(filter) !== -1 ||
|
||||
this.filterQueue(item, filter)
|
||||
)))
|
||||
.map((changeQueue, idx) => (
|
||||
<ChangeQueue
|
||||
queue={changeQueue}
|
||||
expanded={expanded}
|
||||
pipeline={pipeline.name}
|
||||
key={idx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Pipeline
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 204 B After Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
BIN
web/src/images/logo.png
Normal file
After Width: | Height: | Size: 930 B |
83
web/src/images/logo.svg
Normal file
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 144 144"
|
||||
style="enable-background:new 0 0 144 144;"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/data/logo.png"
|
||||
inkscape:export-xdpi="8.7290258"
|
||||
inkscape:export-ydpi="8.7290258"><metadata
|
||||
id="metadata21"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs19" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1220"
|
||||
inkscape:window-height="740"
|
||||
id="namedview17"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
inkscape:zoom="0.81944444"
|
||||
inkscape:cx="250.26215"
|
||||
inkscape:cy="186.8512"
|
||||
inkscape:window-x="252"
|
||||
inkscape:window-y="337"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1"><inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3360" /></sodipodi:namedview><style
|
||||
type="text/css"
|
||||
id="style3">
|
||||
.st0{fill:#071D49;}
|
||||
</style><path
|
||||
style="fill:#e6e6e6"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path7"
|
||||
d="m 12.8,102.6 118.5,0 -25.3,-43.8 0,-15 7,-9.2 -21,0 L 72,0 52,34.7 l -20.9,0 7,9.2 0,15 -25.3,43.7 z m 25.2,-6 -14.9,0 14.9,-25.8 0,25.8 z m 10.4,0 -4.4,0 0,-35.3 4.3,0 0,35.3 z m 0,-41.3 -4.4,0 0,-4.3 4.3,0 0,4.3 z m 20.6,41.3 -14.7,0 0,-35.3 14.7,0 0,35.3 z m 20.7,0 -14.7,0 0,-35.3 14.7,0 0,35.3 z m 0,-41.3 -35.4,0 0,-4.3 35.3,0 0,4.3 z m 10.3,41.3 -4.3,0 0,-35.3 4.3,0 0,35.3 z m 0,-41.3 -4.3,0 0,-4.3 4.3,0 0,4.3 z m 6,15.5 14.9,25.8 -14.9,0 0,-25.8 z M 72,12 85.1,34.7 58.9,34.7 72,12 Z m 28.9,28.7 -0.9,1.2 0,3.1 -56,0 0,-3.2 -0.9,-1.2 57.8,0 z"
|
||||
class="st0"
|
||||
inkscape:export-xdpi="14.038373"
|
||||
inkscape:export-ydpi="14.038373" /><g
|
||||
id="g3354"
|
||||
transform="matrix(3,0,0,3,155.07357,-334.75)"
|
||||
style="fill:#e6e6e6"
|
||||
inkscape:export-xdpi="14.038373"
|
||||
inkscape:export-ydpi="14.038373"><polygon
|
||||
class="st0"
|
||||
points="138.2,137.3 125.1,137.3 125.1,114.6 119.1,118.1 119.1,137.3 119.1,139.6 119.1,143.3 141.6,143.3 "
|
||||
id="polygon9"
|
||||
style="fill:#e6e6e6" /><path
|
||||
class="st0"
|
||||
d="m 99.1,131.5 0,0 0,0 c 0,3.6 -2.9,6.5 -6.5,6.5 -3.6,0 -6.5,-2.9 -6.5,-6.5 l 0,0 0,0 0,-16.9 -6,3.5 0,13.5 0,0 c 0,6.9 5.6,12.5 12.5,12.5 6.9,0 12.5,-5.6 12.5,-12.5 l 0,0 0,-16.9 -6,3.5 0,13.3 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#e6e6e6" /><path
|
||||
class="st0"
|
||||
d="m 60.2,131.5 0,0 0,0 c 0,3.6 -2.9,6.5 -6.5,6.5 -3.6,0 -6.5,-2.9 -6.5,-6.5 l 0,0 0,0 0,-16.9 -6,3.5 0,13.5 0,0 c 0,6.9 5.6,12.5 12.5,12.5 6.9,0 12.5,-5.6 12.5,-12.5 l 0,0 0,-16.9 -6,3.5 0,13.3 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#e6e6e6" /><polygon
|
||||
class="st0"
|
||||
points="2.4,143.3 23.8,143.3 27.3,137.3 12.7,137.3 25.8,114.6 25.4,114.6 18.9,114.6 5.8,114.6 2.4,120.6 15.5,120.6 "
|
||||
id="polygon15"
|
||||
style="fill:#e6e6e6" /></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
122
web/src/index.css
Normal file
@ -0,0 +1,122 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
a.refresh {
|
||||
cursor: pointer;
|
||||
border-bottom-style: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Status page */
|
||||
.zuul-change {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.zuul-change-id {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.zuul-job-result {
|
||||
float: right;
|
||||
width: 70px;
|
||||
height: 15px;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.zuul-change-total-result {
|
||||
height: 10px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.zuul-spinner,
|
||||
.zuul-spinner:hover {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-out;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zuul-spinner-on,
|
||||
.zuul-spinner-on:hover {
|
||||
opacity: 1;
|
||||
transition-duration: 0.2s;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.zuul-change-cell {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.zuul-change-job {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.zuul-job-name {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.zuul-non-voting-desc {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.zuul-patchset-header {
|
||||
font-size: small;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.form-inline > .form-group {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.zuul-change-row {
|
||||
height: 100%;
|
||||
padding: 0 0 10px 0;
|
||||
margin: 0;
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.zuul-build-status {
|
||||
background: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.zuul-build-merge-conflict:before {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.zuul-change-row-line {
|
||||
background-image: url('images/line.png');
|
||||
background-repeat: 'repeat-y';
|
||||
}
|
||||
|
||||
/* Stream page */
|
||||
#zuulstreamoverlay {
|
||||
float: right;
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
right: 5px;
|
||||
background-color: white;
|
||||
padding: 2px 0px 0px 2px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
pre#zuulstreamcontent {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 0px 10px;
|
||||
background-color: black;
|
||||
color: lightgrey;
|
||||
border: none;
|
||||
}
|
||||
p.zuulstreamline {
|
||||
margin: 0px 0px;
|
||||
line-height: 1.4;
|
||||
}
|
40
web/src/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// The index is the main of the project. The App is wrapped with
|
||||
// a Provider to share the redux store and a Router to manage the location.
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import 'patternfly/dist/css/patternfly.min.css'
|
||||
import 'patternfly/dist/css/patternfly-additions.min.css'
|
||||
import './index.css'
|
||||
|
||||
import { getHomepageUrl } from './api'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import { createZuulStore, fetchInfoAction } from './reducers'
|
||||
import App from './App'
|
||||
|
||||
// This calls the /api/info endpoint asynchronously, the App is connected
|
||||
// with redux and it will update the info prop when fetch succeed.
|
||||
const store = createZuulStore()
|
||||
store.dispatch(fetchInfoAction())
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router basename={new URL(getHomepageUrl()).pathname}><App /></Router>
|
||||
</Provider>, document.getElementById('root'))
|
||||
registerServiceWorker()
|
159
web/src/pages/Builds.jsx
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Table } from 'patternfly-react'
|
||||
|
||||
import { fetchBuilds } from '../api'
|
||||
import TableFilters from '../containers/TableFilters'
|
||||
|
||||
|
||||
class BuildsPage extends TableFilters {
|
||||
static propTypes = {
|
||||
tenant: PropTypes.object
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.prepareTableHeaders()
|
||||
this.state = {
|
||||
builds: null,
|
||||
currentFilterType: this.filterTypes[0],
|
||||
activeFilters: [],
|
||||
currentValue: ''
|
||||
}
|
||||
}
|
||||
|
||||
updateData = (filters) => {
|
||||
let queryString = ''
|
||||
if (filters) {
|
||||
filters.forEach(item => queryString += '&' + item.key + '=' + item.value)
|
||||
}
|
||||
this.setState({builds: null})
|
||||
fetchBuilds(this.props.tenant.apiPrefix, queryString).then(response => {
|
||||
this.setState({builds: response.data})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = 'Zuul Build'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData(this.getFilterFromUrl())
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData(this.getFilterFromUrl())
|
||||
}
|
||||
}
|
||||
|
||||
prepareTableHeaders() {
|
||||
const headerFormat = value => <Table.Heading>{value}</Table.Heading>
|
||||
const cellFormat = (value) => (
|
||||
<Table.Cell>{value}</Table.Cell>)
|
||||
const linkCellFormat = (value) => (
|
||||
<Table.Cell>
|
||||
<a href={value} target='_blank' rel='noopener noreferrer'>link</a>
|
||||
</Table.Cell>
|
||||
)
|
||||
this.columns = []
|
||||
this.filterTypes = []
|
||||
const myColumns = [
|
||||
'job',
|
||||
'project',
|
||||
'branch',
|
||||
'pipeline',
|
||||
'change',
|
||||
'duration',
|
||||
'log',
|
||||
'start time',
|
||||
'result']
|
||||
myColumns.forEach(column => {
|
||||
let prop = column
|
||||
let formatter = cellFormat
|
||||
// Adapt column name and property name
|
||||
if (column === 'job') {
|
||||
prop = 'job_name'
|
||||
} else if (column === 'start time') {
|
||||
prop = 'start_time'
|
||||
} else if (column === 'change') {
|
||||
prop = 'ref_url'
|
||||
formatter = linkCellFormat
|
||||
} else if (column === 'log') {
|
||||
prop = 'log_url'
|
||||
formatter = linkCellFormat
|
||||
}
|
||||
const label = column.charAt(0).toUpperCase() + column.slice(1)
|
||||
this.columns.push({
|
||||
header: {label: label, formatters: [headerFormat]},
|
||||
property: prop,
|
||||
cell: {formatters: [formatter]}
|
||||
})
|
||||
if (prop !== 'start_time' && prop !== 'ref_url' && prop !== 'duration'
|
||||
&& prop !== 'log_url' && prop !== 'uuid') {
|
||||
this.filterTypes.push({
|
||||
id: prop,
|
||||
title: label,
|
||||
placeholder: 'Filter by ' + label,
|
||||
filterType: 'text',
|
||||
})
|
||||
}
|
||||
})
|
||||
// Add build filter at the end
|
||||
this.filterTypes.push({
|
||||
id: 'uuid',
|
||||
title: 'Build',
|
||||
palceholder: 'Filter by Build UUID',
|
||||
fileterType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
renderTable (builds) {
|
||||
return (
|
||||
<Table.PfProvider
|
||||
striped
|
||||
bordered
|
||||
columns={this.columns}
|
||||
>
|
||||
<Table.Header/>
|
||||
<Table.Body
|
||||
rows={builds}
|
||||
rowKey='uuid'
|
||||
onRow={(row) => {
|
||||
switch (row.result) {
|
||||
case 'SUCCESS':
|
||||
return { className: 'success' }
|
||||
default:
|
||||
return { className: 'warning' }
|
||||
}
|
||||
}} />
|
||||
</Table.PfProvider>)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { builds } = this.state
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderFilter()}
|
||||
{builds ? this.renderTable(builds) : <p>Loading...</p>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(BuildsPage)
|
99
web/src/pages/Jobs.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Table } from 'patternfly-react'
|
||||
|
||||
import { fetchJobs } from '../api'
|
||||
|
||||
|
||||
class JobsPage extends React.Component {
|
||||
static propTypes = {
|
||||
tenant: PropTypes.object
|
||||
}
|
||||
|
||||
state = {
|
||||
jobs: null
|
||||
}
|
||||
|
||||
updateData () {
|
||||
fetchJobs(this.props.tenant.apiPrefix).then(response => {
|
||||
this.setState({jobs: response.data})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = 'Zuul Jobs'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { jobs } = this.state
|
||||
if (!jobs) {
|
||||
return (<p>Loading...</p>)
|
||||
}
|
||||
|
||||
const headerFormat = value => <Table.Heading>{value}</Table.Heading>
|
||||
const cellFormat = (value) => (
|
||||
<Table.Cell>{value}</Table.Cell>)
|
||||
const cellBuildFormat = (value) => (
|
||||
<Table.Cell>
|
||||
<Link to={this.props.tenant.linkPrefix + '/builds?job_name=' + value}>
|
||||
builds
|
||||
</Link>
|
||||
</Table.Cell>)
|
||||
const columns = []
|
||||
const myColumns = ['name', 'description', 'Last builds']
|
||||
myColumns.forEach(column => {
|
||||
let formatter = cellFormat
|
||||
let prop = column
|
||||
if (column === 'Last builds') {
|
||||
prop = 'name'
|
||||
formatter = cellBuildFormat
|
||||
}
|
||||
columns.push({
|
||||
header: {label: column,
|
||||
formatters: [headerFormat]},
|
||||
property: prop,
|
||||
cell: {formatters: [formatter]}
|
||||
})
|
||||
})
|
||||
return (
|
||||
<Table.PfProvider
|
||||
striped
|
||||
bordered
|
||||
hover
|
||||
columns={columns}
|
||||
>
|
||||
<Table.Header/>
|
||||
<Table.Body
|
||||
rows={jobs}
|
||||
rowKey="name"
|
||||
/>
|
||||
</Table.PfProvider>)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(JobsPage)
|
289
web/src/pages/Status.jsx
Normal file
@ -0,0 +1,289 @@
|
||||
/* global setTimeout, clearTimeout */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
Alert,
|
||||
Checkbox,
|
||||
Icon,
|
||||
Form,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
Spinner
|
||||
} from 'patternfly-react'
|
||||
|
||||
import { fetchStatus } from '../api'
|
||||
import Pipeline from '../containers/status/Pipeline'
|
||||
|
||||
|
||||
class StatusPage extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
tenant: PropTypes.object
|
||||
}
|
||||
|
||||
state = {
|
||||
status: null,
|
||||
filter: null,
|
||||
expanded: false,
|
||||
error: null,
|
||||
loading: false,
|
||||
autoReload: true
|
||||
}
|
||||
|
||||
visibilityListener = () => {
|
||||
if (document[this.visibilityStateProperty] === 'visible') {
|
||||
this.visible = true
|
||||
this.updateData()
|
||||
} else {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.timer = null
|
||||
this.visible = true
|
||||
|
||||
// Stop refresh when page is not visible
|
||||
if (typeof document.hidden !== 'undefined') {
|
||||
this.visibilityChangeEvent = 'visibilitychange'
|
||||
this.visibilityStateProperty = 'visibilityState'
|
||||
} else if (typeof document.mozHidden !== 'undefined') {
|
||||
this.visibilityChangeEvent = 'mozvisibilitychange'
|
||||
this.visibilityStateProperty = 'mozVisibilityState'
|
||||
} else if (typeof document.msHidden !== 'undefined') {
|
||||
this.visibilityChangeEvent = 'msvisibilitychange'
|
||||
this.visibilityStateProperty = 'msVisibilityState'
|
||||
} else if (typeof document.webkitHidden !== 'undefined') {
|
||||
this.visibilityChangeEvent = 'webkitvisibilitychange'
|
||||
this.visibilityStateProperty = 'webkitVisibilityState'
|
||||
}
|
||||
document.addEventListener(
|
||||
this.visibilityChangeEvent, this.visibilityListener, false)
|
||||
}
|
||||
|
||||
setCookie (name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/'
|
||||
}
|
||||
|
||||
updateData = (force) => {
|
||||
/* // Create fake delay
|
||||
function sleeper(ms) {
|
||||
return function(x) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(x), ms));
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
if (force || (this.visible && this.state.autoReload)) {
|
||||
this.setState({error: null, loading: true})
|
||||
fetchStatus(this.props.tenant.apiPrefix)
|
||||
// .then(sleeper(2000))
|
||||
.then(response => {
|
||||
this.setState({status: response.data, loading: false})
|
||||
}).catch(error => {
|
||||
this.setState({error: error.message, status: null})
|
||||
})
|
||||
}
|
||||
// Clear any running timer
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
if (this.state.autoReload) {
|
||||
this.timer = setTimeout(this.updateData, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = 'Zuul Status'
|
||||
this.loadState()
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
// When autoReload is set, also call updateData to retrigger the setTimeout
|
||||
if (this.props.tenant.name !== prevProps.tenant.name || (
|
||||
this.state.autoReload &&
|
||||
this.state.autoReload !== prevState.autoReload)) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
document.removeEventListener(
|
||||
this.visibilityChangeEvent, this.visibilityListener)
|
||||
}
|
||||
|
||||
setFilter = (filter) => {
|
||||
this.filter.value = filter
|
||||
this.setState({filter: filter})
|
||||
this.setCookie('zuul_filter_string', filter)
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if (e.charCode === 13) {
|
||||
this.setFilter(e.target.value)
|
||||
e.preventDefault()
|
||||
e.target.blur()
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckBox = (e) => {
|
||||
this.setState({expanded: e.target.checked})
|
||||
this.setCookie('zuul_expand_by_default', e.target.checked)
|
||||
}
|
||||
|
||||
loadState = () => {
|
||||
function readCookie (name, defaultValue) {
|
||||
let nameEQ = name + '='
|
||||
let ca = document.cookie.split(';')
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i]
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1, c.length)
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
let filter = readCookie('zuul_filter_string', '')
|
||||
let expanded = readCookie('zuul_expand_by_default', false)
|
||||
if (typeof expanded === 'string') {
|
||||
expanded = (expanded === 'true')
|
||||
}
|
||||
|
||||
if (this.props.location.hash) {
|
||||
filter = this.props.location.hash.slice(1)
|
||||
}
|
||||
if (filter || expanded) {
|
||||
this.setState({
|
||||
filter: filter,
|
||||
expanded: expanded
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
renderStatusHeader (status) {
|
||||
return (
|
||||
<p>
|
||||
Queue lengths: <span>{status.trigger_event_queue ?
|
||||
status.trigger_event_queue.length : '0'
|
||||
}</span> events,
|
||||
<span>{status.management_event_queue ?
|
||||
status.management_event_queue.length : '0'
|
||||
}</span> management events,
|
||||
<span>{status.result_event_queue ?
|
||||
status.result_event_queue.length : '0'
|
||||
}</span> results.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
renderStatusFooter (status) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p>Zuul version: <span>{status.zuul_version}</span></p>
|
||||
{status.last_reconfigured ? (
|
||||
<p>Last reconfigured: <span>
|
||||
{new Date(status.last_reconfigured).toString()}
|
||||
</span></p>) : ''}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { autoReload, error, status, filter, expanded, loading } = this.state
|
||||
if (error) {
|
||||
return (<Alert>{this.state.error}</Alert>)
|
||||
}
|
||||
if (this.filter && filter) {
|
||||
this.filter.value = filter
|
||||
}
|
||||
const statusControl = (
|
||||
<Form inline>
|
||||
<FormGroup controlId='status'>
|
||||
<FormControl
|
||||
type='text'
|
||||
placeholder='change or project name'
|
||||
defaultValue={filter}
|
||||
inputRef={i => this.filter = i}
|
||||
onKeyPress={this.handleKeyPress} />
|
||||
{filter && (
|
||||
<FormControl.Feedback>
|
||||
<span
|
||||
onClick={() => {this.setFilter('')}}
|
||||
style={{cursor: 'pointer', zIndex: 10, pointerEvents: 'auto'}}
|
||||
>
|
||||
<Icon type='pf' title='Clear filter' name='delete' />
|
||||
|
||||
</span>
|
||||
</FormControl.Feedback>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup controlId='status'>
|
||||
Expand by default:
|
||||
<Checkbox
|
||||
defaultChecked={expanded}
|
||||
onChange={this.handleCheckBox} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
)
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="pull-right" style={{display: 'flex'}}>
|
||||
<Spinner loading={loading}>
|
||||
<a className="refresh" onClick={() => {this.updateData(true)}}>
|
||||
<Icon type="fa" name="refresh" /> refresh
|
||||
</a>
|
||||
</Spinner>
|
||||
<Checkbox
|
||||
defaultChecked={autoReload}
|
||||
onChange={(e) => {this.setState({autoReload: e.target.checked})}}
|
||||
style={{marginTop: '0px'}}>
|
||||
auto reload
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
{status && this.renderStatusHeader(status)}
|
||||
{statusControl}
|
||||
<div className='row'>
|
||||
{status && status.pipelines.map(item => (
|
||||
<Pipeline
|
||||
pipeline={item}
|
||||
filter={filter}
|
||||
expanded={expanded}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{status && this.renderStatusFooter(status)}
|
||||
</React.Fragment>)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(StatusPage)
|
158
web/src/pages/Stream.jsx
Normal file
@ -0,0 +1,158 @@
|
||||
/* global clearTimeout, setTimeout, JSON, URLSearchParams */
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Checkbox, Form, FormGroup } from 'patternfly-react'
|
||||
import Sockette from 'sockette'
|
||||
|
||||
import { getStreamUrl } from '../api'
|
||||
|
||||
|
||||
class StreamPage extends React.Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
tenant: PropTypes.object
|
||||
}
|
||||
|
||||
state = {
|
||||
autoscroll: true,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.receiveBuffer = ''
|
||||
this.displayRef = React.createRef()
|
||||
this.lines = []
|
||||
}
|
||||
|
||||
refreshLoop = () => {
|
||||
if (this.displayRef.current) {
|
||||
let newLine = false
|
||||
this.lines.forEach(line => {
|
||||
newLine = true
|
||||
this.displayRef.current.appendChild(line)
|
||||
})
|
||||
this.lines = []
|
||||
if (newLine) {
|
||||
const { autoscroll } = this.state
|
||||
if (autoscroll) {
|
||||
this.messagesEnd.scrollIntoView({ behavior: 'instant' })
|
||||
}
|
||||
}
|
||||
}
|
||||
this.timer = setTimeout(this.refreshLoop, 250)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
if (this.ws) {
|
||||
console.log('Remove ws')
|
||||
this.ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
onLine = (line) => {
|
||||
// Create dom elements
|
||||
const lineDom = document.createElement('p')
|
||||
lineDom.className = 'zuulstreamline'
|
||||
lineDom.appendChild(document.createTextNode(line))
|
||||
this.lines.push(lineDom)
|
||||
}
|
||||
|
||||
onMessage = (message) => {
|
||||
this.receiveBuffer += message
|
||||
const lines = this.receiveBuffer.split('\n')
|
||||
const lastLine = lines.slice(-1)[0]
|
||||
// Append all completed lines
|
||||
lines.slice(0, -1).forEach(line => {
|
||||
this.onLine(line)
|
||||
})
|
||||
// Check if last chunk is completed
|
||||
if (lastLine && this.receiveBuffer.slice(-1) === '\n') {
|
||||
this.onLine(lastLine)
|
||||
this.receiveBuffer = ''
|
||||
} else {
|
||||
this.receiveBuffer = lastLine
|
||||
}
|
||||
this.refreshLoop()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const params = {
|
||||
uuid: this.props.match.params.buildId
|
||||
}
|
||||
const urlParams = new URLSearchParams(this.props.location.search)
|
||||
const logfile = urlParams.get('logfile')
|
||||
if (logfile) {
|
||||
params.logfile = logfile
|
||||
}
|
||||
document.title = 'Zuul Stream | ' + params.uuid.slice(0, 7)
|
||||
this.ws = new Sockette(getStreamUrl(this.props.tenant.apiPrefix), {
|
||||
timeout: 5e3,
|
||||
maxAttempts: 3,
|
||||
onopen: () => {
|
||||
console.log('onopen')
|
||||
this.ws.send(JSON.stringify(params))
|
||||
},
|
||||
onmessage: e => {
|
||||
this.onMessage(e.data)
|
||||
},
|
||||
onreconnect: e => {
|
||||
console.log('Reconnecting...', e)
|
||||
},
|
||||
onmaximum: e => {
|
||||
console.log('Stop Attempting!', e)
|
||||
},
|
||||
onclose: e => {
|
||||
console.log('onclose', e)
|
||||
this.onMessage('\n--- END OF STREAM ---\n')
|
||||
},
|
||||
onerror: e => {
|
||||
console.log('onerror:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleCheckBox = (e) => {
|
||||
this.setState({autoscroll: e.target.checked})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form inline id='zuulstreamoverlay'>
|
||||
<FormGroup controlId='stream'>
|
||||
<Checkbox
|
||||
checked={this.state.autoscroll}
|
||||
onChange={this.handleCheckBox}>
|
||||
autoscroll
|
||||
</Checkbox>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<pre id='zuulstreamcontent' ref={this.displayRef} />
|
||||
<div ref={(el) => { this.messagesEnd = el }} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(StreamPage)
|
79
web/src/pages/Tenants.jsx
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Table } from 'patternfly-react'
|
||||
|
||||
import { fetchTenants } from '../api'
|
||||
|
||||
class TenantsPage extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
tenants: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = 'Zuul Tenants'
|
||||
fetchTenants().then(response => {
|
||||
this.setState({tenants: response.data})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { tenants } = this.state
|
||||
if (tenants.length === 0) {
|
||||
return (<p>Loading...</p>)
|
||||
}
|
||||
const headerFormat = value => <Table.Heading>{value}</Table.Heading>
|
||||
const cellFormat = (value) => (
|
||||
<Table.Cell>{value}</Table.Cell>)
|
||||
const columns = []
|
||||
const myColumns = ['name', 'status', 'jobs', 'builds', 'projects', 'queue']
|
||||
myColumns.forEach(column => {
|
||||
columns.push({
|
||||
header: {label: column,
|
||||
formatters: [headerFormat]},
|
||||
property: column,
|
||||
cell: {formatters: [cellFormat]}
|
||||
})
|
||||
})
|
||||
tenants.forEach(tenant => {
|
||||
tenant.status = (
|
||||
<Link to={'/t/' + tenant.name + '/status'}>Status</Link>)
|
||||
tenant.jobs = (
|
||||
<Link to={'/t/' + tenant.name + '/jobs'}>Jobs</Link>)
|
||||
tenant.builds = (
|
||||
<Link to={'/t/' + tenant.name + '/builds'}>Builds</Link>)
|
||||
})
|
||||
return (
|
||||
<Table.PfProvider
|
||||
striped
|
||||
bordered
|
||||
hover
|
||||
columns={columns}
|
||||
>
|
||||
<Table.Header/>
|
||||
<Table.Body
|
||||
rows={tenants}
|
||||
rowKey="name"
|
||||
/>
|
||||
</Table.PfProvider>)
|
||||
}
|
||||
}
|
||||
|
||||
export default TenantsPage
|
94
web/src/reducers.js
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Redux store enable to share global variables through state
|
||||
// To update the store, use a reducer and dispatch method,
|
||||
// see the App.setTenant method
|
||||
//
|
||||
// The store contains:
|
||||
// info: the info object, tenant is set when white-label api
|
||||
// tenant: the current tenant name, only used with multi-tenant api
|
||||
|
||||
import { applyMiddleware, createStore, combineReducers } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import { fetchInfo } from './api'
|
||||
|
||||
const infoReducer = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case 'FETCH_INFO_SUCCESS':
|
||||
return action.info
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const tenantReducer = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_TENANT':
|
||||
return action.tenant
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function createZuulStore() {
|
||||
return createStore(combineReducers({
|
||||
info: infoReducer,
|
||||
tenant: tenantReducer
|
||||
}), applyMiddleware(thunk))
|
||||
}
|
||||
|
||||
// Reducer actions
|
||||
function fetchInfoAction () {
|
||||
return (dispatch) => {
|
||||
return fetchInfo()
|
||||
.then(response => {
|
||||
dispatch({type: 'FETCH_INFO_SUCCESS', info: response.data.info})
|
||||
})
|
||||
.catch(error => {
|
||||
throw (error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setTenantAction (name, whiteLabel) {
|
||||
let apiPrefix = ''
|
||||
let linkPrefix = ''
|
||||
let routePrefix = ''
|
||||
let defaultRoute = '/status'
|
||||
if (!whiteLabel) {
|
||||
apiPrefix = 'tenant/' + name + '/'
|
||||
linkPrefix = '/t/' + name
|
||||
routePrefix = '/t/:tenant'
|
||||
defaultRoute = '/tenants'
|
||||
}
|
||||
return {
|
||||
type: 'SET_TENANT',
|
||||
tenant: {
|
||||
name: name,
|
||||
whiteLabel: whiteLabel,
|
||||
defaultRoute: defaultRoute,
|
||||
linkPrefix: linkPrefix,
|
||||
apiPrefix: apiPrefix,
|
||||
routePrefix: routePrefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createZuulStore,
|
||||
setTenantAction,
|
||||
fetchInfoAction
|
||||
}
|
119
web/src/registerServiceWorker.js
Normal file
@ -0,0 +1,119 @@
|
||||
/* global process */
|
||||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read
|
||||
// https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#making-a-progressive-web-app
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
)
|
||||
|
||||
export default function register () {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW (swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.')
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker (swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function unregister () {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
}
|
||||
}
|
52
web/src/routes.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import StatusPage from './pages/Status'
|
||||
import JobsPage from './pages/Jobs'
|
||||
import BuildsPage from './pages/Builds'
|
||||
import TenantsPage from './pages/Tenants'
|
||||
import StreamPage from './pages/Stream'
|
||||
|
||||
// The Route object are created in the App component.
|
||||
// Object with a title are created in the menu.
|
||||
// Object with globalRoute are not tenant scoped.
|
||||
// Remember to update the api getHomepageUrl subDir list for route with params
|
||||
const routes = () => [
|
||||
{
|
||||
title: 'Status',
|
||||
to: '/status',
|
||||
component: StatusPage
|
||||
},
|
||||
{
|
||||
title: 'Jobs',
|
||||
to: '/jobs',
|
||||
component: JobsPage
|
||||
},
|
||||
{
|
||||
title: 'Builds',
|
||||
to: '/builds',
|
||||
component: BuildsPage
|
||||
},
|
||||
{
|
||||
to: '/stream/:buildId',
|
||||
component: StreamPage
|
||||
},
|
||||
{
|
||||
to: '/tenants',
|
||||
component: TenantsPage,
|
||||
globalRoute: true
|
||||
}
|
||||
]
|
||||
|
||||
export { routes }
|
@ -1,944 +0,0 @@
|
||||
/* global Image, jQuery */
|
||||
// jquery plugin for Zuul status 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.
|
||||
|
||||
import RedImage from '../images/red.png'
|
||||
import GreyImage from '../images/grey.png'
|
||||
import GreenImage from '../images/green.png'
|
||||
import BlackImage from '../images/black.png'
|
||||
import LineImage from '../images/line.png'
|
||||
import LineAngleImage from '../images/line-angle.png'
|
||||
import LineTImage from '../images/line-t.png';
|
||||
|
||||
(function ($) {
|
||||
function setCookie (name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/'
|
||||
}
|
||||
|
||||
function readCookie (name, defaultValue) {
|
||||
let nameEQ = name + '='
|
||||
let ca = document.cookie.split(';')
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i]
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1, c.length)
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length)
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
$.zuul = function (options, zuulService) {
|
||||
options = $.extend({
|
||||
'enabled': true,
|
||||
'graphite_url': '',
|
||||
'source': 'status',
|
||||
'source_data': null,
|
||||
'msg_id': '#zuul_msg',
|
||||
'pipelines_id': '#zuul_pipelines',
|
||||
'queue_events_num': '#zuul_queue_events_num',
|
||||
'queue_management_events_num': '#zuul_queue_management_events_num',
|
||||
'queue_results_num': '#zuul_queue_results_num'
|
||||
}, options)
|
||||
|
||||
let collapsedExceptions = []
|
||||
let currentFilter = readCookie('zuul_filter_string', '')
|
||||
let changeSetInURL = window.location.href.split('#')[1]
|
||||
if (changeSetInURL) {
|
||||
currentFilter = changeSetInURL
|
||||
}
|
||||
let $jq
|
||||
|
||||
let xhr
|
||||
let zuulGraphUpdateCount = 0
|
||||
let zuulSparklineURLs = {}
|
||||
|
||||
function getSparklineURL (pipelineName) {
|
||||
if (options.graphite_url !== '') {
|
||||
if (!(pipelineName in zuulSparklineURLs)) {
|
||||
zuulSparklineURLs[pipelineName] = $.fn.graphite
|
||||
.geturl({
|
||||
url: options.graphite_url,
|
||||
from: '-8hours',
|
||||
width: 100,
|
||||
height: 26,
|
||||
margin: 0,
|
||||
hideLegend: true,
|
||||
hideAxes: true,
|
||||
hideGrid: true,
|
||||
target: [
|
||||
'color(stats.gauges.zuul.pipeline.' + pipelineName +
|
||||
".current_changes, '6b8182')"
|
||||
]
|
||||
})
|
||||
}
|
||||
return zuulSparklineURLs[pipelineName]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let format = {
|
||||
job: function (job) {
|
||||
let $jobLine = $('<span />')
|
||||
|
||||
if (job.result !== null) {
|
||||
$jobLine.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.report_url)
|
||||
.text(job.name)
|
||||
)
|
||||
} else if (job.url !== null) {
|
||||
$jobLine.append(
|
||||
$('<a />')
|
||||
.addClass('zuul-job-name')
|
||||
.attr('href', job.url)
|
||||
.text(job.name)
|
||||
)
|
||||
} else {
|
||||
$jobLine.append(
|
||||
$('<span />')
|
||||
.addClass('zuul-job-name')
|
||||
.text(job.name)
|
||||
)
|
||||
}
|
||||
|
||||
$jobLine.append(this.job_status(job))
|
||||
|
||||
if (job.voting === false) {
|
||||
$jobLine.append(
|
||||
$(' <small />')
|
||||
.addClass('zuul-non-voting-desc')
|
||||
.text(' (non-voting)')
|
||||
)
|
||||
}
|
||||
|
||||
$jobLine.append($('<div style="clear: both"></div>'))
|
||||
return $jobLine
|
||||
},
|
||||
|
||||
job_status: function (job) {
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
if (job.url === null) {
|
||||
result = 'queued'
|
||||
} else if (job.paused !== null && job.paused) {
|
||||
result = 'paused'
|
||||
} else {
|
||||
result = 'in progress'
|
||||
}
|
||||
}
|
||||
|
||||
if (result === 'in progress') {
|
||||
return this.job_progress_bar(job.elapsed_time,
|
||||
job.remaining_time)
|
||||
} else {
|
||||
return this.status_label(result)
|
||||
}
|
||||
},
|
||||
|
||||
status_label: function (result) {
|
||||
let $status = $('<span />')
|
||||
$status.addClass('zuul-job-result label')
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$status.addClass('label-success')
|
||||
break
|
||||
case 'failure':
|
||||
$status.addClass('label-danger')
|
||||
break
|
||||
case 'unstable':
|
||||
$status.addClass('label-warning')
|
||||
break
|
||||
case 'skipped':
|
||||
$status.addClass('label-info')
|
||||
break
|
||||
// 'in progress' 'queued' 'lost' 'aborted' ...
|
||||
default:
|
||||
$status.addClass('label-default')
|
||||
}
|
||||
$status.text(result)
|
||||
return $status
|
||||
},
|
||||
|
||||
job_progress_bar: function (elapsedTime, remainingTime) {
|
||||
let progressPercent = 100 * (elapsedTime / (elapsedTime +
|
||||
remainingTime))
|
||||
let $barInner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
.attr('role', 'progressbar')
|
||||
.attr('aria-valuenow', 'progressbar')
|
||||
.attr('aria-valuemin', progressPercent)
|
||||
.attr('aria-valuemin', '0')
|
||||
.attr('aria-valuemax', '100')
|
||||
.css('width', progressPercent + '%')
|
||||
|
||||
let $barOutter = $('<div />')
|
||||
.addClass('progress zuul-job-result')
|
||||
.append($barInner)
|
||||
|
||||
return $barOutter
|
||||
},
|
||||
|
||||
enqueueTime: function (ms) {
|
||||
// Special format case for enqueue time to add style
|
||||
let hours = 60 * 60 * 1000
|
||||
let now = Date.now()
|
||||
let delta = now - ms
|
||||
let status = 'text-success'
|
||||
let text = this.time(delta, true)
|
||||
if (delta > (4 * hours)) {
|
||||
status = 'text-danger'
|
||||
} else if (delta > (2 * hours)) {
|
||||
status = 'text-warning'
|
||||
}
|
||||
return '<span class="' + status + '">' + text + '</span>'
|
||||
},
|
||||
|
||||
time: function (ms, words) {
|
||||
if (typeof (words) === 'undefined') {
|
||||
words = false
|
||||
}
|
||||
let seconds = (+ms) / 1000
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = Math.floor(minutes / 60)
|
||||
seconds = Math.floor(seconds % 60)
|
||||
minutes = Math.floor(minutes % 60)
|
||||
let r = ''
|
||||
if (words) {
|
||||
if (hours) {
|
||||
r += hours
|
||||
r += ' hr '
|
||||
}
|
||||
r += minutes + ' min'
|
||||
} else {
|
||||
if (hours < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += hours + ':'
|
||||
if (minutes < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += minutes + ':'
|
||||
if (seconds < 10) {
|
||||
r += '0'
|
||||
}
|
||||
r += seconds
|
||||
}
|
||||
return r
|
||||
},
|
||||
|
||||
changeTotalProgressBar: function (change) {
|
||||
let jobPercent = Math.floor(100 / change.jobs.length)
|
||||
let $barOutter = $('<div />')
|
||||
.addClass('progress zuul-change-total-result')
|
||||
|
||||
$.each(change.jobs, function (i, job) {
|
||||
let result = job.result ? job.result.toLowerCase() : null
|
||||
if (result === null) {
|
||||
result = job.url ? 'in progress' : 'queued'
|
||||
}
|
||||
|
||||
if (result !== 'queued') {
|
||||
let $barInner = $('<div />')
|
||||
.addClass('progress-bar')
|
||||
|
||||
switch (result) {
|
||||
case 'success':
|
||||
$barInner.addClass('progress-bar-success')
|
||||
break
|
||||
case 'lost':
|
||||
case 'failure':
|
||||
$barInner.addClass('progress-bar-danger')
|
||||
break
|
||||
case 'unstable':
|
||||
$barInner.addClass('progress-bar-warning')
|
||||
break
|
||||
case 'in progress':
|
||||
case 'queued':
|
||||
break
|
||||
}
|
||||
$barInner.attr('title', job.name)
|
||||
.css('width', jobPercent + '%')
|
||||
$barOutter.append($barInner)
|
||||
}
|
||||
})
|
||||
return $barOutter
|
||||
},
|
||||
|
||||
changeHeader: function (change) {
|
||||
let changeId = change.id || 'NA'
|
||||
|
||||
let $changeLink = $('<small />')
|
||||
if (change.url !== null) {
|
||||
let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
|
||||
if (githubId) {
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', changeId)
|
||||
.text('#' + githubId[1])
|
||||
)
|
||||
)
|
||||
} else if (/^[0-9a-f]{40}$/.test(changeId)) {
|
||||
let changeIdShort = changeId.slice(0, 7)
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).append(
|
||||
$('<abbr />')
|
||||
.attr('title', changeId)
|
||||
.text(changeIdShort)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
$changeLink.append(
|
||||
$('<a />').attr('href', change.url).text(changeId)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (changeId.length === 40) {
|
||||
changeId = changeId.substr(0, 7)
|
||||
}
|
||||
$changeLink.text(changeId)
|
||||
}
|
||||
|
||||
let $changeProgressRowLeft = $('<div />')
|
||||
.addClass('col-xs-4')
|
||||
.append($changeLink)
|
||||
let $changeProgressRowRight = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append(this.changeTotalProgressBar(change))
|
||||
|
||||
let $changeProgressRow = $('<div />')
|
||||
.addClass('row')
|
||||
.append($changeProgressRowLeft)
|
||||
.append($changeProgressRowRight)
|
||||
|
||||
let $projectSpan = $('<span />')
|
||||
.addClass('change_project')
|
||||
.text(change.project)
|
||||
|
||||
let $left = $('<div />')
|
||||
.addClass('col-xs-8')
|
||||
.append($projectSpan, $changeProgressRow)
|
||||
|
||||
let remainingTime
|
||||
if (change.remaining_time === null) {
|
||||
remainingTime = 'unknown'
|
||||
} else {
|
||||
remainingTime = this.time(change.remaining_time, true)
|
||||
}
|
||||
let enqueueTime = this.enqueueTime(change.enqueue_time)
|
||||
let $remainingTime = $('<small />').addClass('time')
|
||||
.attr('title', 'Remaining Time').html(remainingTime)
|
||||
let $enqueueTime = $('<small />').addClass('time')
|
||||
.attr('title', 'Elapsed Time').html(enqueueTime)
|
||||
|
||||
let $right = $('<div />')
|
||||
if (change.live === true) {
|
||||
$right.addClass('col-xs-4 text-right')
|
||||
.append($remainingTime, $('<br />'), $enqueueTime)
|
||||
}
|
||||
|
||||
let $header = $('<div />')
|
||||
.addClass('row')
|
||||
.append($left, $right)
|
||||
return $header
|
||||
},
|
||||
|
||||
change_list: function (jobs) {
|
||||
let format = this
|
||||
let $list = $('<ul />')
|
||||
.addClass('list-group zuul-patchset-body')
|
||||
|
||||
$.each(jobs, function (i, job) {
|
||||
let $item = $('<li />')
|
||||
.addClass('list-group-item')
|
||||
.addClass('zuul-change-job')
|
||||
.append(format.job(job))
|
||||
$list.append($item)
|
||||
})
|
||||
|
||||
return $list
|
||||
},
|
||||
|
||||
changePanel: function (change) {
|
||||
let $header = $('<div />')
|
||||
.addClass('panel-heading zuul-patchset-header')
|
||||
.append(this.changeHeader(change))
|
||||
|
||||
let panelId = change.id ? change.id.replace(',', '_')
|
||||
: change.project.replace('/', '_') +
|
||||
'-' + change.enqueue_time
|
||||
let $panel = $('<div />')
|
||||
.attr('id', panelId)
|
||||
.addClass('panel panel-default zuul-change')
|
||||
.append($header)
|
||||
.append(this.change_list(change.jobs))
|
||||
|
||||
$header.click(this.toggle_patchset)
|
||||
return $panel
|
||||
},
|
||||
|
||||
change_status_icon: function (change) {
|
||||
let iconFile = GreenImage
|
||||
let iconTitle = 'Succeeding'
|
||||
|
||||
if (change.active !== true) {
|
||||
// Grey icon
|
||||
iconFile = GreyImage
|
||||
iconTitle = 'Waiting until closer to head of queue to' +
|
||||
' start jobs'
|
||||
} else if (change.live !== true) {
|
||||
// Grey icon
|
||||
iconFile = GreyImage
|
||||
iconTitle = 'Dependent change required for testing'
|
||||
} else if (change.failing_reasons &&
|
||||
change.failing_reasons.length > 0) {
|
||||
let reason = change.failing_reasons.join(', ')
|
||||
iconTitle = 'Failing because ' + reason
|
||||
if (reason.match(/merge conflict/)) {
|
||||
// Black icon
|
||||
iconFile = BlackImage
|
||||
} else {
|
||||
// Red icon
|
||||
iconFile = RedImage
|
||||
}
|
||||
}
|
||||
|
||||
let $icon = $('<img />')
|
||||
.attr('src', zuulService.appBaseHref + iconFile)
|
||||
.attr('title', iconTitle)
|
||||
.css('display', 'block')
|
||||
|
||||
return $icon
|
||||
},
|
||||
|
||||
change_with_status_tree: function (change, changeQueue) {
|
||||
let $changeRow = $('<tr />')
|
||||
|
||||
for (let i = 0; i < changeQueue._tree_columns; i++) {
|
||||
let $treeCell = $('<td />')
|
||||
.css('height', '100%')
|
||||
.css('padding', '0 0 10px 0')
|
||||
.css('margin', '0')
|
||||
.css('width', '16px')
|
||||
.css('min-width', '16px')
|
||||
.css('overflow', 'hidden')
|
||||
.css('vertical-align', 'top')
|
||||
|
||||
if (i < change._tree.length && change._tree[i] !== null) {
|
||||
$treeCell.css('background-image',
|
||||
'url(' + LineImage + ')')
|
||||
.css('background-repeat', 'repeat-y')
|
||||
}
|
||||
|
||||
if (i === change._tree_index) {
|
||||
$treeCell.append(
|
||||
this.change_status_icon(change))
|
||||
}
|
||||
if (change._tree_branches.indexOf(i) !== -1) {
|
||||
let $image = $('<img />')
|
||||
.css('vertical-align', 'baseline')
|
||||
if (change._tree_branches.indexOf(i) ===
|
||||
change._tree_branches.length - 1) {
|
||||
// Angle line
|
||||
$image.attr('src', LineAngleImage)
|
||||
} else {
|
||||
// T line
|
||||
$image.attr('src', LineTImage)
|
||||
}
|
||||
$treeCell.append($image)
|
||||
}
|
||||
$changeRow.append($treeCell)
|
||||
}
|
||||
|
||||
let changeWidth = 360 - 16 * changeQueue._tree_columns
|
||||
let $changeColumn = $('<td />')
|
||||
.css('width', changeWidth + 'px')
|
||||
.addClass('zuul-change-cell')
|
||||
.append(this.changePanel(change))
|
||||
|
||||
$changeRow.append($changeColumn)
|
||||
|
||||
let $changeTable = $('<table />')
|
||||
.addClass('zuul-change-box')
|
||||
.css('-moz-box-sizing', 'content-box')
|
||||
.css('box-sizing', 'content-box')
|
||||
.append($changeRow)
|
||||
|
||||
return $changeTable
|
||||
},
|
||||
|
||||
pipeline_sparkline: function (pipelineName) {
|
||||
if (options.graphite_url !== '') {
|
||||
let $sparkline = $('<img />')
|
||||
.addClass('pull-right')
|
||||
.attr('src', getSparklineURL(pipelineName))
|
||||
return $sparkline
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
pipeline_header: function (pipeline, count) {
|
||||
// Format the pipeline name, sparkline and description
|
||||
let $headerDiv = $('<div />')
|
||||
.addClass('zuul-pipeline-header')
|
||||
|
||||
let $heading = $('<h3 />')
|
||||
.css('vertical-align', 'middle')
|
||||
.text(pipeline.name)
|
||||
.append(
|
||||
$('<span />')
|
||||
.addClass('badge pull-right')
|
||||
.css('vertical-align', 'middle')
|
||||
.css('margin-top', '0.5em')
|
||||
.text(count)
|
||||
)
|
||||
.append(this.pipeline_sparkline(pipeline.name))
|
||||
|
||||
$headerDiv.append($heading)
|
||||
|
||||
if (typeof pipeline.description === 'string') {
|
||||
let descr = $('<small />')
|
||||
$.each(pipeline.description.split(/\r?\n\r?\n/),
|
||||
function (index, descrPart) {
|
||||
descr.append($('<p />').text(descrPart))
|
||||
})
|
||||
$headerDiv.append($('<p />').append(descr))
|
||||
}
|
||||
return $headerDiv
|
||||
},
|
||||
|
||||
pipeline: function (pipeline, count) {
|
||||
let format = this
|
||||
let $html = $('<div />')
|
||||
.addClass('zuul-pipeline col-md-4')
|
||||
.append(this.pipeline_header(pipeline, count))
|
||||
|
||||
$.each(pipeline.change_queues, function (queueIndex, changeQueue) {
|
||||
$.each(changeQueue.heads, function (headIndex, changes) {
|
||||
let $changeQueueHtml = $('<div />')
|
||||
.addClass('change-queue')
|
||||
.data('zuul-pipeline', pipeline.name)
|
||||
$html.append($changeQueueHtml)
|
||||
|
||||
if (pipeline.change_queues.length > 1 && headIndex === 0) {
|
||||
let name = changeQueue.name
|
||||
let shortName = name
|
||||
if (shortName.length > 32) {
|
||||
shortName = shortName.substr(0, 32) + '...'
|
||||
}
|
||||
$changeQueueHtml.append($('<p />')
|
||||
.text('Queue: ')
|
||||
.append(
|
||||
$('<abbr />')
|
||||
.attr('title', name)
|
||||
.text(shortName)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let $changeBoxes = $.map(changes, function (change) {
|
||||
return format.change_with_status_tree(change, changeQueue)
|
||||
})
|
||||
|
||||
let visible = $.map($changeBoxes, function (changeBox) {
|
||||
$changeQueueHtml.append(changeBox)
|
||||
return format.display_patchset(changeBox)
|
||||
}).some(function (visible) {
|
||||
return visible
|
||||
})
|
||||
|
||||
if (!visible) $changeQueueHtml.remove()
|
||||
})
|
||||
})
|
||||
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()
|
||||
|
||||
let showPanel = true
|
||||
|
||||
if (currentFilter !== '') {
|
||||
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)
|
||||
}
|
||||
|
||||
return showPanel
|
||||
}
|
||||
}
|
||||
|
||||
let app = {
|
||||
schedule: function (app) {
|
||||
app = app || this
|
||||
if (!options.enabled) {
|
||||
app.timer = setTimeout(function () { app.schedule(app) }, 5000)
|
||||
return
|
||||
}
|
||||
app.update().always(function () {
|
||||
app.timer = setTimeout(function () { app.schedule(app) }, 5000)
|
||||
})
|
||||
|
||||
// Only update graphs every minute
|
||||
if (zuulGraphUpdateCount > 11) {
|
||||
zuulGraphUpdateCount = 0
|
||||
$.zuul.update_sparklines()
|
||||
}
|
||||
},
|
||||
injest: function (data, $msg) {
|
||||
if ('message' in data) {
|
||||
$msg.removeClass('alert-danger')
|
||||
.addClass('alert-info')
|
||||
.text(data.message)
|
||||
.show()
|
||||
} else {
|
||||
$msg.empty()
|
||||
.hide()
|
||||
}
|
||||
|
||||
if ('zuul_version' in data) {
|
||||
$('#zuul-version-span').text(data.zuul_version)
|
||||
}
|
||||
if ('last_reconfigured' in data) {
|
||||
let lastReconfigured =
|
||||
new Date(data.last_reconfigured)
|
||||
$('#last-reconfigured-span').text(
|
||||
lastReconfigured.toString())
|
||||
}
|
||||
|
||||
let $pipelines = $(options.pipelines_id)
|
||||
$pipelines.html('')
|
||||
$.each(data.pipelines, function (i, pipeline) {
|
||||
let count = app.create_tree(pipeline)
|
||||
$pipelines.append(
|
||||
format.pipeline(pipeline, count))
|
||||
})
|
||||
|
||||
$(options.queue_events_num).text(
|
||||
data.trigger_event_queue
|
||||
? data.trigger_event_queue.length : '0'
|
||||
)
|
||||
$(options.queue_results_num).text(
|
||||
data.result_event_queue
|
||||
? data.result_event_queue.length : '0'
|
||||
)
|
||||
},
|
||||
/** @return {jQuery.Promise} */
|
||||
update: function () {
|
||||
// Cancel the previous update if it hasn't completed yet.
|
||||
if (xhr) {
|
||||
xhr.abort()
|
||||
}
|
||||
|
||||
this.emit('update-start')
|
||||
let app = this
|
||||
|
||||
let $msg = $(options.msg_id)
|
||||
if (options.source_data !== null) {
|
||||
app.injest(options.source_data, $msg)
|
||||
return
|
||||
}
|
||||
xhr = $.getJSON(options.source)
|
||||
.done(function (data) {
|
||||
app.injest(data, $msg)
|
||||
})
|
||||
.fail(function (jqXHR, statusText, errMsg) {
|
||||
if (statusText === 'abort') {
|
||||
return
|
||||
}
|
||||
$msg.text(options.source + ': ' + errMsg)
|
||||
.addClass('alert-danger')
|
||||
.removeClass('zuul-msg-wrap-off')
|
||||
.show()
|
||||
})
|
||||
.always(function () {
|
||||
xhr = undefined
|
||||
app.emit('update-end')
|
||||
})
|
||||
|
||||
return xhr
|
||||
},
|
||||
|
||||
update_sparklines: function () {
|
||||
$.each(zuulSparklineURLs, function (name, url) {
|
||||
let newimg = new Image()
|
||||
let parts = url.split('#')
|
||||
newimg.src = parts[0] + '#' + new Date().getTime()
|
||||
$(newimg).load(function () {
|
||||
zuulSparklineURLs[name] = newimg.src
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
emit: function () {
|
||||
$jq.trigger.apply($jq, arguments)
|
||||
return this
|
||||
},
|
||||
on: function () {
|
||||
$jq.on.apply($jq, arguments)
|
||||
return this
|
||||
},
|
||||
one: function () {
|
||||
$jq.one.apply($jq, arguments)
|
||||
return this
|
||||
},
|
||||
|
||||
controlForm: function () {
|
||||
// Build the filter form filling anything from cookies
|
||||
|
||||
let $controlForm = $('<form />')
|
||||
.attr('role', 'form')
|
||||
.addClass('form-inline')
|
||||
.submit(this.handleFilterChange)
|
||||
|
||||
$controlForm
|
||||
.append(this.filterFormGroup())
|
||||
.append(this.expandFormGroup())
|
||||
|
||||
return $controlForm
|
||||
},
|
||||
|
||||
filterFormGroup: function () {
|
||||
// Update the filter form with a clear button if required
|
||||
|
||||
let $label = $('<label />')
|
||||
.addClass('control-label')
|
||||
.attr('for', 'filter_string')
|
||||
.text('Filters')
|
||||
.css('padding-right', '0.5em')
|
||||
|
||||
let $input = $('<input />')
|
||||
.attr('type', 'text')
|
||||
.attr('id', 'filter_string')
|
||||
.addClass('form-control')
|
||||
.attr('title',
|
||||
'project(s), pipeline(s) or review(s) comma ' +
|
||||
'separated')
|
||||
.attr('value', currentFilter)
|
||||
|
||||
$input.change(this.handleFilterChange)
|
||||
|
||||
let $clearIcon = $('<span />')
|
||||
.addClass('form-control-feedback')
|
||||
.addClass('glyphicon glyphicon-remove-circle')
|
||||
.attr('id', 'filter_form_clear_box')
|
||||
.attr('title', 'clear filter')
|
||||
.css('cursor', 'pointer')
|
||||
|
||||
$clearIcon.click(function () {
|
||||
$('#filter_string').val('').change()
|
||||
})
|
||||
|
||||
if (currentFilter === '') {
|
||||
$clearIcon.hide()
|
||||
}
|
||||
|
||||
let $formGroup = $('<div />')
|
||||
.addClass('form-group has-feedback')
|
||||
.append($label, $input, $clearIcon)
|
||||
return $formGroup
|
||||
},
|
||||
|
||||
expandFormGroup: function () {
|
||||
let expandByDefault = (
|
||||
readCookie('zuul_expand_by_default', false) === 'true')
|
||||
|
||||
let $checkbox = $('<input />')
|
||||
.attr('type', 'checkbox')
|
||||
.attr('id', 'expand_by_default')
|
||||
.prop('checked', expandByDefault)
|
||||
.change(this.handleExpandByDefault)
|
||||
|
||||
let $label = $('<label />')
|
||||
.css('padding-left', '1em')
|
||||
.html('Expand by default: ')
|
||||
.append($checkbox)
|
||||
|
||||
let $formGroup = $('<div />')
|
||||
.addClass('checkbox')
|
||||
.append($label)
|
||||
return $formGroup
|
||||
},
|
||||
|
||||
handleFilterChange: function () {
|
||||
// Update the filter and save it to a cookie
|
||||
currentFilter = $('#filter_string').val()
|
||||
setCookie('zuul_filter_string', currentFilter)
|
||||
if (currentFilter === '') {
|
||||
$('#filter_form_clear_box').hide()
|
||||
} else {
|
||||
$('#filter_form_clear_box').show()
|
||||
}
|
||||
|
||||
this.update()
|
||||
return false
|
||||
},
|
||||
|
||||
handleExpandByDefault: function (e) {
|
||||
// Handle toggling expand by default
|
||||
setCookie('zuul_expand_by_default', e.target.checked)
|
||||
collapsedExceptions = []
|
||||
$('.zuul-change-box').each(function (index, obj) {
|
||||
let $changeBox = $(obj)
|
||||
format.display_patchset($changeBox, 200)
|
||||
})
|
||||
},
|
||||
|
||||
create_tree: function (pipeline) {
|
||||
let count = 0
|
||||
let pipelineMaxTreeColumns = 1
|
||||
$.each(pipeline.change_queues,
|
||||
function (changeQueueIndex, changeQueue) {
|
||||
let tree = []
|
||||
let maxTreeColumns = 1
|
||||
let changes = []
|
||||
let lastTreeLength = 0
|
||||
$.each(changeQueue.heads, function (headIndex, head) {
|
||||
$.each(head, function (changeIndex, change) {
|
||||
changes[change.id] = change
|
||||
change._tree_position = changeIndex
|
||||
})
|
||||
})
|
||||
$.each(changeQueue.heads, function (headIndex, head) {
|
||||
$.each(head, function (changeIndex, change) {
|
||||
if (change.live === true) {
|
||||
count += 1
|
||||
}
|
||||
let idx = tree.indexOf(change.id)
|
||||
if (idx > -1) {
|
||||
change._tree_index = idx
|
||||
// remove...
|
||||
tree[idx] = null
|
||||
while (tree[tree.length - 1] === null) {
|
||||
tree.pop()
|
||||
}
|
||||
} else {
|
||||
change._tree_index = 0
|
||||
}
|
||||
change._tree_branches = []
|
||||
change._tree = []
|
||||
if (typeof (change.items_behind) === 'undefined') {
|
||||
change.items_behind = []
|
||||
}
|
||||
change.items_behind.sort(function (a, b) {
|
||||
return (changes[b]._tree_position - changes[a]._tree_position)
|
||||
})
|
||||
$.each(change.items_behind, function (i, id) {
|
||||
tree.push(id)
|
||||
if (tree.length > lastTreeLength && lastTreeLength > 0) {
|
||||
change._tree_branches.push(tree.length - 1)
|
||||
}
|
||||
})
|
||||
if (tree.length > maxTreeColumns) {
|
||||
maxTreeColumns = tree.length
|
||||
}
|
||||
if (tree.length > pipelineMaxTreeColumns) {
|
||||
pipelineMaxTreeColumns = tree.length
|
||||
}
|
||||
change._tree = tree.slice(0) // make a copy
|
||||
lastTreeLength = tree.length
|
||||
})
|
||||
})
|
||||
changeQueue._tree_columns = maxTreeColumns
|
||||
})
|
||||
pipeline._tree_columns = pipelineMaxTreeColumns
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
$jq = $(app)
|
||||
return {
|
||||
options: options,
|
||||
format: format,
|
||||
app: app,
|
||||
jq: $jq
|
||||
}
|
||||
}
|
||||
}(jQuery))
|
@ -1,32 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<div class="container">
|
||||
<div class="zuul-container" id="zuul-container">
|
||||
<div style="display: none;" class="alert" id="zuul_msg"></div>
|
||||
<button class="btn pull-right zuul-spinner">
|
||||
updating <span class="glyphicon glyphicon-refresh"></span>
|
||||
</button>
|
||||
<p>Queue lengths:
|
||||
<span id="zuul_queue_events_num">0</span> events,
|
||||
<span id="zuul_queue_management_events_num">0</span> management events,
|
||||
<span id="zuul_queue_results_num">0</span> results.
|
||||
</p>
|
||||
<div id="zuul_controls"></div>
|
||||
<div id="zuul_pipelines" class="row"></div>
|
||||
<p>Zuul version: <span id="zuul-version-span"></span></p>
|
||||
<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>
|
||||
</div>
|
||||
</div>
|
@ -1,57 +0,0 @@
|
||||
// Copyright 2018 Red Hat, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
import zuulStart from './zuulStart'
|
||||
|
||||
interface ZuulStatusOption {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface ZuulStatus {
|
||||
options: ZuulStatusOption
|
||||
timer: number
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: require('./status.component.html')
|
||||
})
|
||||
export default class StatusComponent implements OnInit, OnDestroy {
|
||||
tenant: string
|
||||
app: ZuulStatus
|
||||
|
||||
constructor(private route: ActivatedRoute, private zuul: ZuulService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
|
||||
|
||||
if (typeof this.app === 'undefined') {
|
||||
this.app = zuulStart(
|
||||
jQuery, this.zuul)
|
||||
}
|
||||
this.app.options.enabled = true
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.options.enabled = false
|
||||
if (typeof this.app.timer !== 'undefined') {
|
||||
clearTimeout(this.app.timer)
|
||||
this.app.timer = 0
|
||||
}
|
||||
jQuery(document).off()
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
/* global URL, DemoStatusBasic, DemoStatusOpenStack, DemoStatusTree, BuiltinConfig */
|
||||
// Client script for Zuul status page
|
||||
//
|
||||
// Copyright 2013 OpenStack Foundation
|
||||
// Copyright 2013 Timo Tijhof
|
||||
// Copyright 2013 Wikimedia Foundation
|
||||
// Copyright 2014 Rackspace Australia
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import 'jquery-visibility/jquery-visibility'
|
||||
import 'graphitejs/jquery.graphite.js'
|
||||
|
||||
import './jquery.zuul'
|
||||
|
||||
/**
|
||||
* @return The $.zuul instance
|
||||
*/
|
||||
function zuulStart ($, zuulService) {
|
||||
// Start the zuul app (expects default dom)
|
||||
|
||||
let $container, $indicator
|
||||
|
||||
let url = new URL(window.location)
|
||||
let params = {
|
||||
// graphite_url: 'http://graphite.openstack.org/render/'
|
||||
}
|
||||
|
||||
if (typeof BuiltinConfig !== 'undefined') {
|
||||
params['source'] = BuiltinConfig.api_endpoint + '/' + 'status'
|
||||
} else if (url.searchParams.has('source_url')) {
|
||||
params['source'] = url.searchParams.get('source_url') + '/' + 'status'
|
||||
} else if (url.searchParams.has('demo')) {
|
||||
let demo = url.searchParams.get('demo') || 'basic'
|
||||
if (demo === 'basic') {
|
||||
params['source_data'] = DemoStatusBasic
|
||||
} else if (demo === 'openstack') {
|
||||
params['source_data'] = DemoStatusOpenStack
|
||||
} else if (demo === 'tree') {
|
||||
params['source_data'] = DemoStatusTree
|
||||
}
|
||||
} else {
|
||||
params['source'] = zuulService.getSourceUrl('status')
|
||||
}
|
||||
|
||||
let zuul = $.zuul(params, zuulService)
|
||||
|
||||
zuul.jq.on('update-start', function () {
|
||||
$container.addClass('zuul-container-loading')
|
||||
$indicator.addClass('zuul-spinner-on')
|
||||
})
|
||||
|
||||
zuul.jq.on('update-end', function () {
|
||||
$container.removeClass('zuul-container-loading')
|
||||
setTimeout(function () {
|
||||
$indicator.removeClass('zuul-spinner-on')
|
||||
}, 500)
|
||||
})
|
||||
|
||||
zuul.jq.one('update-end', function () {
|
||||
// Do this asynchronous so that if the first update adds a
|
||||
// message, it will not animate while we fade in the content.
|
||||
// Instead it simply appears with the rest of the content.
|
||||
setTimeout(function () {
|
||||
// Fade in the content
|
||||
$container.addClass('zuul-container-ready')
|
||||
})
|
||||
})
|
||||
|
||||
$(function ($) {
|
||||
// DOM ready
|
||||
$container = $('#zuul-container')
|
||||
$indicator = $('#zuul-spinner')
|
||||
$('#zuul_controls').append(zuul.app.controlForm())
|
||||
|
||||
zuul.app.schedule()
|
||||
|
||||
$(document).on({
|
||||
'show.visibility': function () {
|
||||
zuul.options.enabled = true
|
||||
zuul.app.update()
|
||||
},
|
||||
'hide.visibility': function () {
|
||||
zuul.options.enabled = false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return zuul
|
||||
}
|
||||
|
||||
export default zuulStart
|
@ -1,18 +0,0 @@
|
||||
#zuulstreamoverlay {
|
||||
float: right;
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: white;
|
||||
padding: 2px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
pre#zuulstreamcontent {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 0px 10px;
|
||||
background-color: black;
|
||||
color: lightgrey;
|
||||
border: none;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 BMW Car IT GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<div class="container-fluid">
|
||||
<span id="zuulstreamoverlay">
|
||||
<form>
|
||||
<input type="checkbox" id="autoscroll" checked> autoscroll
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<pre id="zuulstreamcontent"></pre>
|
@ -1,90 +0,0 @@
|
||||
// Copyright 2018 Red Hat, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
declare var BuiltinConfig: object
|
||||
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
|
||||
function escapeLog (text) {
|
||||
const pattern = /[<>&"']/g
|
||||
|
||||
return text.replace(pattern, function (match) {
|
||||
return '&#' + match.charCodeAt(0) + ';'
|
||||
})
|
||||
}
|
||||
|
||||
@Component({
|
||||
styles: [require('./stream.component.css').toString()],
|
||||
template: require('./stream.component.html')
|
||||
})
|
||||
export default class StreamComponent implements OnInit {
|
||||
|
||||
constructor(private route: ActivatedRoute, private zuul: ZuulService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
|
||||
this.startStream()
|
||||
}
|
||||
|
||||
startStream () {
|
||||
const pageUpdateInMS = 250
|
||||
let receiveBuffer = ''
|
||||
|
||||
setInterval(function () {
|
||||
console.log('autoScroll')
|
||||
if (receiveBuffer !== '') {
|
||||
document.getElementById('zuulstreamcontent').innerHTML += receiveBuffer
|
||||
receiveBuffer = ''
|
||||
if ((<HTMLInputElement>document.getElementById('autoscroll')).checked) {
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
}
|
||||
}
|
||||
}, pageUpdateInMS)
|
||||
|
||||
const queryParamMap = this.route.snapshot.queryParamMap
|
||||
|
||||
const params = {
|
||||
uuid: queryParamMap.get('uuid')
|
||||
}
|
||||
if (queryParamMap.has('logfile')) {
|
||||
params['logfile'] = queryParamMap.get('logfile')
|
||||
const logfileSuffix = `(${params['logfile']})`
|
||||
}
|
||||
if (typeof BuiltinConfig !== 'undefined') {
|
||||
params['websocket_url'] = BuiltinConfig['websocket_url']
|
||||
} else if (queryParamMap.has('websocket_url')) {
|
||||
params['websocket_url'] = queryParamMap.get('websocket_url')
|
||||
} else {
|
||||
params['websocket_url'] = this.zuul.getWebsocketUrl('console-stream')
|
||||
}
|
||||
const ws = new WebSocket(params['websocket_url'])
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
console.log('onmessage')
|
||||
receiveBuffer = receiveBuffer + escapeLog(event.data)
|
||||
}
|
||||
|
||||
ws.onopen = function (event) {
|
||||
console.log('onopen')
|
||||
ws.send(JSON.stringify(params))
|
||||
}
|
||||
|
||||
ws.onclose = function (event) {
|
||||
console.log('onclose')
|
||||
receiveBuffer = receiveBuffer + '\n--- END OF STREAM ---\n'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
@import url('~bootstrap/dist/css/bootstrap.css');
|
||||
|
||||
.zuul-change {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.zuul-change-id {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.zuul-job-result {
|
||||
float: right;
|
||||
width: 70px;
|
||||
height: 15px;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.zuul-change-total-result {
|
||||
height: 10px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.zuul-spinner,
|
||||
.zuul-spinner:hover {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-out;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zuul-spinner-on,
|
||||
.zuul-spinner-on:hover {
|
||||
opacity: 1;
|
||||
transition-duration: 0.2s;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.zuul-change-cell {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.zuul-change-job {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.zuul-job-name {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.zuul-non-voting-desc {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.zuul-patchset-header {
|
||||
font-size: small;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-inline > .form-group {
|
||||
padding-right: 5px;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
class Tenant {
|
||||
name: string
|
||||
projects: number
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
<!--
|
||||
Copyright 2017 Red Hat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<div class="container-fluid">
|
||||
<!-- TODO(mordred) Make navigation smarter to handle tenants list -->
|
||||
<table class="table table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Jobs</th>
|
||||
<th>Builds</th>
|
||||
<th>Projects count</th>
|
||||
<th>Queue size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let tenant of tenants">
|
||||
<td>{{ tenant.name }}</td>
|
||||
<td><a [routerLink]="['/t', tenant.name, 'status.html']">status</a></td>
|
||||
<td><a [routerLink]="['/t', tenant.name, 'jobs.html']">jobs</a></td>
|
||||
<td><a [routerLink]="['/t', tenant.name, 'builds.html']">builds</a></td>
|
||||
<td>{{ tenant.projects }}</td>
|
||||
<td>{{ tenant.queue }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -1,38 +0,0 @@
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
|
||||
import ZuulService from '../zuul/zuul.service'
|
||||
|
||||
@Component({
|
||||
template: require('./tenants.component.html')
|
||||
})
|
||||
export default class TenantsComponent implements OnInit {
|
||||
|
||||
tenants: Tenant[]
|
||||
|
||||
constructor(private http: HttpClient, private zuul: ZuulService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.zuul.setTenant()
|
||||
this.tenantsFetch()
|
||||
}
|
||||
|
||||
tenantsFetch(): void {
|
||||
this.http.get<Tenant[]>(this.zuul.getSourceUrl('tenants'))
|
||||
.subscribe(tenants => { this.tenants = tenants })
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
export default class RouteDescription {
|
||||
title: string
|
||||
url: string[]
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
export default class Info {
|
||||
tenant: string
|
||||
whiteLabel: boolean
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// Copyright 2018 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Info from './info'
|
||||
|
||||
export default class InfoResponse {
|
||||
info: Info
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
// Copyright 2017 Red Hat
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import { Injectable, EventEmitter, Output } from '@angular/core'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Router } from '@angular/router'
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject'
|
||||
|
||||
import * as url from 'url'
|
||||
|
||||
import Info from './info'
|
||||
import InfoResponse from './infoResponse'
|
||||
import RouteDescription from './description'
|
||||
|
||||
declare var ZUUL_API_URL: string
|
||||
declare var ZUUL_BASE_HREF: string
|
||||
|
||||
function getAppBaseHrefFromPath () {
|
||||
const path = window.location.pathname
|
||||
if (path.includes('/t/')) {
|
||||
return path.slice(0, path.lastIndexOf('/t/') + 1)
|
||||
} else {
|
||||
return path.split('/').slice(0, -1).join('/') || '/'
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppBaseHref (): string {
|
||||
/*
|
||||
* Return a value suitable for use in
|
||||
* https://angular.io/api/common/APP_BASE_HREF
|
||||
*/
|
||||
let path
|
||||
if (typeof ZUUL_BASE_HREF !== 'undefined') {
|
||||
path = ZUUL_BASE_HREF
|
||||
} else {
|
||||
// Use window.location.pathname because we're looking for a path
|
||||
// prefix, not a URL.
|
||||
path = getAppBaseHrefFromPath()
|
||||
}
|
||||
if (! path.endsWith('/')) {
|
||||
path = path + '/'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ZuulService {
|
||||
public baseApiUrl: string
|
||||
public appBaseHref: string
|
||||
public info: Info
|
||||
navbarRoutes: RouteDescription[]
|
||||
private routePages = ['status', 'jobs', 'builds']
|
||||
|
||||
constructor(private router: Router, private http: HttpClient) {
|
||||
this.baseApiUrl = this.getBaseApiUrl()
|
||||
this.appBaseHref = getAppBaseHref()
|
||||
}
|
||||
|
||||
async setTenant (tenant?: string) {
|
||||
if (!this.info) {
|
||||
const infoEndpoint = this.baseApiUrl + 'api/info'
|
||||
const infoResponse = await this.http.get<InfoResponse>(
|
||||
infoEndpoint).toPromise()
|
||||
this.info = infoResponse.info
|
||||
if (this.info.tenant && !tenant) {
|
||||
this.info.whiteLabel = true
|
||||
} else {
|
||||
this.info.whiteLabel = false
|
||||
}
|
||||
}
|
||||
if (tenant) {
|
||||
this.info.tenant = tenant
|
||||
}
|
||||
this.navbarRoutes = this.getNavbarRoutes()
|
||||
}
|
||||
|
||||
getBaseApiUrl (): string {
|
||||
let path
|
||||
if (typeof ZUUL_API_URL !== 'undefined') {
|
||||
path = ZUUL_API_URL
|
||||
} else {
|
||||
path = url.resolve(window.location.href, getAppBaseHrefFromPath())
|
||||
}
|
||||
if (! path.endsWith('/')) {
|
||||
path = path + '/'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
getSourceUrl (filename: string): string {
|
||||
const tenant = this.info.tenant
|
||||
if (this.info.whiteLabel || filename === 'tenants') {
|
||||
if (!this.info.whiteLabel) {
|
||||
// Reset selected tenant
|
||||
this.info.tenant = ''
|
||||
}
|
||||
return url.resolve(this.baseApiUrl, `api/${filename}`)
|
||||
}
|
||||
if (!tenant) {
|
||||
// No tenant selected, go to tenant list
|
||||
console.log('No tenant selected, navigate to tenants list')
|
||||
this.router.navigate(['/tenants.html'])
|
||||
}
|
||||
return url.resolve(this.baseApiUrl, `api/tenant/${tenant}/${filename}`)
|
||||
}
|
||||
|
||||
getWebsocketUrl (filename: string): string {
|
||||
return this.getSourceUrl(filename)
|
||||
.replace(/(http)(s)?\:\/\//, 'ws$2://')
|
||||
}
|
||||
|
||||
getNavbarRoutes(): RouteDescription[] {
|
||||
const routes = []
|
||||
for (const routePage of this.routePages) {
|
||||
const description: RouteDescription = {
|
||||
title: this.getRouteTitle(routePage),
|
||||
url: this.getRouterLink(routePage)
|
||||
}
|
||||
routes.push(description)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
getRouteTitle(target: string): string {
|
||||
return target.charAt(0).toUpperCase() + target.slice(1)
|
||||
}
|
||||
|
||||
getRouterLink(target: string): string[] {
|
||||
const htmlTarget = target + '.html'
|
||||
if (this.info.whiteLabel) {
|
||||
return ['/' + htmlTarget]
|
||||
} else {
|
||||
return ['/t', this.info.tenant, htmlTarget]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ZuulService
|
@ -1,3 +0,0 @@
|
||||
module.exports = function(env) {
|
||||
return require(`./web/config/webpack.${env}.js`)
|
||||
}
|
@ -24,12 +24,18 @@ _old_from_git = pbr.packaging._from_git
|
||||
def _build_javascript():
|
||||
if subprocess.call(['which', 'yarn']) != 0:
|
||||
return
|
||||
if not os.path.exists('node_modules/.bin/webpack'):
|
||||
subprocess.check_call(['yarn', 'install', '-d'])
|
||||
if not os.path.exists('zuul/web/static/status.html'):
|
||||
subprocess.check_call(['npm', 'run', 'build:dist'])
|
||||
with open('zuul/web/static/__init__.py', 'w'):
|
||||
pass
|
||||
if not os.path.exists('web/node_modules/.bin/webpack'):
|
||||
r = subprocess.Popen(['yarn', 'install', '-d'], cwd="web/").wait()
|
||||
if r:
|
||||
raise RuntimeError("Yarn install failed")
|
||||
if not os.path.exists('web/build/index.html'):
|
||||
r = subprocess.Popen(['yarn', 'build'], cwd="web/").wait()
|
||||
if r:
|
||||
raise RuntimeError("Yarn build failed")
|
||||
# Touch the static paths so that bdist_wheel includes them
|
||||
for path in ('', 'static', 'static/js', 'static/css', 'static/media'):
|
||||
with open(os.path.join('web/build', path, '__init__.py'), 'w'):
|
||||
pass
|
||||
|
||||
|
||||
def _from_git(distribution):
|
||||
|
@ -448,22 +448,24 @@ class ZuulWebAPI(object):
|
||||
cherrypy.request.ws_handler.zuulweb = self.zuulweb
|
||||
|
||||
|
||||
class TenantStaticHandler(object):
|
||||
def __init__(self, path):
|
||||
self._cp_config = {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': path,
|
||||
'tools.staticdir.index': 'status.html',
|
||||
}
|
||||
class StaticHandler(object):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
|
||||
|
||||
class RootStaticHandler(object):
|
||||
def __init__(self, path):
|
||||
self._cp_config = {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': path,
|
||||
'tools.staticdir.index': 'tenants.html',
|
||||
}
|
||||
def default(self, path, **kwargs):
|
||||
# Try to handle static file first
|
||||
handled = cherrypy.lib.static.staticdir(
|
||||
section="",
|
||||
dir=self.root,
|
||||
index='index.html')
|
||||
if not path or not handled:
|
||||
# When not found, serve the index.html
|
||||
return cherrypy.lib.static.serve_file(
|
||||
path=os.path.join(self.root, "index.html"),
|
||||
content_type="text/html")
|
||||
else:
|
||||
return cherrypy.lib.static.serve_file(
|
||||
path=os.path.join(self.root, path))
|
||||
|
||||
|
||||
class StreamManager(object):
|
||||
@ -556,8 +558,6 @@ class ZuulWeb(object):
|
||||
|
||||
route_map = cherrypy.dispatch.RoutesDispatcher()
|
||||
api = ZuulWebAPI(self)
|
||||
tenant_static = TenantStaticHandler(self.static_path)
|
||||
root_static = RootStaticHandler(self.static_path)
|
||||
route_map.connect('api', '/api/info',
|
||||
controller=api, action='info')
|
||||
route_map.connect('api', '/api/tenants',
|
||||
@ -600,10 +600,10 @@ class ZuulWeb(object):
|
||||
'/api/connection/%s' % connection.connection_name)
|
||||
|
||||
# Add fallthrough routes at the end for the static html/js files
|
||||
route_map.connect('root_static', '/{path:.*}',
|
||||
controller=root_static, action='default')
|
||||
route_map.connect('tenant_static', '/t/{tenant}/{path:.*}',
|
||||
controller=tenant_static, action='default')
|
||||
route_map.connect(
|
||||
'root_static', '/{path:.*}',
|
||||
controller=StaticHandler(self.static_path),
|
||||
action='default')
|
||||
|
||||
conf = {
|
||||
'/': {
|
||||
|