| // Copyright 2020 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. |
| |
| const STATUS_INTERVAL_MS = 60 * 1000; |
| const UNKNOWN_DATE = new Date(0); |
| |
| const DISABLED_BRANCH_TREE_STATUS = { |
| disabled: true, |
| isOpen: true, |
| generalState: 'unknown', |
| message: 'No tree status for this branch.', |
| username: 'Warning', |
| date: UNKNOWN_DATE, |
| url: null, |
| }; |
| |
| export class TreeStatus { |
| constructor(restApi) { |
| this.clear(); |
| this.restApi = restApi; |
| this.updateInterval = window.setInterval( |
| this._updateStatusOnElements.bind(this), STATUS_INTERVAL_MS); |
| } |
| |
| clear() { |
| this.initialized = false; |
| this.config = null; |
| this.elements = []; |
| this.status = null; |
| this.shouldFetchStatus = false; |
| if (this.updateInterval) { |
| window.clearInterval(this.updateInterval); |
| } |
| } |
| |
| async register(element) { |
| await this._ensureInitialized(element.change); |
| element.tree = this; |
| element.config = this.config; |
| this.elements.push(element); |
| this._updateStatusOnElements(); |
| } |
| |
| async _ensureInitialized(change) { |
| if (!change || this.initialized) { |
| return; |
| } |
| this.initialized = true; |
| this.shouldFetchStatus = false; |
| |
| const configUrl = |
| `/projects/${encodeURIComponent(change.project)}/chumpdetector~config`; |
| this.config = await this.restApi.get(configUrl) || null; |
| |
| if (!this.config || !this.config.statusURL) { |
| console.info('Chumpdetector is not configured.'); |
| return; |
| } |
| |
| if (this.config.disabled) { |
| console.info('Chumpdetector is disabled for this project.'); |
| return; |
| } |
| |
| if (this.config.disabledBranchPattern && |
| new RegExp(this.config.disabledBranchPattern).test(change.branch)) { |
| console.info('Disabling Chumpdetector based on branch name.'); |
| this.status = DISABLED_BRANCH_TREE_STATUS; |
| return; |
| } |
| |
| console.info(`Chumpdetector using status url: ${this.config.statusURL}`); |
| |
| // If the configuration specifies loading an image URL first then |
| // request the image. Whether it succeeds or fails immediately |
| // move into setting up the tree status request. This allows users |
| // the opportunity to use the image load to establish cookies the |
| // browser may need in order to make the status request which can |
| // work even without the image request succeeding. |
| const setupTreeStatus = () => { |
| this._updateStatusOnElements(); |
| } |
| if (this.config.preloadImageURL) { |
| const img = new Image(); |
| img.addEventListener("load", setupTreeStatus); |
| img.addEventListener("error", setupTreeStatus); |
| img.src = this.config.preloadImageURL; |
| } |
| |
| this.shouldFetchStatus = true; |
| return; |
| } |
| |
| async _updateStatusOnElements() { |
| if (!this.initialized || !this.shouldFetchStatus) { |
| return; |
| } |
| await this._fetchStatus(); |
| this.elements.forEach(element => { |
| element.status = Object.assign({}, this.status); |
| }); |
| } |
| |
| async _fetchStatus() { |
| // Launch async GET. |
| const options = {}; |
| if (this.config.withCredentials) { |
| options.credentials = 'include'; |
| } |
| const response = await fetch(this.config.statusURL, options); |
| const responseText = await response.text(); |
| console.info('Fetched', this.config.statusURL); |
| |
| if (response.status == 200) { |
| try { |
| const jsonResponse = JSON.parse(responseText); |
| this.status = { |
| disabled: false, |
| isOpen: jsonResponse.can_commit_freely, |
| generalState: jsonResponse.general_state, |
| message: jsonResponse.message, |
| username: jsonResponse.username, |
| date: new Date(jsonResponse.date.replace(' ', 'T')), |
| url: this.config.viewURL, |
| }; |
| } catch (exception) { |
| console.error('Error parsing json: ' + exception); |
| this.status = { |
| disabled: false, |
| isOpen: false, |
| generalState: 'unknown', |
| message: String(exception), |
| username: 'ParseError', |
| date: UNKNOWN_DATE, |
| url: this.config.viewURL, |
| }; |
| } |
| return; |
| } |
| |
| this.status = { |
| disabled: false, |
| isOpen: false, |
| generalState: 'unknown', |
| username: 'RequestError', |
| date: UNKNOWN_DATE, |
| }; |
| |
| if (!this.config.withCredentials) { |
| this.status.url = this.config.viewURL; |
| this.status.message = ( |
| responseText || |
| 'Error ' + response.status + ' requesting tree status'); |
| } else if (this.config.loginURL) { |
| this.status.url = this.config.loginURL; |
| this.status.message = 'Login required. Click here to login.'; |
| } else { |
| this.status.url = null; |
| this.status.message = 'Login required.'; |
| console.warn( |
| `chumpdetector failed to request ${this.config.viewURL} and got ` + |
| `status ${response.status}. You may want to add a loginURL to your ` + |
| `chumpdetector configuration to give your users a link they can ` + |
| `click on to take them to a logging page. See ` + |
| `https://chromium.googlesource.com/infra/gerrit-plugins/chumpdetector/#loginurl.`); |
| } |
| } |
| } |
| |
| /** |
| * Tree status UI for some single change. |
| */ |
| export class TreeStatusView extends Polymer.Element { |
| static get is() { |
| return 'tree-status-view'; |
| } |
| |
| static get template() { |
| return Polymer.html` |
| <style> |
| #outerDiv { |
| align-items: center; |
| display: flex; |
| font-size: 1.1em; |
| overflow-wrap: anywhere; |
| margin: 5px 0 8px; |
| padding: .4em; |
| max-width: 20em; |
| } |
| #outerDiv.unknown { |
| color: black; |
| background-color: #FFFC6C; |
| } |
| #outerDiv.open { |
| color: black; |
| background-color: #BCE889; |
| } |
| #outerDiv.closed { |
| color: white; |
| background-color: #E98080; |
| } |
| #outerDiv.hidden { |
| display: none; |
| } |
| </style> |
| <div id="outerDiv" class$="[[_getStyle(status)]]"> |
| <div> |
| <template is="dom-if" if="[[status.url]]"> |
| <a href="[[status.url]]" target="_blank"> |
| [[status.message]] |
| </a> |
| </template> |
| <template is="dom-if" if="[[!status.url]]"> |
| [[status.username]]: [[status.message]] |
| </template> |
| </div> |
| </div> |
| `; |
| } |
| |
| static get properties() { |
| return { |
| change: { |
| type: Object, |
| }, |
| config: { |
| type: Object, |
| value: null, |
| }, |
| status: { |
| type: Object, |
| value: null, |
| }, |
| tree: { |
| type: Object, |
| value: null, |
| }, |
| }; |
| } |
| |
| _getStyle(status) { |
| if (!status) { |
| return 'hidden'; |
| } |
| return status.generalState || 'unknown'; |
| } |
| |
| disconnectedCallback() { |
| super.disconnectedCallback(); |
| if (this.tree) { |
| this.tree.clear(); |
| } |
| } |
| } |
| |
| customElements.define(TreeStatusView.is, TreeStatusView); |