blob: fa590a3238d1bc58c29e5c6f378c8747967fe11b [file] [log] [blame]
/**
* @license
* Copyright (C) 2021 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-submit-requirement-hovercard/gr-submit-requirement-hovercard';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {ParsedChangeInfo} from '../../../types/types';
import {
AccountInfo,
isDetailedLabelInfo,
LabelNameToInfoMap,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
import {unique} from '../../../utils/common-util';
import {
extractAssociatedLabels,
hasVotes,
iconForStatus,
} from '../../../utils/label-util';
import {fontStyles} from '../../../styles/gr-font-styles';
import {charsOnly} from '../../../utils/string-util';
@customElement('gr-submit-requirements')
export class GrSubmitRequirements extends LitElement {
@property({type: Object})
change?: ParsedChangeInfo;
@property({type: Object})
account?: AccountInfo;
@property({type: Boolean})
mutable?: boolean;
static override get styles() {
return [
fontStyles,
css`
.metadata-title {
font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
margin: 0 0 var(--spacing-s);
border-top: 1px solid var(--border-color);
padding-top: var(--spacing-s);
}
iron-icon {
width: var(--line-height-normal, 20px);
height: var(--line-height-normal, 20px);
}
iron-icon.check {
color: var(--success-foreground);
}
iron-icon.close {
color: var(--warning-foreground);
}
.testing {
margin-top: var(--spacing-xxl);
padding-left: var(--metadata-horizontal-padding);
color: var(--deemphasized-text-color);
}
.testing gr-button {
min-width: 25px;
}
.testing * {
visibility: hidden;
}
.testing:hover * {
visibility: visible;
}
.requirements,
section.votes {
margin-left: var(--spacing-l);
}
gr-limited-text.name {
font-weight: var(--font-weight-bold);
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td {
padding: var(--spacing-s);
}
`,
];
}
override render() {
const submit_requirements = (this.change?.submit_requirements ?? []).filter(
req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
);
return html` <h2
class="metadata-title heading-3"
id="submit-requirements-caption"
>
Submit Requirements
</h2>
<table class="requirements" aria-labelledby="submit-requirements-caption">
<thead hidden>
<tr>
<th>Status</th>
<th>Name</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
${submit_requirements.map(
requirement => html`<tr
id="requirement-${charsOnly(requirement.name)}"
>
<td>${this.renderStatus(requirement.status)}</td>
<td class="name">
<gr-limited-text
class="name"
limit="25"
.text="${requirement.name}"
></gr-limited-text>
</td>
<td>${this.renderVotes(requirement)}</td>
</tr>`
)}
</tbody>
</table>
${submit_requirements.map(
requirement => html`
<gr-submit-requirement-hovercard
for="requirement-${charsOnly(requirement.name)}"
.requirement="${requirement}"
.change="${this.change}"
.account="${this.account}"
.mutable="${this.mutable}"
></gr-submit-requirement-hovercard>
`
)}
${this.renderTriggerVotes(
submit_requirements
)}${this.renderFakeControls()}`;
}
renderStatus(status: SubmitRequirementStatus) {
const icon = iconForStatus(status);
return html`<iron-icon
class="${icon}"
icon="gr-icons:${icon}"
role="img"
aria-label="${status.toLowerCase()}"
></iron-icon>`;
}
renderVotes(requirement: SubmitRequirementResultInfo) {
const requirementLabels = extractAssociatedLabels(requirement);
const allLabels = this.change?.labels ?? {};
const associatedLabels = Object.keys(allLabels).filter(label =>
requirementLabels.includes(label)
);
const everyAssociatedLabelsIsWithoutVotes = associatedLabels.every(
label => !hasVotes(allLabels[label])
);
if (everyAssociatedLabelsIsWithoutVotes) return html`No votes`;
return associatedLabels.map(label =>
this.renderLabelVote(label, allLabels)
);
}
renderLabelVote(label: string, labels: LabelNameToInfoMap) {
const labelInfo = labels[label];
if (!isDetailedLabelInfo(labelInfo)) return;
const uniqueApprovals = (labelInfo.all ?? [])
.filter(
(approvalInfo, index, array) =>
index === array.findIndex(other => other.value === approvalInfo.value)
)
.sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
return uniqueApprovals.map(
approvalInfo =>
html`<gr-vote-chip
.vote="${approvalInfo}"
.label="${labelInfo}"
.more="${(labelInfo.all ?? []).filter(
other => other.value === approvalInfo.value
).length > 1}"
></gr-vote-chip>`
);
}
renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
const labels = this.change?.labels ?? {};
const allLabels = Object.keys(labels);
const labelAssociatedWithSubmitReqs = submitReqs
.flatMap(req => extractAssociatedLabels(req))
.filter(unique);
const triggerVotes = allLabels
.filter(label => !labelAssociatedWithSubmitReqs.includes(label))
.filter(label => hasVotes(labels[label]));
if (!triggerVotes.length) return;
return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
<section class="votes">
${triggerVotes.map(
label => html`${label}:
<gr-label-info
.change="${this.change}"
.account="${this.account}"
.mutable="${this.mutable}"
label="${label}"
.labelInfo="${labels[label]}"
></gr-label-info>`
)}
</section>`;
}
renderFakeControls() {
return html`
<div class="testing">
<div>Toggle fake data:</div>
<gr-button link @click="${() => this.renderFakeSubmitRequirements()}"
>G</gr-button
>
</div>
`;
}
renderFakeSubmitRequirements() {
if (!this.change) return;
this.change = {
...this.change,
submit_requirements: [
{
name: 'Code-Review',
status: SubmitRequirementStatus.SATISFIED,
description:
"At least one maximum vote for label 'Code-Review' is required",
submittability_expression_result: {
expression: 'label:Code-Review=MAX -label:Code-Review=MIN',
fulfilled: true,
passing_atoms: [],
failing_atoms: [],
},
},
{
name: 'Verified',
status: SubmitRequirementStatus.UNSATISFIED,
description: 'CI build and tests results are verified',
submittability_expression_result: {
expression: 'label:Verified=MAX -label:Verified=MIN',
fulfilled: false,
passing_atoms: [],
failing_atoms: [],
},
},
],
};
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-submit-requirements': GrSubmitRequirements;
}
}