| /** |
| * @license |
| * Copyright (C) 2020 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 './gr-checks-status.js'; |
| import {statusClass, Statuses} from './gr-checks-all-statuses.js'; |
| |
| const StatusPriorityOrder = [ |
| Statuses.FAILED, |
| Statuses.SCHEDULED, |
| Statuses.RUNNING, |
| Statuses.SUCCESSFUL, |
| Statuses.NOT_STARTED, |
| Statuses.NOT_RELEVANT, |
| ]; |
| |
| const HumanizedStatuses = { |
| // non-terminal statuses |
| NOT_STARTED: 'in progress', |
| NOT_RELEVANT: 'not relevant', |
| SCHEDULED: 'in progress', |
| RUNNING: 'in progress', |
| |
| // terminal statuses |
| SUCCESSFUL: 'successful', |
| FAILED: 'failed', |
| }; |
| |
| const CHECKS_POLL_INTERVAL_MS = 60 * 1000; |
| |
| const Defs = {}; |
| /** |
| * @typedef {{ |
| * _number: number, |
| * }} |
| */ |
| Defs.Change; |
| /** |
| * @typedef {{ |
| * _number: number, |
| * }} |
| */ |
| Defs.Revision; |
| |
| /** |
| * @param {Array<Object>} checks |
| * @returns {Object} |
| */ |
| function computeCheckStatuses(checks) { |
| return checks.reduce((accum, check) => { |
| accum[check.state] || (accum[check.state] = 0); |
| accum[check.state]++; |
| return accum; |
| }, {total: checks.length}); |
| } |
| |
| /** |
| * @param {Array<Object>} checks |
| * @returns {string} |
| */ |
| function downgradeFailureToWarning(checks) { |
| const hasFailedCheck = checks.some( |
| check => { |
| return check.state == Statuses.FAILED; |
| } |
| ); |
| if (!hasFailedCheck) return ''; |
| const hasRequiredFailedCheck = checks.some( |
| check => { |
| return check.state == Statuses.FAILED && |
| check.blocking && check.blocking.length > 0; |
| } |
| ); |
| return hasRequiredFailedCheck ? '' : 'set'; |
| } |
| |
| class GrChecksChipView extends Polymer.GestureEventListeners( |
| Polymer.LegacyElementMixin( |
| Polymer.Element)) { |
| /** @returns {string} name of the component */ |
| static get is() { return 'gr-checks-chip-view'; } |
| |
| /** @returns {?} template for this component */ |
| static get template() { |
| return Polymer.html`<style> |
| |
| :host { |
| display: inline-block; |
| cursor: pointer; |
| } |
| |
| .chip { |
| border-color: #D0D0D0; |
| border-radius: 4px; |
| border-style: solid; |
| border-width: 1px; |
| padding: 4px 8px; |
| } |
| .chip.failed { |
| border-color: #DA4236; |
| } |
| </style> |
| <template is="dom-if" if="[[_hasChecks]]"> |
| Checks: |
| <span |
| class$="[[_chipClasses]]" |
| role="button" |
| aria-label="Open checks tab" |
| > |
| <gr-checks-status |
| status="[[_status]]" |
| downgrade-failure-to-warning="[[_downgradeFailureToWarning]]"> |
| </gr-checks-status> |
| [[_statusString]] |
| </span> |
| </template>`; |
| } |
| |
| /** |
| * Defines properties of the component |
| * |
| * @returns {?} |
| */ |
| static get properties() { |
| return { |
| revision: Object, |
| change: Object, |
| /** @type {function(number, number): !Promise<!Object>} */ |
| getChecks: Function, |
| _checkStatuses: Object, |
| _hasChecks: Boolean, |
| _failedRequiredChecksCount: Number, |
| _status: {type: String, computed: '_computeStatus(_checkStatuses)'}, |
| _statusString: { |
| type: String, |
| computed: '_computeStatusString(_status, _checkStatuses,' + |
| '_failedRequiredChecksCount)', |
| }, |
| _chipClasses: {type: String, computed: '_computeChipClass(_status)'}, |
| // Type is set as string so that it reflects on changes |
| // Polymer does not support reflecting changes in Boolean property |
| _downgradeFailureToWarning: { |
| type: String, |
| value: '', |
| }, |
| pollChecksInterval: Object, |
| visibilityChangeListenerAdded: { |
| type: Boolean, |
| value: false, |
| }, |
| }; |
| } |
| |
| created() { |
| super.created(); |
| this.addEventListener('click', |
| () => this.showChecksTable()); |
| } |
| |
| detached() { |
| super.detached(); |
| clearInterval(this.pollChecksInterval); |
| this.unlisten(document, 'visibilitychange', '_onVisibililityChange'); |
| } |
| |
| static get observers() { |
| return [ |
| '_pollChecksRegularly(change, getChecks)', |
| ]; |
| } |
| |
| showChecksTable() { |
| this.dispatchEvent( |
| new CustomEvent( |
| 'show-checks-table', |
| { |
| bubbles: true, |
| composed: true, |
| detail: { |
| tab: 'change-view-tab-header-checks', |
| scrollIntoView: true, |
| }, |
| }) |
| ); |
| } |
| |
| /** |
| * @param {!Defs.Change} change |
| * @param {function(number, number): !Promise<!Object>} getChecks |
| */ |
| _fetchChecks(change, getChecks) { |
| if (!getChecks || !change) return; |
| |
| // change.current_revision always points to latest patchset |
| getChecks(change._number, change.revisions[change.current_revision] |
| ._number).then(checks => { |
| this.set('_hasChecks', checks.length > 0); |
| if (checks.length > 0) { |
| this._downgradeFailureToWarning = |
| downgradeFailureToWarning(checks); |
| this._failedRequiredChecksCount = |
| this.computeFailedRequiredChecksCount(checks); |
| this._checkStatuses = computeCheckStatuses(checks); |
| } |
| }, error => { |
| this.set('_hasChecks', false); |
| console.error(error); |
| }); |
| } |
| |
| _onVisibililityChange() { |
| if (document.hidden) { |
| clearInterval(this.pollChecksInterval); |
| return; |
| } |
| this._pollChecksRegularly(this.change, this.getChecks); |
| } |
| |
| _pollChecksRegularly(change, getChecks) { |
| if (this.pollChecksInterval) { |
| clearInterval(this.pollChecksInterval); |
| } |
| const poll = () => this._fetchChecks(change, getChecks); |
| poll(); |
| this.pollChecksInterval = setInterval(poll, CHECKS_POLL_INTERVAL_MS); |
| if (!this.visibilityChangeListenerAdded) { |
| this.visibilityChangeListenerAdded = true; |
| this.listen(document, 'visibilitychange', '_onVisibililityChange'); |
| } |
| } |
| |
| /** |
| * @param {!Object} checkStatuses The number of checks in each status. |
| * @return {string} |
| */ |
| _computeStatus(checkStatuses) { |
| return StatusPriorityOrder.find( |
| status => checkStatuses[status] > 0) || |
| Statuses.STATUS_UNKNOWN; |
| } |
| |
| computeFailedRequiredChecksCount(checks) { |
| const failedRequiredChecks = checks.filter( |
| check => { |
| return check.state == Statuses.FAILED && |
| check.blocking && check.blocking.length > 0; |
| } |
| ); |
| return failedRequiredChecks.length; |
| } |
| |
| /** |
| * @param {string} status The overall status of the checks. |
| * @param {!Object} checkStatuses The number of checks in each status. |
| * @param {number} failedRequiredChecksCount The number of failed required checks. |
| * @return {string} |
| */ |
| _computeStatusString(status, checkStatuses, failedRequiredChecksCount) { |
| if (!checkStatuses) return; |
| if (checkStatuses.total === 0) return 'No checks'; |
| let statusString = `${checkStatuses[status]} of ${ |
| checkStatuses.total} checks ${HumanizedStatuses[status]}`; |
| if (status === Statuses.FAILED && failedRequiredChecksCount > 0) { |
| statusString += ` (${failedRequiredChecksCount} required)`; |
| } |
| return statusString; |
| } |
| |
| /** |
| * @param {string} status The overall status of the checks. |
| * @return {string} |
| */ |
| _computeChipClass(status) { |
| return `chip ${statusClass(status)}`; |
| } |
| } |
| |
| customElements.define(GrChecksChipView.is, GrChecksChipView); |