|  | /** | 
|  | * @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 {CodeOwnersModelMixin} from './code-owners-model-mixin'; | 
|  | import {showPluginFailedMessage} from './code-owners-banner'; | 
|  | import {isPluginErrorState, UserRole} from './code-owners-model'; | 
|  | import {css, html, LitElement} from 'lit'; | 
|  | import {customElement} from 'lit/decorators'; | 
|  | import {when} from 'lit/directives/when'; | 
|  | import { | 
|  | ApprovalInfo, | 
|  | DetailedLabelInfo, | 
|  | } from '@gerritcodereview/typescript-api/rest-api'; | 
|  | import {OwnerStatus} from './code-owners-api'; | 
|  |  | 
|  | const base = CodeOwnersModelMixin(LitElement); | 
|  |  | 
|  | export const OWNER_REQUIREMENT_VALUE = 'owner-requirement-value'; | 
|  | /** | 
|  | * Owner requirement control for `submit-requirement-item-code-owners` endpoint. | 
|  | * | 
|  | * This will show the status and suggest owners button next to | 
|  | * the code-owners submit requirement. | 
|  | */ | 
|  | @customElement(OWNER_REQUIREMENT_VALUE) | 
|  | export class OwnerRequirementValue extends base { | 
|  | static override get styles() { | 
|  | return [ | 
|  | css` | 
|  | :host { | 
|  | --gr-button: { | 
|  | padding: 0px; | 
|  | } | 
|  | } | 
|  | p.loading { | 
|  | display: flex; | 
|  | align-content: center; | 
|  | align-items: center; | 
|  | justify-content: center; | 
|  | } | 
|  | .loadingSpin { | 
|  | display: inline-block; | 
|  | margin-right: var(--spacing-m); | 
|  | width: 18px; | 
|  | height: 18px; | 
|  | } | 
|  | gr-button { | 
|  | padding-left: var(--spacing-m); | 
|  | } | 
|  | gr-button::part(paper-button) { | 
|  | padding: 0 var(--spacing-s); | 
|  | } | 
|  | a { | 
|  | text-decoration: none; | 
|  | } | 
|  | `, | 
|  | ]; | 
|  | } | 
|  |  | 
|  | override render() { | 
|  | // Compute whether plugin failed first because it might mean some of the | 
|  | // model parameters are not set which would result in a loading screen. | 
|  | if (this.pluginFailed()) { | 
|  | return html` | 
|  | <span>Code-owners plugin has failed</span> | 
|  | <gr-button link @click=${this.showFailDetails}>Details</gr-button> | 
|  | `; | 
|  | } | 
|  | if (!this.branchConfig || !this.status || !this.userRole) { | 
|  | return html` | 
|  | <p class="loading"> | 
|  | <span class="loadingSpin"></span> | 
|  | Loading status ... | 
|  | </p> | 
|  | `; | 
|  | } | 
|  |  | 
|  | if (this.status.newerPatchsetUploaded) { | 
|  | return html`<span>A newer patch set has been uploaded.</span>`; | 
|  | } | 
|  | const overrideInfoUrl = this.computeOverrideInfoUrl(); | 
|  | const statusCount = this.getStatusCount(); | 
|  | this.reporting?.reportLifeCycle('owners-submit-requirement-summary-shown', { | 
|  | ...statusCount, | 
|  | user_role: this.userRole, | 
|  | }); | 
|  | return html` | 
|  | <span>${this.computeStatusText(statusCount)}</span> | 
|  | ${when( | 
|  | !!overrideInfoUrl, | 
|  | () => html` | 
|  | <a | 
|  | @click=${this.reportDocClick} | 
|  | href=${overrideInfoUrl} | 
|  | target="_blank" | 
|  | > | 
|  | <gr-icon | 
|  | icon="help" | 
|  | title="Documentation for overriding code owners" | 
|  | ></gr-icon> | 
|  | </a> | 
|  | ` | 
|  | )} | 
|  | ${when( | 
|  | this.computeIsSignedInUser(this.userRole), | 
|  | () => html` | 
|  | <gr-button link @click=${this.openReplyDialog}> | 
|  | ${this.getSuggestOwnersText(statusCount)} | 
|  | </gr-button> | 
|  | ` | 
|  | )} | 
|  | `; | 
|  | } | 
|  |  | 
|  | override loadPropertiesAfterModelChanged() { | 
|  | super.loadPropertiesAfterModelChanged(); | 
|  | this.reporting?.reportLifeCycle('owners-submit-requirement-summary-start'); | 
|  | this.modelLoader?.loadBranchConfig(); | 
|  | this.modelLoader?.loadStatus(); | 
|  | this.modelLoader?.loadUserRole(); | 
|  | } | 
|  |  | 
|  | private computeIsSignedInUser(userRole: UserRole) { | 
|  | return userRole && userRole !== UserRole.ANONYMOUS; | 
|  | } | 
|  |  | 
|  | private pluginFailed() { | 
|  | return this.pluginStatus && isPluginErrorState(this.pluginStatus.state); | 
|  | } | 
|  |  | 
|  | private computeOverrideInfoUrl() { | 
|  | if (!this.branchConfig) { | 
|  | return ''; | 
|  | } | 
|  | return this.branchConfig.general && | 
|  | this.branchConfig.general.override_info_url | 
|  | ? this.branchConfig.general.override_info_url | 
|  | : ''; | 
|  | } | 
|  |  | 
|  | private computeIsOverriden() { | 
|  | if ( | 
|  | !this.change || | 
|  | !this.branchConfig || | 
|  | !this.branchConfig['override_approval'] | 
|  | ) { | 
|  | // no override labels configured | 
|  | return false; | 
|  | } | 
|  |  | 
|  | for (const requiredApprovalInfo of this.branchConfig['override_approval']) { | 
|  | const overridenLabel = requiredApprovalInfo.label; | 
|  | const overridenValue = Number(requiredApprovalInfo.value); | 
|  | if (isNaN(overridenValue)) continue; | 
|  |  | 
|  | if (this.change.labels?.[overridenLabel]) { | 
|  | const votes = | 
|  | (this.change.labels[overridenLabel] as DetailedLabelInfo).all || []; | 
|  | if ( | 
|  | votes.find((v: ApprovalInfo) => Number(v.value) >= overridenValue) | 
|  | ) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // otherwise always reset it to false | 
|  | return false; | 
|  | } | 
|  |  | 
|  | private getSuggestOwnersText(statusCount: { | 
|  | missing: number; | 
|  | pending: number; | 
|  | approved: number; | 
|  | }) { | 
|  | return statusCount && statusCount.missing === 0 | 
|  | ? 'Add owners' | 
|  | : 'Suggest owners'; | 
|  | } | 
|  |  | 
|  | private getStatusCount() { | 
|  | return (this.status?.rawStatuses ?? []).reduce( | 
|  | (prev, cur) => { | 
|  | const oldPathStatus = cur.old_path_status; | 
|  | const newPathStatus = cur.new_path_status; | 
|  | if (newPathStatus && this.isMissing(newPathStatus.status)) { | 
|  | prev.missing++; | 
|  | } else if (newPathStatus && this.isPending(newPathStatus.status)) { | 
|  | prev.pending++; | 
|  | } else if (oldPathStatus) { | 
|  | // check oldPath if newPath approved or the file is deleted | 
|  | if (this.isMissing(oldPathStatus.status)) { | 
|  | prev.missing++; | 
|  | } else if (this.isPending(oldPathStatus.status)) { | 
|  | prev.pending++; | 
|  | } | 
|  | } else { | 
|  | prev.approved++; | 
|  | } | 
|  | return prev; | 
|  | }, | 
|  | {missing: 0, pending: 0, approved: 0} | 
|  | ); | 
|  | } | 
|  |  | 
|  | private computeStatusText(statusCount: { | 
|  | missing: number; | 
|  | pending: number; | 
|  | approved: number; | 
|  | }) { | 
|  | if (this.model === undefined || this.change === undefined) return ''; | 
|  | const isOverriden = this.computeIsOverriden(); | 
|  | const statusText = []; | 
|  | if (statusCount.missing) { | 
|  | statusText.push(`${statusCount.missing} missing`); | 
|  | } | 
|  |  | 
|  | if (statusCount.pending) { | 
|  | statusText.push(`${statusCount.pending} pending`); | 
|  | } | 
|  |  | 
|  | if (!statusText.length) { | 
|  | statusText.push(isOverriden ? 'Approved (Owners-Override)' : 'Approved'); | 
|  | } | 
|  |  | 
|  | return statusText.join(', '); | 
|  | } | 
|  |  | 
|  | private isMissing(status: OwnerStatus | undefined) { | 
|  | return status === OwnerStatus.INSUFFICIENT_REVIEWERS; | 
|  | } | 
|  |  | 
|  | private isPending(status: OwnerStatus | undefined) { | 
|  | return status === OwnerStatus.PENDING; | 
|  | } | 
|  |  | 
|  | private openReplyDialog() { | 
|  | this.model!.setShowSuggestions(true); | 
|  | this.dispatchEvent( | 
|  | new CustomEvent('open-reply-dialog', { | 
|  | detail: {}, | 
|  | composed: true, | 
|  | bubbles: true, | 
|  | }) | 
|  | ); | 
|  | this.reporting?.reportInteraction( | 
|  | 'suggest-owners-from-submit-requirement', | 
|  | { | 
|  | user_role: this.userRole, | 
|  | } | 
|  | ); | 
|  | } | 
|  |  | 
|  | private reportDocClick() { | 
|  | this.reporting?.reportInteraction('code-owners-doc-click', { | 
|  | section: 'no owners found', | 
|  | }); | 
|  | } | 
|  |  | 
|  | private showFailDetails() { | 
|  | showPluginFailedMessage(this, this.pluginStatus!); | 
|  | } | 
|  | } |