blob: 2073f787b231695c3e0bc48d1f8aaf1af0e861cd [file] [log] [blame]
/**
* @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);
},
});
})();