| /** |
| * @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', |
| _legacyUndefinedCheck: true, |
| |
| 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); |
| }, |
| }); |
| })(); |