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>`;