blob: 7caf8818dd7ccf414a9a6a5c057b6693b2e4ee6b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
const DEFAULT_UPDATE_INTERVAL_MS = 1000 * 5;
const BASE_URL = 'https://cr-buildbucket.appspot.com/_ah/api/buildbucket/v1';
const MAX_UPDATE_INTERVAL_MS = 1000 * 60 * 5;
Polymer({
is: 'cr-buildbucket-view',
properties: {
baseUrl: {
type: String,
value: BASE_URL,
},
plugin: Object,
projectName: String,
change: Object,
revision: Object,
_validPatchNums: {
type: Array,
computed: '_computeValidPatchNums(change, revision._number)',
},
_builds: Array,
_pluginConfig: Object,
_expBuilds: {
type: Array,
value() { return []; },
},
_buildTags: {
type: Array,
computed: '_computeBuildTags(_pluginConfig, change._number, ' +
'_validPatchNums)',
},
_loading: {
type: Boolean,
value: false,
},
_loggedIn: {
type: Boolean,
value: false,
},
_lastUpdate: String,
_showExpBuilds: {
type: Boolean,
value: false,
},
_needsUpdate: {
type: Boolean,
value: false,
},
_updateIntervalMs: {
type: Number,
value: DEFAULT_UPDATE_INTERVAL_MS,
},
_updateTimeoutID: Number,
},
attached() {
this.listen(document, 'visibilitychange', '_handleVisibilityChange');
if (!this.change) {
console.warn('element attached without change property set.');
return;
}
this.reload();
},
/**
* Reset the state of the element and fetch and display builds.
*
* @return {Promise} Resolves upon completion.
*/
reload() {
this._loading = true;
const pluginName = this.plugin.getPluginName();
const project = this.change.project;
return this.$.client.getConfig(project, pluginName).then((config) => {
this._pluginConfig = config;
}).then(() => {
if (this._pluginConfig.buckets > 0) {
this._refreshBuilds().then(() => { this._loading = false; });
} else {
console.log('Buildbucket plugin not configured for this project.');
}
});
},
_handleAuthChange(e) {
this._loggedIn = !!e.detail.loggedIn;
},
/**
* Fetch builds from Buildbucket and update the element state.
*
* @return {Promise} Resolves upon completion.
*/
_refreshBuilds() {
return this.$.client.getOAuthToken().then(() => {
return this.$.client.getBuilds(this._buildTags).then(
(allBuilds) => {
this._updateBuilds(allBuilds);
}).catch((err) => {
this._updateIntervalMs = Math.min(MAX_UPDATE_INTERVAL_MS,
(1 + Math.random()) * this._updateIntervalMs * 2);
console.error(err);
}).then(() => {
this._updateTimeoutID =
window.setTimeout(this._updateTimerFired.bind(this),
this._updateIntervalMs);
});
});
},
/**
* Update element state based on the given fetched builds.
*
* @param {Object[]} allBuilds Array of fetched build objects.
*/
_updateBuilds(allBuilds) {
const builds = [];
const experimentalBuilds = [];
allBuilds.forEach((b) => {
let params = {};
try {
params = JSON.parse(b.parameters_json);
} catch (e) {
console.error(e);
}
if (params.properties &&
params.properties.category === 'cq_experimental') {
experimentalBuilds.push(b);
} else {
builds.push(b);
}
});
this._updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS;
this._lastUpdate = new Date().toLocaleTimeString();
this._builds = builds;
this._expBuilds = experimentalBuilds;
},
_handleVisibilityChange(e) {
if (!document.hidden && this._needsUpdate) {
this._refreshBuilds();
this._needsUpdate = false;
}
},
_updateTimerFired() {
if (document.hidden) {
this._needsUpdate = true;
return;
}
this._refreshBuilds();
},
/**
* List numbers of patchsets (revisions) that are applicable.
*
* The reason why this is not just the current patchset number is because
* there may have been a succession of "trivial" changes before the current
* patchset.
*
* @param {Object} change A Gerrit ChangeInfo object.
* @param {number} patchNum Revision number of currently displayed patch.
* @return {number[]} Revision numbers for the displayed builds.
*/
_computeValidPatchNums(change, patchNum) {
const validKinds = ['TRIVIAL_REBASE', 'NO_CHANGE', 'NO_CODE_CHANGE'];
const revisions = [];
for (const revision in change.revisions) {
if (!change.revisions.hasOwnProperty(revision)) { continue; }
revisions.push(change.revisions[revision]);
}
revisions.sort((a, b) => {
// Reverse sort.
return b._number - a._number;
});
const patchNums = [];
for (let i = 0; i < revisions.length; i++) {
if (i == 0 && change.status === 'MERGED') {
// Skip past the most recent patch on submitted CLs because the last
// patchset is always the autogenerated one, which may or may not
// count as a trivial change depending on the submit strategy.
continue;
}
if (revisions[i]._number > patchNum) {
// Patches after the one we're displaying don't count.
continue;
}
patchNums.push(revisions[i]._number);
if (validKinds.indexOf(revisions[i].kind) === -1) {
// If this revision was a non-trivial change,
// don't consider patchsets prior to it.
break;
}
}
return patchNums;
},
/**
* Construct a list of build tags used to query Buildbucket.
*
* @param {Object} config The config object for this project.
* @param {number} changeNum The legacy numeric ID of the change.
* @param {number[]} validPatchNums Patchset numbers to fetch builds for.
* @return {string[]} Build tags to use to query Buildbucket.
*/
_computeBuildTags(config, changeNum, validPatchNums) {
if (!config) {
return [];
}
return validPatchNums.map((patchNum) => {
return 'buildset:patch/gerrit/' + config.gerritHost + '/' +
changeNum + '/' + patchNum;
});
},
_computeTitleText(builds) {
return (builds.length === 0 ? 'No ' : '') + 'Tryjobs';
},
_computeExpBuildsButtonHidden(expBuilds) {
return !expBuilds || expBuilds.length === 0;
},
_computeExpBuildsButtonText(showExpBuilds) {
return (showExpBuilds ? 'Hide' : 'Show') + ' experimental tryjobs';
},
_toggleExpBuilds(e) {
e.preventDefault();
this._showExpBuilds = !this._showExpBuilds;
},
_computeChooseTryJobsHidden(buckets, loggedIn) {
return buckets == null || buckets.length === 0 || !loggedIn;
},
_computeGroupLuci(buckets) {
// Show the LUCI chip only if there are non-LUCI buckets.
return buckets.some(b => !b.name.startsWith('luci.'));
},
_handleChooseTryJobsTap(e) {
e.preventDefault();
this.$.tryJobsPicker.open();
},
_handleChooseTryJobsClose(e) {
e.preventDefault();
this.$.tryJobsPicker.close();
},
/**
* Construct a description string for the given patchset numbers.
*
* @param {number[]} validPatchNums List of patchset numbers.
* @return {string} A description to show in the UI.
* */
_computePatchsetDescriptor(validPatchNums) {
if (!validPatchNums) {
return '';
} else if (validPatchNums.length === 1) {
return 'Showing jobs from patch set ' + validPatchNums[0] + ', ';
} else {
return 'Showing jobs from patch sets ' + Math.min(...validPatchNums) +
'-' + Math.max(...validPatchNums) + ', ';
}
},
});
})();