blob: b3d92fb528e6722942e3aa812d1e6e83d2d4ae42 [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 {OwnerStatus} from './code-owners-service.js';
import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
const MAGIC_FILES = ['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL'];
const STATUS_CODE = {
PENDING: 'pending',
PENDING_OLD_PATH: 'pending-old-path',
MISSING: 'missing',
MISSING_OLD_PATH: 'missing-old-path',
APPROVED: 'approved',
ERROR: 'error',
ERROR_OLD_PATH: 'error-old-path',
};
const STATUS_ICON = {
[STATUS_CODE.PENDING]: 'gr-icons:schedule',
[STATUS_CODE.MISSING]: 'gr-icons:close',
[STATUS_CODE.PENDING_OLD_PATH]: 'gr-icons:schedule',
[STATUS_CODE.MISSING_OLD_PATH]: 'gr-icons:close',
[STATUS_CODE.APPROVED]: 'gr-icons:check',
[STATUS_CODE.ERROR]: 'gr-icons:info-outline',
};
const STATUS_TOOLTIP = {
[STATUS_CODE.PENDING]: 'Pending code owner approval',
[STATUS_CODE.MISSING]: 'Missing code owner approval',
[STATUS_CODE.PENDING_OLD_PATH]:
'Pending code owner approval on pre-renamed file',
[STATUS_CODE.MISSING_OLD_PATH]:
'Missing code owner approval on pre-renamed file',
[STATUS_CODE.APPROVED]: 'Approved by code owner',
[STATUS_CODE.ERROR]: 'Failed to fetch code owner status',
};
class BaseEl extends CodeOwnersModelMixin(Polymer.Element) {
computeHidden(change, patchRange) {
if ([change, patchRange].includes(undefined)) return true;
// if code-owners is not a submit requirement, don't show status column
if (change.requirements
&& !change.requirements.find(r => r.type === 'code-owners')) {
return true;
}
const latestPatchset = change.revisions[change.current_revision];
// only show if its comparing against base
if (patchRange.basePatchNum !== 'PARENT') return true;
// Note: in some special cases, patchNum is undefined on latest patchset like
// after publishing the edit, still show for them
// TODO: this should be fixed in Gerrit
if (patchRange.patchNum === undefined) return false;
// only show if its latest patchset
if (`${patchRange.patchNum}` !== `${latestPatchset._number}`) return true;
return false;
}
}
/**
* Column header element for owner status.
*/
export class OwnerStatusColumnHeader extends BaseEl {
static get is() {
return 'owner-status-column-header';
}
static get template() {
return Polymer.html`
<style include="shared-styles">
:host(:not([hidden])) {
display: block;
padding-right: var(--spacing-m);
width: 3em;
}
</style>
<div></div>
`;
}
static get properties() {
return {
patchRange: Object,
hidden: {
type: Boolean,
reflectToAttribute: true,
computed: 'computeHidden(change, patchRange)',
},
};
}
}
customElements.define(OwnerStatusColumnHeader.is, OwnerStatusColumnHeader);
/**
* Row content element for owner status.
*/
export class OwnerStatusColumnContent extends BaseEl {
static get is() {
return 'owner-status-column-content';
}
static get properties() {
return {
path: String,
oldPath: String,
patchRange: Object,
hidden: {
type: Boolean,
reflectToAttribute: true,
computed: 'computeHidden(change, patchRange)',
},
ownerService: Object,
statusIcon: {
type: String,
computed: '_computeIcon(status)',
},
statusInfo: {
type: String,
computed: '_computeTooltip(status)',
},
status: {
type: String,
reflectToAttribute: true,
},
};
}
static get template() {
return Polymer.html`
<style include="shared-styles">
:host(:not([hidden])) {
display:block;
padding-right: var(--spacing-m);
width: 3em;
text-align: center;
}
iron-icon {
padding: var(--spacing-xs) 0px;
}
:host([status=approved]) iron-icon {
color: var(--positive-green-text-color);
}
:host([status=pending]) iron-icon {
color: #ffa62f;
}
:host([status=missing]) iron-icon,
:host([status=error]) iron-icon {
color: var(--negative-red-text-color);
}
</style>
<gr-tooltip-content title="[[statusInfo]]" has-tooltip>
<iron-icon icon="[[statusIcon]]"></iron-icon>
</gr-tooltip-content>
`;
}
static get observers() {
return [
'computeStatusIcon(model.status, path, oldPath)',
];
}
loadPropertiesAfterModelChanged() {
super.loadPropertiesAfterModelChanged();
this.modelLoader.loadStatus();
}
computeStatusIcon(modelStatus, path, oldPath) {
if ([modelStatus, path, oldPath].includes(undefined)) return;
if (MAGIC_FILES.includes(path)) return;
const codeOwnerStatusMap = modelStatus.codeOwnerStatusMap;
const statusItem = codeOwnerStatusMap.get(path);
if (!statusItem) {
this.status = STATUS_CODE.ERROR;
return;
}
const status = statusItem.status;
let oldPathStatus = null;
if (oldPath !== path) {
const oldStatusItem = codeOwnerStatusMap.get(oldPath);
if (!oldStatusItem) {
this.status = STATUS_CODE.ERROR;
} else {
oldPathStatus = oldStatusItem.status;
}
}
const newPathStatus = this._computeStatus(status);
if (!oldPathStatus) {
this.status = newPathStatus;
} else {
this.status = newPathStatus === STATUS_CODE.APPROVED
? this._computeStatus(oldPathStatus, /* oldPath= */ true)
: newPathStatus;
}
}
_computeIcon(status) {
return STATUS_ICON[status];
}
_computeTooltip(status) {
return STATUS_TOOLTIP[status];
}
_computeStatus(status, oldPath = false) {
if (status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
return oldPath ? STATUS_CODE.MISSING_OLD_PATH : STATUS_CODE.MISSING;
} else if (status === OwnerStatus.PENDING) {
return oldPath ? STATUS_CODE.PENDING_OLD_PATH : STATUS_CODE.PENDING;
} else {
return STATUS_CODE.APPROVED;
}
}
}
customElements.define(OwnerStatusColumnContent.is, OwnerStatusColumnContent);