Migrate to polymer 3
Change-Id: I6cd88920bfa5d7d7f3d6a6cb9d291b3a805965ff
diff --git a/.gitignore b/.gitignore
index 499784b..6ad0d3e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,14 @@
/.classpath
+/.primary_build_tool
/.project
-/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
+/.settings
+/bazel-*
+/eclipse-out
+node_modules
+bower_components
+npm-debug.log
+dist
+fonts
+.tmp
+.vscode
+.DS_Store
\ No newline at end of file
diff --git a/BUILD b/BUILD
index 782e3c0..0a532a8 100644
--- a/BUILD
+++ b/BUILD
@@ -1,4 +1,6 @@
+load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:genrule2.bzl", "genrule2")
load("//tools/bzl:js.bzl", "polygerrit_plugin")
gerrit_plugin(
@@ -10,14 +12,36 @@
"Implementation-Title: Zuul status plugin",
"Implementation-Vendor: Wikimedia Foundation",
],
+ resource_jars = [":zuul-status-static"],
resources = glob(["src/main/**/*"]),
)
-polygerrit_plugin(
- name = "zuul-status-ui",
- srcs = glob([
- "zuul-status/*.html",
- "zuul-status/*.js",
+genrule2(
+ name = "zuul-status-static",
+ srcs = [":zuul_status"],
+ outs = ["zuul-status-static.jar"],
+ cmd = " && ".join([
+ "mkdir $$TMP/static",
+ "cp -r $(locations :zuul_status) $$TMP/static",
+ "cd $$TMP",
+ "zip -Drq $$ROOT/$@ -g .",
]),
- app = "zuul-status/zuul-status.html",
+)
+
+polygerrit_plugin(
+ name = "zuul_status",
+ app = "zuul-status-bundle.js",
+ plugin_name = "zuul-status",
+)
+
+rollup_bundle(
+ name = "zuul-status-bundle",
+ srcs = glob(["zuul-status/*.js"]),
+ entry_point = "zuul-status/plugin.js",
+ format = "iife",
+ rollup_bin = "//tools/node_tools:rollup-bin",
+ sourcemap = "hidden",
+ deps = [
+ "@tools_npm//rollup-plugin-node-resolve",
+ ],
)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
index 59dcff6..d80f679 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuulstatus/Module.java
@@ -27,7 +27,7 @@
@Override
protected void configure() {
DynamicSet.bind(binder(), WebUiPlugin.class)
- .toInstance(new JavaScriptPlugin("zuul-status.html"));
+ .toInstance(new JavaScriptPlugin("zuul-status.js"));
get(PROJECT_KIND, "config").to(GetConfig.class);
diff --git a/src/main/resources/static/zuul-status-view.html b/src/main/resources/static/zuul-status-view.html
deleted file mode 100644
index e50beab..0000000
--- a/src/main/resources/static/zuul-status-view.html
+++ /dev/null
@@ -1,234 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-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.
--->
-
-<dom-module id="zuul-status-view">
- <template>
- <style include="shared-styles">
- #view-container {
- display: block;
- }
- .container {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- }
- .header {
- background-color: var(--table-header-background-color);
- justify-content: space-between;
- min-height: 3.2em;
- padding: .5em var(--default-horizontal-margin, 1rem);
- border-top: 1px solid var(--border-color);
- border-bottom: 1px solid var(--border-color);
- }
- .header .label {
- font: inherit;
- font-family: var(--font-family-bold);
- font-size: 1.17em;
- margin-right: 1em;
- }
- .progress {
- overflow: hidden;
- height: 20px;
- margin-bottom: 20px;
- background-color: #ededed;
- border-radius: 1px;
- -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
- box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
- }
- .progress-bar {
- float: left;
- width: 0;
- height: 100%;
- font-size: 11px;
- line-height: 20px;
- color: #fff;
- text-align: center;
- background-color: #39a5dc;
- -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
- box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
- -webkit-transition: width .6s ease;
- -o-transition: width .6s ease;
- transition: width .6s ease;
- }
- #progressBar {
- width: var(--progress-bar-width, 0%);
- }
- #list {
- padding-bottom: 1em;
- }
- .labels {
- display: inline;
- padding: .2em .6em .3em;
- font-size: 100%;
- font-weight: 600;
- line-height: 1;
- color: #fff;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- border-radius: 0;
- }
- .label-danger {
- background-color: #c00;
- }
- .label-default {
- background-color: #9c9c9c;
- }
- .label-info {
- background-color: #00659c;
- }
- .label-success {
- background-color: #3f9c35;
- }
- .label-warning {
- background-color: #ec7a08;
- }
- .zuul-job-result {
- float: right;
- width: 90px;
- height: 20px;
- margin: 2px 5px 0;
- padding: 4px;
- }
- a {
- color: var(--primary-text-color);
- cursor: pointer;
- display: inline-block;
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- .time, {
- align-items: center;
- color: #757575;
- display: flex;
- }
- #icon {
- margin-right: 0.5em;
- }
- </style>
- <style include="gr-change-list-styles">
- :host {
- font-size: var(--font-size-normal);
- }
- .padding {
- width: var(--default-horizontal-margin);
- padding: 0.5em;
- }
- #changeList {
- border-collapse: collapse;
- width: 100%;
- }
- </style>
- <template is="dom-if" if="[[zuulUrl]]">
- <div id="view-container">
- <div class="header container">
- <div class="container">
- <h3 class="labelName">[[_computeTitle(_response)]]</h3>
- </div>
- <div class="container">
- <template is="dom-if" if="[[!zuulDisable]]" restamp="true">
- <gr-button link on-tap="_handleDisableZuulStatus">Disable Zuul Status</gr-button>
- </template>
- <template is="dom-if" if="[[zuulDisable]]" restamp="true">
- <gr-button link on-tap="_handleEnableZuulStatus">Enable Zuul Status</gr-button>
- </template>
- </div>
- </div>
- <template is="dom-if" if="[[!zuulDisable]]" restamp="true">
- <div class="bottom container">
- <table id="changeList">
- <template is="dom-repeat" items=[[_response]] as="response">
- <tr class="groupHeader">
- <td class="padding">
- <template is="dom-if" if="[[!_isFailing(response)]]" restamp="true">
- <span class="fullStatus success">
- <iron-icon id="icon" icon="gr-icons:check" style="color: #388E3C;"></iron-icon>
- </span>
- </template>
- <template is="dom-if" if="[[_isFailing(response)]]" restamp="true">
- <span class="fullStatus failed">
- <iron-icon id="icon" icon="gr-icons:close" style="color: #D32F2F;"></iron-icon>
- </span>
- </template>
- </td>
- <td class="cell" colspan="15">[[response.name]]</td>
- </tr>
- <template is="dom-repeat" items=[[response.results.jobs]] as="jobs">
- <tr class="table">
- <td class="padding"></td>
- <td class="cell jobName">
- <iron-icon id="icon" icon="zuul-icons:code"></iron-icon>
- <a href="[[_computeReportURL(jobs)]]" target="_blank"
- hidden$="[[!jobs.name]]">
- [[jobs.name]]
- </a>
- </td>
- <td class="cell">
- <div class="time">
- <iron-icon id="icon" icon="zuul-icons:query-builder"></iron-icon>
- <div class="remainingTime">
- [[_getRemainingTime(response.results.remaining_time)]]
- </div>
- </div>
- </td>
- <td class="cell">
- <div class="time">
- <iron-icon id="icon" icon="zuul-icons:today"></iron-icon>
- <div class="enqueueTime">
- [[_getEnqueueTime(response.results.enqueue_time)]]
- </div>
- </div>
- </td>
- <td class="cell progressName">
- <template is="dom-if" if="[[_getResults(jobs, 'true', 'in progress')]]" restamp="true">
- <div class="progress zuul-job-result">
- <div class="progress-bar"
- id="progressBar"
- role="progressbar"
- aria-valuenow$="[[_progressPercent(jobs)]]"
- aria-valuemin="0"
- aria-valuemax="100"></div>
- </div>
- </template>
- <template is="dom-if" if="[[!_getResults(jobs, 'true', 'in progress')]]" restamp="true">
- <span class$="zuul-job-result labels [[_renderJobStatusLabel(jobs)]]">
- [[_getResults(jobs)]]
- </span>
- </template>
- </td>
- </tr>
- </template>
- </template>
- </table>
- </div>
- </template>
- </div>
- </template>
- <iron-iconset-svg name="zuul-icons" size="24">
- <svg>
- <defs>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="code"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="query-builder"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="today"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"></path></g>
- </defs>
- </svg>
- </iron-iconset-svg>
- </template>
- <script src="zuul-status-view.js"></script>
-</dom-module>
diff --git a/src/main/resources/static/zuul-status-view.js b/src/main/resources/static/zuul-status-view.js
deleted file mode 100644
index 2073f78..0000000
--- a/src/main/resources/static/zuul-status-view.js
+++ /dev/null
@@ -1,411 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- (function() {
- 'use strict';
-
- const DEFAULT_UPDATE_INTERVAL_MS = 1000 * 2;
- const MAX_UPDATE_INTERVAL_MS = 1000 * 30 * 2;
-
- /**
- * Wrapper around localStorage to prevent using it if you have it disabled.
- *
- * Temporary until https://gerrit-review.googlesource.com/c/gerrit/+/211472 is merged.
- *
- */
- class ZuulSiteBasedStorage {
- // Returns the local storage.
- _storage() {
- try {
- return window.localStorage;
- } catch (err) {
- console.error('localStorage is disabled with this error ' + err);
- return null;
- }
- }
-
- getItem(key) {
- if (this._storage() === null) { return null; }
- return this._storage().getItem(key);
- }
-
- setItem(key, value) {
- if (this._storage() === null) { return null; }
- return this._storage().setItem(key, value);
- }
-
- removeItem(key) {
- if (this._storage() === null) { return null; }
- return this._storage().removeItem(key);
- }
-
- clear() {
- if (this._storage() === null) { return null; }
- return this._storage().clear();
- }
- }
-
- Polymer({
- is: 'zuul-status-view',
-
- properties: {
- zuulUrl: String,
- zuulTenant: {
- type: String,
- value: null,
- },
- plugin: Object,
- change: Object,
- revision: {
- type: Object,
- observer: 'reload',
- },
- _needsUpdate: {
- type: Boolean,
- value: false,
- },
- _response: Array,
- _updateIntervalMs: {
- type: Number,
- value: DEFAULT_UPDATE_INTERVAL_MS,
- },
- // Start time is the time that this element was loaded,
- // used to determine how long we've been trying to update.
- _startTime: Date,
- _updateTimeoutID: Number,
- _storage: {
- type: Object,
- value: new ZuulSiteBasedStorage(),
- },
- zuulDisable: {
- type: Boolean,
- value: false,
- }
- },
-
- attached() {
- this.listen(document, 'visibilitychange', '_handleVisibilityChange');
- if (!this.change || !this.revision) {
- console.warn('element attached without change and revision set.');
- return;
- }
- },
-
- detached() {
- this._clearUpdateTimeout();
- },
-
- /**
- * Reset the state of the element, then fetch and display progress.
- *
- * @return {Promise} Resolves upon completion.
- */
- async reload() {
- this._response = null;
- this._startTime = new Date();
-
- if (this._storage.getItem('disable_zuul_status')) {
- this.set('zuulDisable', true);
- } else {
- this.set('zuulDisable', false);
- }
-
- const project = this.change.project;
- const plugin = this.plugin.getPluginName();
- const config = await this.getConfig(project, plugin);
- if (config && config.zuul_url) {
- this.zuulUrl = config.zuul_url;
- if (config.zuul_tenant) {
- this.zuulTenant = config.zuul_tenant;
- console.info(`zuul-status: Zuul v3 at ${this.zuulUrl}, tenant ${this.zuulTenant}`);
- } else {
- console.info(`zuul-status: Zuul v2 at ${this.zuulUrl}`);
- }
- } else {
- console.info("No config found for plugin zuul-status");
- }
- if (this.zuulUrl) {
- await this._update();
- }
- },
-
- /**
- * Fetch the config for this plugin
- *
- * @return {Promise} Resolves to the fetched config object,
- * or rejects if the response is non-OK.
- */
- async getConfig(project, plugin) {
- return await this.plugin.restApi().get(
- `/projects/${encodeURIComponent(project)}` +
- `/${encodeURIComponent(plugin)}~config`);
- },
-
- /**
- * Fetch current progress state and update properties.
- *
- * @return {Promise} Resolves upon completion.
- */
- async _update() {
- try {
- const response = await this.getZuulStatus(this.change, this.revision);
- this._response = response.map((results, i) => ({
- name: results.jobs[i].pipeline,
- results,
- }));
- this._updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS;
- } catch (err) {
- this._updateIntervalMs = Math.min(
- MAX_UPDATE_INTERVAL_MS,
- (1 + Math.random()) * this._updateIntervalMs * 2);
- console.warn(err);
- }
- this._resetTimeout();
- },
-
- /**
- * Makes a request to the zuul server to get the status on the change.
- *
- * @param {ChangeInfo} change The current CL.
- * @param {RevisionInfo} revision The current patchset.
- * @return {Promise} Resolves to a fetch Response object.
- */
- async getZuulStatus(change, revision) {
- const response = await this._getReponse(change, revision);
-
- if (response && response.status && response.status === 200) {
- const text = await response.text();
- return await JSON.parse(text);
- }
-
- return [];
- },
-
- /**
- * Makes a GET request to the zuul server.
- *
- * @param {ChangeInfo} change change The current CL.
- * @param {RevisionInfo} revision The current patchset.
- * @return {Promise} Resolves to a fetch Response object.
- */
- async _getReponse(change, revision) {
- if (!change || !revision) return false;
-
- const url = this.zuulTenant === null ?
- `${this.zuulUrl}${change._number},${revision._number}` :
- `${this.zuulUrl}/api/tenant/${this.zuulTenant}/status/change/${change._number},${revision._number}`;
- const options = {method: 'GET'};
-
- return await fetch(url, options);
- },
-
- /**
- * Set a timeout to update again if applicable.
- */
- _resetTimeout() {
- this._clearUpdateTimeout();
-
- if (this._response === []) {
- return;
- }
-
- this._updateTimeoutID = window.setTimeout(
- this._updateTimeoutFired.bind(this),
- this._updateIntervalMs);
- },
-
- _clearUpdateTimeout() {
- if (this._updateTimeoutID) {
- window.clearTimeout(this._updateTimeoutID);
- this._updateTimeoutID = null;
- }
- },
-
- _handleVisibilityChange(e) {
- if (!document.hidden && this._needsUpdate) {
- this._update();
- this._needsUpdate = false;
- }
- },
-
- _updateTimeoutFired() {
- if (document.hidden) {
- this._needsUpdate = true;
- return;
- }
-
- this._update();
- },
-
- /**
- * Return a string to show as the bold title in the UI.
- */
- _computeTitle(response) {
- if (!response) {
- return 'No Zuul Status Results';
- }
-
- return 'Zuul Status';
- },
-
- /**
- * Check whether the response contains report_url.
- *
- * @return {String} True when we are done requesting results.
- */
- _computeReportURL(response) {
-
- if (this.zuulTenant) {
- // Zuul v3 live streaming URL has to be checked early because `report_url` always contains at least a placeholder
- if (response && response.result == null && response.url && response.url.startsWith('stream/')) {
- return `${this.zuulUrl}/t/${this.zuulTenant}/${response.url}`;
- }
- }
-
- if (response && response.report_url) {
- return response.report_url;
- }
-
- return '';
- },
-
- _progressPercent(jobs) {
- if (!jobs || !jobs.elapsed_time && !jobs.remaining_time) {
- return '';
- }
-
- let progressPercent = 100 * (jobs.elapsed_time / (jobs.elapsed_time +
- jobs.remaining_time));
-
- this.customStyle['--progress-bar-width'] = `${progressPercent}%;`;
- this.updateStyles();
-
- return progressPercent;
- },
-
- _getResults(jobs, equals, name) {
- let result = jobs.result ? jobs.result.toLowerCase() : null;
- if (result === null) {
- if (jobs.url === null) {
- result = 'queued'
- } else if (jobs.paused !== null && jobs.paused) {
- result = 'paused'
- } else {
- result = 'in progress'
- }
- }
-
- if (equals) {
- return result === name;
- } else {
- return result;
- }
- },
-
- _renderJobStatusLabel (jobs) {
- let result = jobs.result ? jobs.result.toLowerCase() : null;
-
- 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 className;
- },
-
- _handleDisableZuulStatus(e) {
- this._storage.setItem('disable_zuul_status', 'yes');
-
- this.reload();
- },
-
- _handleEnableZuulStatus(e) {
- this._storage.removeItem('disable_zuul_status');
-
- this.reload();
- },
-
- _isFailing(results) {
- if (results && results.results &&
- results.results.failing_reasons.length !== 0) {
- return true;
- }
-
- return false;
- },
-
- _getTime(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 + ' 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;
- },
-
- _getRemainingTime(time) {
- return time === null ?
- 'unknown' : this._getTime(time, true);
- },
-
- _getEnqueueTime(ms) {
- // Special format case for enqueue time to add style
- let now = Date.now();
- let delta = now - ms;
-
- return this._getTime(delta, true);
- },
- });
-})();
diff --git a/src/main/resources/static/zuul-status.html b/src/main/resources/static/zuul-status.html
deleted file mode 100644
index e72548e..0000000
--- a/src/main/resources/static/zuul-status.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-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.
--->
-
-<link rel="import" href="zuul-status-view.html">
-
-<script>
- (function() {
- 'use strict';
-
- // This plugin is only supported in PolyGerrit from gerrit 2.16+
- if (window.Polymer) {
- Gerrit.install((plugin) => {
-
- plugin.registerCustomComponent(
- 'change-view-integration', 'zuul-status-view');
- });
- }
- })();
-</script>
diff --git a/zuul-status/plugin.js b/zuul-status/plugin.js
new file mode 100644
index 0000000..3dba001
--- /dev/null
+++ b/zuul-status/plugin.js
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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 './zuul-status.js';
+
+Gerrit.install(plugin => {
+ plugin.registerCustomComponent(
+ 'change-view-integration', 'zuul-status');
+});
diff --git a/zuul-status/zuul-status.js b/zuul-status/zuul-status.js
new file mode 100644
index 0000000..57d5647
--- /dev/null
+++ b/zuul-status/zuul-status.js
@@ -0,0 +1,402 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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 {htmlTemplate} from './zuul-status_html.js';
+
+const DEFAULT_UPDATE_INTERVAL_MS = 1000 * 2;
+const MAX_UPDATE_INTERVAL_MS = 1000 * 30 * 2;
+
+class ZuulStatus extends Polymer.Element {
+ static get is() {
+ return 'zuul-status';
+ }
+
+ static get template() {
+ return htmlTemplate;
+ }
+
+ static get properties() {
+ return {
+ zuulUrl: String,
+ zuulTenant: {
+ type: String,
+ value: null,
+ },
+ plugin: Object,
+ change: Object,
+ revision: {
+ type: Object,
+ observer: 'reload',
+ },
+ _needsUpdate: {
+ type: Boolean,
+ value: false,
+ },
+ _response: Object,
+ _updateIntervalMs: {
+ type: Number,
+ value: DEFAULT_UPDATE_INTERVAL_MS,
+ },
+ // Start time is the time that this element was loaded,
+ // used to determine how long we've been trying to update.
+ _startTime: Date,
+ _updateTimeoutID: Number,
+ zuulDisable: {
+ type: Boolean,
+ value: false,
+ }
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
+
+ if (!this.change || !this.revision) {
+ console.warn('element attached without change and revision set.');
+ return;
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+
+ this._clearUpdateTimeout();
+ }
+
+ /**
+ * Reset the state of the element, then fetch and display progress.
+ */
+ async reload() {
+ this._response = null;
+ this._startTime = new Date();
+
+ if (window.localStorage.getItem('disable_zuul_status')) {
+ this.set('zuulDisable', true);
+ } else {
+ this.set('zuulDisable', false);
+ }
+
+ const project = this.change.project;
+ const plugin = this.plugin.getPluginName();
+ const config = await this.getConfig(project, plugin);
+ if (config && config.zuul_url) {
+ this.zuulUrl = config.zuul_url;
+ if (config.zuul_tenant) {
+ this.zuulTenant = config.zuul_tenant;
+ console.info(`zuul-status: Zuul v3 at ${this.zuulUrl}, tenant ${this.zuulTenant}`);
+ } else {
+ console.info(`zuul-status: Zuul v2 at ${this.zuulUrl}`);
+ }
+ } else {
+ console.info("No config found for plugin zuul-status");
+ }
+ if (this.zuulUrl) {
+ await this._update();
+ }
+ }
+
+ /**
+ * Fetch the config for this plugin
+ *
+ * @return {Promise} Resolves to the fetched config object,
+ * or rejects if the response is non-OK.
+ */
+ async getConfig(project, plugin) {
+ return await this.plugin.restApi().get(
+ `/projects/${encodeURIComponent(project)}` +
+ `/${encodeURIComponent(plugin)}~config`);
+ }
+
+ /**
+ * Fetch current progress state and update properties.
+ *
+ * @return {Promise} Resolves upon completion.
+ */
+ async _update() {
+ try {
+ let response = await this.getZuulStatus(this.change, this.revision);
+ response = response.map((results, i) => {
+ if (!results || typeof results.jobs[i] === 'undefined')
+ return;
+
+ return {
+ name: results.jobs[i].pipeline,
+ results,
+ };
+ });
+
+ // We remove undefined keys from the object
+ Object.keys(response).forEach(key => {
+ if (response && typeof response[key] === 'undefined')
+ delete response[key];
+ });
+
+ this._response = response;
+
+ this._updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS;
+ } catch (err) {
+ this._updateIntervalMs = Math.min(
+ MAX_UPDATE_INTERVAL_MS,
+ (1 + Math.random()) * this._updateIntervalMs * 2);
+ console.warn(err);
+ }
+ this._resetTimeout();
+ }
+
+ /**
+ * Makes a request to the zuul server to get the status on the change.
+ *
+ * @param {ChangeInfo} change The current CL.
+ * @param {RevisionInfo} revision The current patchset.
+ * @return {Promise} Resolves to a fetch Response object.
+ */
+ async getZuulStatus(change, revision) {
+ const response = await this._getReponse(change, revision);
+
+ if (response && response.status && response.status === 200) {
+ const text = await response.text();
+ return await JSON.parse(text);
+ }
+
+ return [];
+ }
+
+ /**
+ * Makes a GET request to the zuul server.
+ *
+ * @param {ChangeInfo} change change The current CL.
+ * @param {RevisionInfo} revision The current patchset.
+ * @return {Promise} Resolves to a fetch Response object.
+ */
+ async _getReponse(change, revision) {
+ if (!change || !revision) return false;
+
+ const url = this.zuulTenant === null ?
+ `${this.zuulUrl}${change._number},${revision._number}` :
+ `${this.zuulUrl}/api/tenant/${this.zuulTenant}/status/change/${change._number},${revision._number}`;
+ const options = {method: 'GET'};
+
+ return await fetch(url, options);
+ }
+
+ /**
+ * Set a timeout to update again if applicable.
+ */
+ _resetTimeout() {
+ this._clearUpdateTimeout();
+
+ if (this._response === []) {
+ return;
+ }
+
+ this._updateTimeoutID = window.setTimeout(
+ this._updateTimeoutFired.bind(this),
+ this._updateIntervalMs);
+ }
+
+ _clearUpdateTimeout() {
+ if (this._updateTimeoutID) {
+ window.clearTimeout(this._updateTimeoutID);
+ this._updateTimeoutID = null;
+ }
+ }
+
+ _handleVisibilityChange(e) {
+ if (!document.hidden && this._needsUpdate) {
+ this._update();
+ this._needsUpdate = false;
+ }
+ }
+
+ _updateTimeoutFired() {
+ if (document.hidden) {
+ this._needsUpdate = true;
+ return;
+ }
+
+ this._update();
+ }
+
+ /**
+ * Return a string to show as the bold title in the UI.
+ */
+ _computeTitle(response) {
+ if (!response) {
+ return 'No Zuul Status Results';
+ }
+
+ return 'Zuul Status';
+ }
+
+ /**
+ * Check whether the response contains report_url.
+ *
+ * @return {String} True when we are done requesting results.
+ */
+ _computeReportURL(response) {
+ if (!response) {
+ return '';
+ }
+
+ if (this.zuulTenant) {
+ // Zuul v3 live streaming URL has to be checked early
+ // because `report_url` always contains at least a placeholder
+ if (response.result == null &&
+ response.url && response.url.startsWith('stream/')) {
+ return `${this.zuulUrl}/t/${this.zuulTenant}/${response.url}`;
+ }
+ }
+
+ if (response.report_url) {
+ return response.report_url;
+ }
+
+ return '';
+ }
+
+ _progressPercent(jobs) {
+ if (!jobs || !jobs.elapsed_time && !jobs.remaining_time) {
+ return '';
+ }
+
+ let progressPercent = 100 * (jobs.elapsed_time / (jobs.elapsed_time +
+ jobs.remaining_time));
+
+ this.updateStyles({'--progress-bar-width': `${progressPercent}%`});
+
+ return progressPercent;
+ }
+
+ _getResults(jobs, equals, name) {
+ if (!jobs) return '';
+
+ let result = jobs.result ? jobs.result.toLowerCase() : null;
+ if (result === null) {
+ if (jobs.url === null) {
+ result = 'queued'
+ } else if (jobs.paused !== null && jobs.paused) {
+ result = 'paused'
+ } else {
+ result = 'in progress'
+ }
+ }
+
+ if (equals) {
+ return result === name;
+ } else {
+ return result;
+ }
+ }
+
+ _renderJobStatusLabel (jobs) {
+ if (!jobs) return '';
+
+ let result = jobs.result ? jobs.result.toLowerCase() : null;
+
+ 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 className;
+ }
+
+ _handleDisableZuulStatus(e) {
+ window.localStorage.setItem('disable_zuul_status', 'yes');
+
+ this.reload();
+ }
+
+ _handleEnableZuulStatus(e) {
+ window.localStorage.removeItem('disable_zuul_status');
+
+ this.reload();
+ }
+
+ _isFailing(results) {
+ if (results && results.results &&
+ results.results.failing_reasons.length !== 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ _getTime(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 + ' 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;
+ }
+
+ _getRemainingTime(time) {
+ return time === null ?
+ 'unknown' : this._getTime(time, true);
+ }
+
+ _getEnqueueTime(ms) {
+ // Special format case for enqueue time to add style
+ let now = Date.now();
+ let delta = now - ms;
+
+ return this._getTime(delta, true);
+ }
+}
+
+customElements.define(ZuulStatus.is, ZuulStatus);
diff --git a/zuul-status/zuul-status_html.js b/zuul-status/zuul-status_html.js
new file mode 100644
index 0000000..be07481
--- /dev/null
+++ b/zuul-status/zuul-status_html.js
@@ -0,0 +1,237 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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 const htmlTemplate = Polymer.html`
+<style include="shared-styles">
+ #view-container {
+ display: block;
+ }
+ .container {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .header {
+ background-color: var(--table-header-background-color);
+ justify-content: space-between;
+ min-height: 3.2em;
+ padding: .5em var(--default-horizontal-margin, 1rem);
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ }
+ .header .label {
+ font: inherit;
+ font-family: var(--font-family-bold);
+ font-size: 1.17em;
+ margin-right: 1em;
+ }
+ .progress {
+ overflow: hidden;
+ height: 20px;
+ margin-bottom: 20px;
+ background-color: #ededed;
+ border-radius: 1px;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
+ box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
+ }
+ .progress-bar {
+ float: left;
+ width: 0;
+ height: 100%;
+ font-size: 11px;
+ line-height: 20px;
+ color: #fff;
+ text-align: center;
+ background-color: #39a5dc;
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+ -webkit-transition: width .6s ease;
+ -o-transition: width .6s ease;
+ transition: width .6s ease;
+ }
+ #progressBar {
+ width: var(--progress-bar-width, 0%);
+ }
+ #list {
+ padding-bottom: 1em;
+ }
+ .labels {
+ display: inline;
+ padding: .2em .6em .3em;
+ font-size: 100%;
+ font-weight: 600;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0;
+ }
+ .label-danger {
+ background-color: #c00;
+ }
+ .label-default {
+ background-color: #9c9c9c;
+ }
+ .label-info {
+ background-color: #00659c;
+ }
+ .label-success {
+ background-color: #3f9c35;
+ }
+ .label-warning {
+ background-color: #ec7a08;
+ }
+ .zuul-job-result {
+ float: right;
+ width: 90px;
+ height: 20px;
+ margin: 2px 5px 0;
+ padding: 4px;
+ }
+ a {
+ color: var(--primary-text-color);
+ cursor: pointer;
+ display: inline-block;
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ .time, {
+ align-items: center;
+ color: #757575;
+ display: flex;
+ }
+ #icon {
+ margin-right: 0.5em;
+ }
+</style>
+<style include="gr-change-list-styles">
+ :host {
+ font-size: var(--font-size-normal);
+ }
+ .padding {
+ width: var(--default-horizontal-margin);
+ padding: 0.5em;
+ }
+ #changeList {
+ border-collapse: collapse;
+ width: 100%;
+ }
+</style>
+<template is="dom-if" if="[[zuulUrl]]">
+ <div id="view-container">
+ <div class="header container">
+ <div class="container">
+ <h3 class="labelName">[[_computeTitle(_response)]]</h3>
+ </div>
+ <div class="container">
+ <template is="dom-if" if="[[!zuulDisable]]" restamp="true">
+ <gr-button link on-tap="_handleDisableZuulStatus">Disable Zuul Status</gr-button>
+ </template>
+ <template is="dom-if" if="[[zuulDisable]]" restamp="true">
+ <gr-button link on-tap="_handleEnableZuulStatus">Enable Zuul Status</gr-button>
+ </template>
+ </div>
+ </div>
+ <template is="dom-if" if="[[!zuulDisable]]" restamp="true">
+ <div class="bottom container">
+ <table id="changeList">
+ <template is="dom-repeat" items=[[_response]] as="response">
+ <tbody>
+ <tr class="groupHeader">
+ <td class="padding">
+ <template is="dom-if" if="[[!_isFailing(response)]]" restamp="true">
+ <span class="fullStatus success">
+ <iron-icon id="icon" icon="gr-icons:check" style="color: #388E3C;"></iron-icon>
+ </span>
+ </template>
+ <template is="dom-if" if="[[_isFailing(response)]]" restamp="true">
+ <span class="fullStatus failed">
+ <iron-icon id="icon" icon="gr-icons:close" style="color: #D32F2F;"></iron-icon>
+ </span>
+ </template>
+ </td>
+ <td class="cell" colspan="15">[[response.name]]</td>
+ </tr>
+ </tbody>
+ <template is="dom-repeat" items=[[response.results.jobs]] as="jobs">
+ <tbody class="groupContent">
+ <tr class="groupTitle">
+ <td class="padding"></td>
+ <td class="cell jobName">
+ <iron-icon id="icon" icon="zuul-icons:code"></iron-icon>
+ <a href="[[_computeReportURL(jobs)]]" target="_blank"
+ hidden$="[[!jobs.name]]">
+ [[jobs.name]]
+ </a>
+ </td>
+ <td class="cell">
+ <div class="time">
+ <iron-icon id="icon" icon="zuul-icons:query-builder"></iron-icon>
+ <div class="remainingTime">
+ [[_getRemainingTime(response.results.remaining_time)]]
+ </div>
+ </div>
+ </td>
+ <td class="cell">
+ <div class="time">
+ <iron-icon id="icon" icon="zuul-icons:today"></iron-icon>
+ <div class="enqueueTime">
+ [[_getEnqueueTime(response.results.enqueue_time)]]
+ </div>
+ </div>
+ </td>
+ <td class="cell progressName">
+ <template is="dom-if" if="[[_getResults(jobs, 'true', 'in progress')]]" restamp="true">
+ <div class="progress zuul-job-result">
+ <div class="progress-bar"
+ id="progressBar"
+ role="progressbar"
+ aria-valuenow$="[[_progressPercent(jobs)]]"
+ aria-valuemin="0"
+ aria-valuemax="100"></div>
+ </div>
+ </template>
+ <template is="dom-if" if="[[!_getResults(jobs, 'true', 'in progress')]]" restamp="true">
+ <span class$="zuul-job-result labels [[_renderJobStatusLabel(jobs)]]">
+ [[_getResults(jobs)]]
+ </span>
+ </template>
+ </td>
+ </tr>
+ </tbody>
+ </template>
+ </template>
+ </table>
+ </div>
+ </template>
+ </div>
+</template>
+<iron-iconset-svg name="zuul-icons" size="24">
+ <svg>
+ <defs>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="code"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="query-builder"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="today"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"></path></g>
+ </defs>
+ </svg>
+</iron-iconset-svg>`;