blob: a1cc7a8fec53aba9586c59476f096223de9fb220 [file] [log] [blame]
/**
* @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!);
}
}