blob: 65fdb93b3473ee4984aaa1da6a66ba609e400963 [file] [log] [blame]
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001/**
2 * @license
Ben Rohlfsb66b68c2022-05-27 11:49:22 +02003 * Copyright 2020 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Ben Rohlfsc9f90982020-12-13 22:00:11 +01005 */
Chris Poucet1c713862022-07-25 13:12:24 +02006import '../shared/gr-icon/gr-icon';
Frank Borden42c1a452022-08-11 16:27:20 +02007import {classMap} from 'lit/directives/class-map.js';
8import {repeat} from 'lit/directives/repeat.js';
9import {ifDefined} from 'lit/directives/if-defined.js';
Ben Rohlfsebef2e12022-09-01 17:22:24 +020010import {
11 LitElement,
12 css,
13 html,
14 PropertyValues,
15 TemplateResult,
16 nothing,
17} from 'lit';
Frank Borden42c1a452022-08-11 16:27:20 +020018import {customElement, property, query, state} from 'lit/decorators.js';
Ben Rohlfsd70a3372021-05-18 12:52:12 +020019import './gr-checks-action';
Ben Rohlfsece23f92021-09-30 14:58:37 +020020import './gr-hovercard-run';
Ben Rohlfsd06c5742021-03-03 14:45:58 +010021import '@polymer/paper-tooltip/paper-tooltip';
Ben Rohlfs6c97c572021-03-22 09:53:03 +010022import {
23 Action,
24 Category,
25 Link,
26 LinkIcon,
27 RunStatus,
28 Tag,
29} from '../../api/checks';
Ben Rohlfsc9f90982020-12-13 22:00:11 +010030import {sharedStyles} from '../../styles/shared-styles';
Ben Rohlfs68b47232023-06-26 12:53:41 +020031import {CheckRun, RunResult, runResult} from '../../models/checks/checks-model';
Ben Rohlfs913ab762021-02-05 12:52:00 +010032import {
Ben Rohlfsebef2e12022-09-01 17:22:24 +020033 ALL_ATTEMPTS,
34 AttemptChoice,
35 attemptChoiceLabel,
36 isAttemptChoice,
37 LATEST_ATTEMPT,
38 sortAttemptChoices,
39 stringToAttemptChoice,
Ben Rohlfs88453492021-03-03 11:32:40 +010040 allResults,
Ben Rohlfsd0207e52022-08-04 22:02:00 +020041 createFixAction,
Ben Rohlfs6e62a0c2021-06-07 13:13:40 +020042 firstPrimaryLink,
Ben Rohlfs4a5ac592021-05-19 15:11:03 +020043 hasCompletedWithoutResults,
Ben Rohlfsf457f892021-06-18 10:57:44 +020044 iconFor,
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020045 iconForLink,
Ben Rohlfsd58e07e2021-06-24 13:22:24 +020046 isCategory,
Ben Rohlfs3bc81282021-06-02 13:48:17 +020047 otherPrimaryLinks,
Ben Rohlfsfb8e8482021-06-02 14:39:11 +020048 secondaryLinks,
Ben Rohlfs6e62a0c2021-06-07 13:13:40 +020049 tooltipForLink,
Ben Rohlfsa2b6b7a2023-05-15 10:12:41 +020050 computeIsExpandable,
Chris Poucet776a68f2022-01-10 19:17:02 +010051} from '../../models/checks/checks-util';
Ben Rohlfsebef2e12022-09-01 17:22:24 +020052import {assertIsDefined, assert, unique} from '../../utils/common-util';
Ben Rohlfs808d1f02023-10-30 10:37:04 +010053import {modifierPressed, whenVisible} from '../../utils/dom-util';
Ben Rohlfs5e5c89e2021-02-25 11:09:33 +010054import {durationString} from '../../utils/date-util';
Ben Rohlfse79d23e2021-05-17 15:37:14 +020055import {charsOnly} from '../../utils/string-util';
Ben Rohlfs54fd41d2021-09-22 09:10:50 +020056import {isAttemptSelected, matches} from './gr-checks-util';
Chris Poucet0f9907c2022-06-09 18:15:51 +020057import {ChecksTabState, ValueChangedEvent} from '../../types/events';
Dhruv Srivastava25e53d82023-02-28 19:13:19 +010058import {
59 DropdownLink,
60 LabelNameToInfoMap,
Ben Rohlfs140fadf2024-06-12 12:10:56 +000061 PARENT,
Dhruv Srivastava25e53d82023-02-28 19:13:19 +010062 PatchSetNumber,
63} from '../../types/common';
Ben Rohlfsc3f6e722021-05-17 17:13:03 +020064import {spinnerStyles} from '../../styles/gr-spinner-styles';
Ben Rohlfs7df68672021-05-21 08:14:38 +020065import {
66 getLabelStatus,
67 getRepresentativeValue,
68 valueString,
69} from '../../utils/label-util';
Ben Rohlfs0ca140a2021-09-03 10:43:37 +020070import {subscribe} from '../lit/subscription-controller';
Ben Rohlfsaa60f932021-09-13 22:06:50 +020071import {fontStyles} from '../../styles/gr-font-styles';
Ben Rohlfs48330a12021-12-09 15:12:17 +010072import {fire} from '../../utils/event-util';
Chris Poucet9221cce2022-01-05 16:37:11 +010073import {resolve} from '../../models/dependency';
Chris Poucet776a68f2022-01-10 19:17:02 +010074import {checksModelToken} from '../../models/checks/checks-model';
Ben Rohlfscced9112021-12-22 16:34:54 +010075import {Interaction} from '../../constants/reporting';
76import {Deduping} from '../../api/reporting';
Chris Poucetbf65b8f2022-01-18 21:18:12 +000077import {changeModelToken} from '../../models/change/change-model';
78import {getAppContext} from '../../services/app-context';
Frank Borden42c1a452022-08-11 16:27:20 +020079import {when} from 'lit/directives/when.js';
Ben Rohlfsebef2e12022-09-01 17:22:24 +020080import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
81import './gr-checks-attempt';
Ben Rohlfs140fadf2024-06-12 12:10:56 +000082import {changeViewModelToken} from '../../models/views/change';
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +020083import {formStyles} from '../../styles/form-styles';
Ben Rohlfs48330a12021-12-09 15:12:17 +010084
85/**
86 * Firing this event sets the regular expression of the results filter.
87 */
88export interface ChecksResultsFilterDetail {
89 filterRegExp?: string;
90}
91export type ChecksResultsFilterEvent = CustomEvent<ChecksResultsFilterDetail>;
92
93declare global {
94 interface HTMLElementEventMap {
95 'checks-results-filter': ChecksResultsFilterEvent;
96 }
97}
Ben Rohlfsc9f90982020-12-13 22:00:11 +010098
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +010099@customElement('gr-result-row')
Ben Rohlfsee9cef72022-06-29 14:53:47 +0200100export class GrResultRow extends LitElement {
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200101 @query('td.nameCol div.name')
102 nameEl?: HTMLElement;
103
Ben Rohlfsece23f92021-09-30 14:58:37 +0200104 @property({attribute: false})
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100105 result?: RunResult;
106
Ben Rohlfsece23f92021-09-30 14:58:37 +0200107 @state()
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100108 isExpanded = false;
109
110 @property({type: Boolean, reflect: true})
111 isExpandable = false;
112
Ben Rohlfsece23f92021-09-30 14:58:37 +0200113 @state()
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100114 shouldRender = false;
115
Ben Rohlfsece23f92021-09-30 14:58:37 +0200116 @state()
Ben Rohlfs7df68672021-05-21 08:14:38 +0200117 labels?: LabelNameToInfoMap;
118
Ben Rohlfs05126b12022-06-29 16:13:56 +0200119 @state()
120 latestPatchNum?: PatchSetNumber;
121
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200122 @state()
123 selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
124
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000125 private getChangeModel = resolve(this, changeModelToken);
Chris Poucet01422482021-11-30 19:43:28 +0100126
Chris Poucet776a68f2022-01-10 19:17:02 +0100127 private getChecksModel = resolve(this, checksModelToken);
Ben Rohlfsf1c277b2021-09-07 21:12:13 +0200128
Ben Rohlfscced9112021-12-22 16:34:54 +0100129 private readonly reporting = getAppContext().reportingService;
130
Chris Poucet5ec77f02022-05-12 11:25:21 +0200131 constructor() {
132 super();
133 subscribe(
134 this,
135 () => this.getChangeModel().labels$,
136 x => (this.labels = x)
137 );
Ben Rohlfs05126b12022-06-29 16:13:56 +0200138 subscribe(
139 this,
140 () => this.getChangeModel().latestPatchNum$,
141 x => (this.latestPatchNum = x)
142 );
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200143 subscribe(
144 this,
145 () => this.getChecksModel().checksSelectedAttemptNumber$,
146 x => (this.selectedAttempt = x)
147 );
Ben Rohlfs7df68672021-05-21 08:14:38 +0200148 }
149
Gerrit Code Review79031232021-08-19 14:32:41 +0000150 static override get styles() {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100151 return [
152 sharedStyles,
153 css`
154 :host {
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100155 display: contents;
156 }
157 :host([isexpandable]) {
158 cursor: pointer;
159 }
Ben Rohlfs9c3696f2021-03-22 10:26:41 +0100160 gr-result-expanded {
161 cursor: default;
162 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200163 tr.container {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100164 border-top: 1px solid var(--border-color);
165 }
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200166 a.link {
167 margin-right: var(--spacing-s);
168 }
Chris Poucet1c713862022-07-25 13:12:24 +0200169 gr-icon.link {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100170 color: var(--link-color);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100171 }
Ben Rohlfs07209832021-05-18 15:23:06 +0200172 td.nameCol div.flex {
173 display: flex;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100174 }
Ben Rohlfs07209832021-05-18 15:23:06 +0200175 td.nameCol .name {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100176 overflow: hidden;
177 text-overflow: ellipsis;
Ben Rohlfs07209832021-05-18 15:23:06 +0200178 margin-right: var(--spacing-s);
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200179 outline-offset: var(--spacing-xs);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100180 }
Ben Rohlfs07209832021-05-18 15:23:06 +0200181 td.nameCol .space {
182 flex-grow: 1;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100183 }
Ben Rohlfs07209832021-05-18 15:23:06 +0200184 td.nameCol gr-checks-action {
185 display: none;
186 }
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200187 tr:focus-within td.nameCol gr-checks-action,
Ben Rohlfs07209832021-05-18 15:23:06 +0200188 tr:hover td.nameCol gr-checks-action {
189 display: inline-block;
190 /* The button should fit into the 20px line-height. The negative
191 margin provides the extra space needed for the vertical padding.
192 Alternatively we could have set the vertical padding to 0, but
193 that would not have been a nice click target. */
194 margin: calc(0px - var(--spacing-s)) 0px;
195 margin-left: var(--spacing-s);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100196 }
197 td {
198 white-space: nowrap;
199 padding: var(--spacing-s);
200 }
Ben Rohlfs894dd292021-06-16 12:10:20 +0200201 td.expandedCol,
Ben Rohlfsdaba0e82021-06-11 08:06:17 +0200202 td.nameCol {
Ben Rohlfs894dd292021-06-16 12:10:20 +0200203 padding-left: var(--spacing-l);
204 }
205 td.expandedCol,
206 td.expanderCol {
207 padding-right: var(--spacing-l);
Ben Rohlfsdaba0e82021-06-11 08:06:17 +0200208 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100209 td .summary-cell {
210 display: flex;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100211 }
212 td .summary-cell .summary {
Ben Rohlfsb630ef72021-02-03 10:09:55 +0100213 font-weight: var(--font-weight-bold);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100214 flex-shrink: 1;
215 overflow: hidden;
216 text-overflow: ellipsis;
Ben Rohlfs23a466c2021-02-17 12:03:53 +0100217 margin-right: var(--spacing-s);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100218 }
219 td .summary-cell .message {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100220 flex-grow: 1;
221 /* Looks a bit stupid, but the idea is that .message shrinks first,
222 and only when that has shrunken to 0, then .summary should also
223 start shrinking (substantially). */
224 flex-shrink: 1000000;
225 overflow: hidden;
226 text-overflow: ellipsis;
227 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200228 tr.container:hover {
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200229 background: var(--hover-background-color);
230 }
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200231 tr.container:focus-within {
232 background: var(--selection-background-color);
233 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200234 tr.container td .summary-cell .links,
235 tr.container td .summary-cell .actions,
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200236 tr.container.collapsed:focus-within td .summary-cell .links,
237 tr.container.collapsed:focus-within td .summary-cell .actions,
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200238 tr.container.collapsed:hover td .summary-cell .links,
239 tr.container.collapsed:hover td .summary-cell .actions,
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200240 :host(.dropdown-open) tr td .summary-cell .links,
241 :host(.dropdown-open) tr td .summary-cell .actions {
242 display: inline-block;
243 margin-left: var(--spacing-s);
244 }
Milutin Kristofic7ada7452024-03-18 20:04:46 +0100245 /* actions-shown-on-collapsed are shown only when .actions is hidden
246 and vice versa. */
247 tr.container td .summary-cell .actions-shown-on-collapsed,
248 tr.container.collapsed:focus-within
249 td
250 .summary-cell
251 .actions-shown-on-collapsed,
252 tr.container.collapsed:hover
253 td
254 .summary-cell
255 .actions-shown-on-collapsed,
256 :host(.dropdown-open) tr td .summary-cell .actions-shown-on-collapsed {
257 display: none;
258 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200259 tr.container.collapsed td .summary-cell .message {
Ben Rohlfse31db032021-05-05 12:55:15 +0200260 color: var(--deemphasized-text-color);
261 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200262 tr.container.collapsed td .summary-cell .links,
263 tr.container.collapsed td .summary-cell .actions {
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200264 display: none;
265 }
Milutin Kristofic7ada7452024-03-18 20:04:46 +0100266 tr.container.collapsed td .summary-cell .actions-shown-on-collapsed {
267 display: inline-block;
268 margin-left: var(--spacing-s);
269 }
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200270 tr.detailsRow.collapsed {
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200271 display: none;
272 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100273 td .summary-cell .tags .tag {
Ben Rohlfs1bbb0962021-04-15 15:22:40 +0200274 color: var(--primary-text-color);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100275 display: inline-block;
276 border-radius: 20px;
277 background-color: var(--tag-background);
278 padding: 0 var(--spacing-m);
279 margin-left: var(--spacing-s);
Ben Rohlfs48330a12021-12-09 15:12:17 +0100280 cursor: pointer;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100281 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200282 td .summary-cell .tag.gray {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100283 background-color: var(--tag-gray);
284 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200285 td .summary-cell .tag.yellow {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100286 background-color: var(--tag-yellow);
287 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200288 td .summary-cell .tag.pink {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100289 background-color: var(--tag-pink);
290 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200291 td .summary-cell .tag.purple {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100292 background-color: var(--tag-purple);
293 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200294 td .summary-cell .tag.cyan {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100295 background-color: var(--tag-cyan);
296 }
Ben Rohlfs83db0612021-05-21 09:18:44 +0200297 td .summary-cell .tag.brown {
Ben Rohlfsceca75d2021-03-17 22:46:22 +0100298 background-color: var(--tag-brown);
299 }
Milutin Kristofic7ada7452024-03-18 20:04:46 +0100300 .actions-shown-on-collapsed gr-checks-action,
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200301 .actions gr-checks-action,
302 .actions gr-dropdown {
303 /* Fitting a 28px button into 20px line-height. */
304 margin: -4px 0;
305 vertical-align: top;
306 }
Chris Poucet1c713862022-07-25 13:12:24 +0200307 #moreActions gr-icon {
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200308 color: var(--link-color);
309 }
310 #moreMessage {
311 display: none;
312 }
Ben Rohlfs7df68672021-05-21 08:14:38 +0200313 td .summary-cell .label {
314 margin-left: var(--spacing-s);
315 border-radius: var(--border-radius);
316 color: var(--vote-text-color);
317 display: inline-block;
318 padding: 0 var(--spacing-s);
319 text-align: center;
320 }
321 td .summary-cell .label.neutral {
322 background-color: var(--vote-color-neutral);
323 }
324 td .summary-cell .label.recommended,
325 td .summary-cell .label.disliked {
326 line-height: calc(var(--line-height-normal) - 2px);
327 color: var(--chip-color);
328 }
329 td .summary-cell .label.recommended {
330 background-color: var(--vote-color-recommended);
331 border: 1px solid var(--vote-outline-recommended);
332 }
333 td .summary-cell .label.disliked {
334 background-color: var(--vote-color-disliked);
335 border: 1px solid var(--vote-outline-disliked);
336 }
337 td .summary-cell .label.approved {
338 background-color: var(--vote-color-approved);
339 }
340 td .summary-cell .label.rejected {
341 background-color: var(--vote-color-rejected);
342 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100343 `,
344 ];
345 }
346
Ben Rohlfsa2b6b7a2023-05-15 10:12:41 +0200347 override willUpdate(changedProperties: PropertyValues) {
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100348 if (changedProperties.has('result')) {
Ben Rohlfsa2b6b7a2023-05-15 10:12:41 +0200349 this.isExpandable = computeIsExpandable(this.result);
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100350 }
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100351 }
352
Gerrit Code Review79031232021-08-19 14:32:41 +0000353 override focus() {
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200354 if (this.nameEl) this.nameEl.focus();
355 }
356
Gerrit Code Review79031232021-08-19 14:32:41 +0000357 override firstUpdated() {
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100358 const loading = this.shadowRoot?.querySelector('.container');
359 assertIsDefined(loading, '"Loading" element');
Chris Poucet955e3b82021-10-11 12:15:16 +0000360 whenVisible(
361 loading,
362 () => {
363 this.shouldRender = true;
364 },
365 200
366 );
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100367 }
368
Gerrit Code Review79031232021-08-19 14:32:41 +0000369 override render() {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100370 if (!this.result) return '';
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100371 if (!this.shouldRender) {
372 return html`
373 <tr class="container">
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100374 <td class="nameCol">
375 <div><span class="loading">Loading...</span></div>
376 </td>
377 <td class="summaryCol"></td>
378 <td class="expanderCol"></td>
379 </tr>
380 `;
381 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100382 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200383 <tr class=${classMap({container: true, collapsed: !this.isExpanded})}>
384 <td class="nameCol" @click=${this.toggleExpandedClick}>
Ben Rohlfs07209832021-05-18 15:23:06 +0200385 <div class="flex">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200386 <gr-hovercard-run .run=${this.result}></gr-hovercard-run>
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200387 <div
388 class="name"
389 role="button"
390 tabindex="0"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200391 @click=${this.toggleExpandedClick}
392 @keydown=${this.toggleExpandedPress}
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200393 >
394 ${this.result.checkName}
395 </div>
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200396 ${this.renderAttempt()}
Ben Rohlfs07209832021-05-18 15:23:06 +0200397 <div class="space"></div>
Ben Rohlfsb5f62852021-03-23 11:23:17 +0100398 </div>
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100399 </td>
400 <td class="summaryCol">
401 <div class="summary-cell">
Ben Rohlfs3bc81282021-06-02 13:48:17 +0200402 ${this.renderLink(firstPrimaryLink(this.result))}
Ben Rohlfs23a466c2021-02-17 12:03:53 +0100403 ${this.renderSummary(this.result.summary)}
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200404 <div class="message" @click=${this.toggleExpandedClick}>
Ben Rohlfs178f56f2021-01-27 14:52:28 +0000405 ${this.isExpanded ? '' : this.result.message}
406 </div>
Ben Rohlfs7e8548c2021-06-11 10:13:40 +0200407 ${this.renderLinks()} ${this.renderActions()}
408 <div class="tags">
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100409 ${(this.result.tags ?? []).map(t => this.renderTag(t))}
410 </div>
Ben Rohlfs7e8548c2021-06-11 10:13:40 +0200411 ${this.renderLabel()}
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100412 </div>
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100413 </td>
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200414 <td class="expanderCol" @click=${this.toggleExpandedClick}>
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100415 <div
416 class="show-hide"
417 role="switch"
418 tabindex="0"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200419 ?hidden=${!this.isExpandable}
420 aria-checked=${this.isExpanded ? 'true' : 'false'}
421 aria-label=${this.isExpanded
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100422 ? 'Collapse result row'
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200423 : 'Expand result row'}
424 @keydown=${this.toggleExpandedPress}
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100425 >
Chris Poucet1c713862022-07-25 13:12:24 +0200426 <gr-icon
427 icon=${this.isExpanded ? 'expand_less' : 'expand_more'}
428 ></gr-icon>
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100429 </div>
430 </td>
431 </tr>
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200432 <tr class=${classMap({detailsRow: true, collapsed: !this.isExpanded})}>
Ben Rohlfs894dd292021-06-16 12:10:20 +0200433 <td class="expandedCol" colspan="3">${this.renderExpanded()}</td>
Ben Rohlfs9ccee7e2021-06-02 14:04:44 +0200434 </tr>
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100435 `;
436 }
437
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200438 private renderAttempt() {
439 if (this.selectedAttempt !== ALL_ATTEMPTS) return nothing;
440 return html`<gr-checks-attempt .run=${this.result}></gr-checks-attempt>`;
441 }
442
Ben Rohlfsb36b2ba2021-04-07 14:36:05 +0200443 private renderExpanded() {
444 if (!this.isExpanded) return;
445 return html`<gr-result-expanded
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200446 .result=${this.result}
Ben Rohlfsb36b2ba2021-04-07 14:36:05 +0200447 ></gr-result-expanded>`;
448 }
449
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200450 private toggleExpandedClick(e: MouseEvent) {
451 if (!this.isExpandable) return;
452 e.preventDefault();
453 e.stopPropagation();
454 this.toggleExpanded();
455 }
456
Ben Rohlfs48330a12021-12-09 15:12:17 +0100457 private tagClick(e: MouseEvent, tagName: string) {
458 e.preventDefault();
459 e.stopPropagation();
Ben Rohlfscced9112021-12-22 16:34:54 +0100460 this.reporting.reportInteraction(Interaction.CHECKS_TAG_CLICKED, {
461 tagName,
462 checkName: this.result?.checkName,
463 });
Ben Rohlfs48330a12021-12-09 15:12:17 +0100464 fire(this, 'checks-results-filter', {filterRegExp: tagName});
465 }
466
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200467 private toggleExpandedPress(e: KeyboardEvent) {
468 if (!this.isExpandable) return;
469 if (modifierPressed(e)) return;
Frank Borden7ff29de2022-08-30 19:15:46 +0200470 if (e.key !== 'Enter' && e.key !== ' ') return;
Ben Rohlfsea7c3d42021-06-18 12:26:22 +0200471 e.preventDefault();
472 e.stopPropagation();
473 this.toggleExpanded();
474 }
475
Ben Rohlfsa8bcf742024-06-14 08:27:11 +0200476 /** Toggles the expanded state, or if `setExpanded` is provided sets it to the desired state. */
477 toggleExpanded(setExpanded?: boolean) {
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100478 if (!this.isExpandable) return;
Ben Rohlfsa8bcf742024-06-14 08:27:11 +0200479 this.isExpanded =
480 setExpanded === undefined ? !this.isExpanded : setExpanded;
Ben Rohlfscced9112021-12-22 16:34:54 +0100481 this.reporting.reportInteraction(Interaction.CHECKS_RESULT_ROW_TOGGLE, {
482 expanded: this.isExpanded,
483 checkName: this.result?.checkName,
484 });
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100485 }
486
Ben Rohlfs23a466c2021-02-17 12:03:53 +0100487 renderSummary(text?: string) {
Kamil Musin48fa4ed2024-02-05 15:16:07 +0100488 text = text ?? '';
Ben Rohlfs23a466c2021-02-17 12:03:53 +0100489 return html`
490 <!-- The &nbsp; is for being able to shrink a tiny amount without
491 the text itself getting shrunk with an ellipsis. -->
Ben Rohlfsee9cef72022-06-29 14:53:47 +0200492 <div class="summary" @click=${this.toggleExpanded} title=${text}>
493 ${text}&nbsp;
494 </div>
Ben Rohlfs23a466c2021-02-17 12:03:53 +0100495 `;
496 }
497
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100498 renderLabel() {
Ben Rohlfs7df68672021-05-21 08:14:38 +0200499 const category = this.result?.category;
500 if (category !== Category.ERROR && category !== Category.WARNING) return;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100501 const label = this.result?.labelName;
502 if (!label) return;
Ben Rohlfs396a6242021-05-26 13:29:55 +0200503 if (!this.result?.isLatestAttempt) return;
Ben Rohlfs05126b12022-06-29 16:13:56 +0200504 // For check results on older patchsets it is impossible to decide whether
505 // the current label score is still influenced by them. But typically it
506 // is really confusing for the user, if we claim that an old (error) result
507 // influences the current (positive) score. So we prefer to be conservative
508 // and only display the label chip for checks results on the latest ps.
509 if (this.result.patchset !== this.latestPatchNum) return;
Ben Rohlfs7df68672021-05-21 08:14:38 +0200510 const info = this.labels?.[label];
511 const status = getLabelStatus(info).toLowerCase();
Ben Rohlfse29f1902021-06-23 10:08:00 +0200512 const value = getRepresentativeValue(info);
513 // A neutral vote is not interesting for the user to see and is just
514 // cluttering the UI.
515 if (value === 0) return;
516 const valueStr = valueString(value);
Ben Rohlfs0629e832021-06-11 12:44:12 +0200517 return html`
518 <div class="label ${status}">
Ben Rohlfse29f1902021-06-23 10:08:00 +0200519 <span>${label} ${valueStr}</span>
Chris Poucet4f744922024-04-09 15:25:30 +0200520 <paper-tooltip offset="5" .fitToVisibleBounds=${true}>
Ben Rohlfs0629e832021-06-11 12:44:12 +0200521 The check result has (probably) influenced this label vote.
522 </paper-tooltip>
523 </div>
524 `;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100525 }
526
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200527 renderLinks() {
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200528 const links = otherPrimaryLinks(this.result)
529 // Showing the same icons twice without text is super confusing.
530 .filter(
531 (link: Link, index: number, array: Link[]) =>
532 array.findIndex(other => link.icon === other.icon) === index
533 )
534 // 4 is enough for the summary row. All are shown in expanded state.
535 .slice(0, 4);
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200536 if (links.length === 0) return;
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200537 return html`<div class="links">
538 ${links.map(link => this.renderLink(link))}
539 </div>`;
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200540 }
541
Ben Rohlfsb04d8562021-05-26 13:57:15 +0200542 renderLink(link?: Link) {
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200543 // The expanded state renders all links in more detail. Hide in summary.
544 if (this.isExpanded) return;
Ben Rohlfsb04d8562021-05-26 13:57:15 +0200545 if (!link) return;
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200546 const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
Chris Poucet5ca5e802022-07-18 15:26:29 +0200547 const icon = iconForLink(link.icon);
Ben Rohlfs3630a382023-06-23 12:53:18 +0200548 return html`<a
549 href=${link.url}
550 class="link"
551 target="_blank"
552 rel="noopener noreferrer"
Chris Poucet1c713862022-07-25 13:12:24 +0200553 ><gr-icon
554 icon=${icon.name}
555 ?filled=${icon.filled}
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200556 aria-label="external link to details"
Chris Poucet1c713862022-07-25 13:12:24 +0200557 class="link"
Ben Rohlfs5faf113d2022-08-15 12:22:43 +0200558 ></gr-icon
559 ><paper-tooltip offset="5">${tooltipText}</paper-tooltip></a
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200560 >`;
561 }
562
563 private renderActions() {
Ben Rohlfsd0207e52022-08-04 22:02:00 +0200564 const actions = [...(this.result?.actions ?? [])];
Ben Rohlfsb8e35262022-11-02 12:02:07 +0100565 const fixAction = createFixAction(this, this.result);
566 if (fixAction) actions.unshift(fixAction);
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200567 if (actions.length === 0) return;
568 const overflowItems = actions.slice(2).map(action => {
569 return {...action, id: action.name};
570 });
Ben Rohlfsd70a3372021-05-18 12:52:12 +0200571 const disabledItems = overflowItems
572 .filter(action => action.disabled)
573 .map(action => action.id);
Milutin Kristofic7ada7452024-03-18 20:04:46 +0100574 return html` ${when(
575 fixAction,
576 () =>
577 html`<div class="actions-shown-on-collapsed">
578 ${this.renderAction(fixAction)}
579 </div> `
580 )}
581 <div class="actions">
582 ${this.renderAction(actions[0])} ${this.renderAction(actions[1])}
583 <gr-dropdown
584 id="moreActions"
585 link=""
586 vertical-offset="32"
587 horizontal-align="right"
588 @tap-item=${this.handleAction}
589 @opened-changed=${(e: ValueChangedEvent<boolean>) =>
590 this.classList.toggle('dropdown-open', e.detail.value)}
591 ?hidden=${overflowItems.length === 0}
592 .items=${overflowItems}
593 .disabledIds=${disabledItems}
594 >
595 <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
596 <span id="moreMessage">More</span>
597 </gr-dropdown>
598 </div>`;
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200599 }
600
601 private handleAction(e: CustomEvent<Action>) {
Ben Rohlfscced9112021-12-22 16:34:54 +0100602 this.getChecksModel().triggerAction(
603 e.detail,
604 this.result,
605 'result-row-dropdown'
606 );
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200607 }
608
609 private renderAction(action?: Action) {
610 if (!action) return;
Ben Rohlfscced9112021-12-22 16:34:54 +0100611 return html`<gr-checks-action
612 context="result-row"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200613 .action=${action}
Ben Rohlfscced9112021-12-22 16:34:54 +0100614 ></gr-checks-action>`;
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +0200615 }
616
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100617 renderTag(tag: Tag) {
Ben Rohlfs48330a12021-12-09 15:12:17 +0100618 return html`<button
619 class="tag ${tag.color}"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200620 @click=${(e: MouseEvent) => this.tagClick(e, tag.name)}
Ben Rohlfs48330a12021-12-09 15:12:17 +0100621 >
Ben Rohlfs0629e832021-06-11 12:44:12 +0200622 <span>${tag.name}</span>
Chris Poucet4f744922024-04-09 15:25:30 +0200623 <paper-tooltip offset="5" .fitToVisibleBounds=${true}>
Milutin Kristofica16c93f2022-01-10 21:28:12 +0100624 ${tag.tooltip ??
625 'A category tag for this check result. Click to filter.'}
Ben Rohlfs0629e832021-06-11 12:44:12 +0200626 </paper-tooltip>
Milutin Kristofica16c93f2022-01-10 21:28:12 +0100627 </button>`;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100628 }
629}
630
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100631@customElement('gr-result-expanded')
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200632class GrResultExpanded extends LitElement {
Ben Rohlfsece23f92021-09-30 14:58:37 +0200633 @property({attribute: false})
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100634 result?: RunResult;
635
Ben Rohlfs21b1cb42021-12-16 13:17:23 +0100636 @property({type: Boolean})
637 hideCodePointers = false;
638
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000639 private getChangeModel = resolve(this, changeModelToken);
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200640
Ben Rohlfs140fadf2024-06-12 12:10:56 +0000641 private readonly getViewModel = resolve(this, changeViewModelToken);
642
Gerrit Code Review79031232021-08-19 14:32:41 +0000643 static override get styles() {
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100644 return [
645 sharedStyles,
646 css`
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200647 .links {
648 white-space: normal;
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200649 }
650 .links a {
651 display: inline-block;
652 margin-right: var(--spacing-xl);
653 }
Chris Poucet1c713862022-07-25 13:12:24 +0200654 .links a gr-icon {
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200655 margin-right: var(--spacing-xs);
656 }
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100657 .message {
Ben Rohlfs894dd292021-06-16 12:10:20 +0200658 padding: var(--spacing-m) 0;
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100659 }
660 `,
661 ];
662 }
663
Gerrit Code Review79031232021-08-19 14:32:41 +0000664 override render() {
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100665 if (!this.result) return '';
Ben Rohlfs178f56f2021-01-27 14:52:28 +0000666 return html`
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200667 ${this.renderFirstPrimaryLink()} ${this.renderOtherPrimaryLinks()}
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200668 ${this.renderSecondaryLinks()} ${this.renderCodePointers()}
Ben Rohlfsc767e662021-08-05 15:23:32 +0200669 <gr-endpoint-decorator
670 name="check-result-expanded"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200671 .targetPlugin=${this.result.pluginName}
Ben Rohlfsc767e662021-08-05 15:23:32 +0200672 >
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200673 <gr-endpoint-param name="run" .value=${this.result}></gr-endpoint-param>
Ben Rohlfsa8af8672021-02-19 16:12:28 +0100674 <gr-endpoint-param
675 name="result"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200676 .value=${this.result}
Ben Rohlfsa8af8672021-02-19 16:12:28 +0100677 ></gr-endpoint-param>
Ben Rohlfs04b6c1f2021-04-15 17:24:23 +0200678 <gr-formatted-text
Ben Rohlfs04b6c1f2021-04-15 17:24:23 +0200679 class="message"
Frank Bordenabdd1872022-09-26 12:55:59 +0200680 .markdown=${true}
681 .content=${this.result.message ?? ''}
Ben Rohlfs04b6c1f2021-04-15 17:24:23 +0200682 ></gr-formatted-text>
Ben Rohlfsa8af8672021-02-19 16:12:28 +0100683 </gr-endpoint-decorator>
Ben Rohlfs178f56f2021-01-27 14:52:28 +0000684 `;
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100685 }
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200686
687 private renderFirstPrimaryLink() {
688 const link = firstPrimaryLink(this.result);
689 if (!link) return;
690 return html`<div class="links">${this.renderLink(link)}</div>`;
691 }
692
693 private renderOtherPrimaryLinks() {
694 const links = otherPrimaryLinks(this.result);
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200695 if (links.length === 0) return;
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200696 return html`<div class="links">
697 ${links.map(link => this.renderLink(link))}
698 </div>`;
699 }
700
701 private renderSecondaryLinks() {
702 const links = secondaryLinks(this.result);
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200703 if (links.length === 0) return;
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200704 return html`<div class="links">
705 ${links.map(link => this.renderLink(link))}
706 </div>`;
707 }
708
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200709 private renderCodePointers() {
Ben Rohlfs21b1cb42021-12-16 13:17:23 +0100710 if (this.hideCodePointers) return;
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200711 const pointers = this.result?.codePointers ?? [];
712 if (pointers.length === 0) return;
713 const links = pointers.map(pointer => {
714 let rangeText = '';
715 const start = pointer?.range?.start_line;
716 const end = pointer?.range?.end_line;
717 if (start) rangeText += `#${start}`;
718 if (end && start !== end) rangeText += `-${end}`;
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000719 const change = this.getChangeModel().getChange();
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200720 assertIsDefined(change);
721 const path = pointer.path;
Ben Rohlfs140fadf2024-06-12 12:10:56 +0000722 const patchset = this.result?.patchset as PatchSetNumber;
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200723 const line = pointer?.range?.start_line;
724 return {
725 icon: LinkIcon.CODE,
726 tooltip: `${path}${rangeText}`,
Ben Rohlfs140fadf2024-06-12 12:10:56 +0000727 url: this.getViewModel().diffUrl({
728 basePatchNum: PARENT,
Ben Rohlfs731738b2022-09-15 15:55:33 +0200729 patchNum: patchset,
Ben Rohlfs05234f52023-05-17 09:40:47 +0200730 checksPatchset: patchset,
Ben Rohlfsecc67992022-12-16 10:10:16 +0100731 diffView: {path, lineNum: line},
Ben Rohlfs731738b2022-09-15 15:55:33 +0200732 }),
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200733 primary: true,
734 };
735 });
736 return links.map(
737 link => html`<div class="links">${this.renderLink(link, false)}</div>`
738 );
739 }
740
741 private renderLink(link?: Link, targetBlank = true) {
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200742 if (!link) return;
743 const text = link.tooltip ?? tooltipForLink(link.icon);
Ben Rohlfs7d262be2021-06-02 14:59:03 +0200744 const target = targetBlank ? '_blank' : undefined;
Chris Poucet5ca5e802022-07-18 15:26:29 +0200745 const icon = iconForLink(link.icon);
Ben Rohlfs3630a382023-06-23 12:53:18 +0200746 return html`<a
747 href=${link.url}
748 target=${ifDefined(target)}
749 rel="noopener noreferrer"
750 >
Chris Poucet1c713862022-07-25 13:12:24 +0200751 <gr-icon icon=${icon.name} class="link" ?filled=${icon.filled}></gr-icon>
752 <span>${text}</span>
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200753 </a>`;
754 }
Ben Rohlfse6c57d02021-01-25 11:22:39 +0100755}
756
Ben Rohlfs5929e4b2021-05-17 14:53:29 +0200757const CATEGORY_TOOLTIPS: Map<Category, string> = new Map();
758CATEGORY_TOOLTIPS.set(Category.ERROR, 'Must be fixed and is blocking submit');
759CATEGORY_TOOLTIPS.set(
760 Category.WARNING,
761 'Should be checked but is not blocking submit'
762);
763CATEGORY_TOOLTIPS.set(
764 Category.INFO,
765 'Does not have to be checked, for your information only'
766);
767CATEGORY_TOOLTIPS.set(
768 Category.SUCCESS,
769 'Successful runs without results and individual successful results'
770);
771
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100772@customElement('gr-checks-results')
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200773export class GrChecksResults extends LitElement {
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100774 @query('#filterInput')
775 filterInput?: HTMLInputElement;
776
Frank Bordenf19f2b72021-05-12 11:21:34 +0200777 @state()
Ben Rohlfs209f1412022-09-30 12:25:46 +0200778 filterRegExp = '';
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100779
Ben Rohlfse6a4c472021-04-22 13:06:09 +0200780 /** All runs. Shown should only the selected/filtered ones. */
Ben Rohlfsece23f92021-09-30 14:58:37 +0200781 @property({attribute: false})
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100782 runs: CheckRun[] = [];
783
Ben Rohlfse6a4c472021-04-22 13:06:09 +0200784 /**
785 * Check names of runs that are selected in the runs panel. When this array
786 * is empty, then no run is selected and all runs should be shown.
787 */
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200788 @state()
Ben Rohlfs20fe8e82022-10-14 11:13:10 +0200789 selectedRuns: Set<string> = new Set();
Ben Rohlfse6a4c472021-04-22 13:06:09 +0200790
Ben Rohlfsece23f92021-09-30 14:58:37 +0200791 @state()
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100792 actions: Action[] = [];
793
Ben Rohlfsece23f92021-09-30 14:58:37 +0200794 @state()
Ben Rohlfs0ae33e32021-04-23 10:01:00 +0200795 links: Link[] = [];
796
Ben Rohlfsece23f92021-09-30 14:58:37 +0200797 @property({attribute: false})
Ben Rohlfsed7d5db2021-03-05 15:12:18 +0100798 tabState?: ChecksTabState;
799
Ben Rohlfsece23f92021-09-30 14:58:37 +0200800 @state()
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100801 someProvidersAreLoading = false;
802
Ben Rohlfsece23f92021-09-30 14:58:37 +0200803 @state()
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100804 checksPatchsetNumber: PatchSetNumber | undefined = undefined;
805
Ben Rohlfsece23f92021-09-30 14:58:37 +0200806 @state()
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100807 latestPatchsetNumber: PatchSetNumber | undefined = undefined;
808
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200809 @state()
810 selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
Ben Rohlfs85a866b2021-04-15 11:20:02 +0200811
Ben Rohlfsfabe7492021-04-23 11:21:16 +0200812 /** Maintains the state of which result sections should show all results. */
Frank Bordenf19f2b72021-05-12 11:21:34 +0200813 @state()
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200814 isShowAll: Map<Category, boolean> = new Map();
Ben Rohlfsfabe7492021-04-23 11:21:16 +0200815
Ben Rohlfs260cce02021-03-05 16:57:09 +0100816 /**
Ben Rohlfs99949282021-03-03 19:19:21 +0100817 * This is the current state of whether a section is expanded or not. As long
818 * as isSectionExpandedByUser is false this will be computed by a default rule
819 * on every render.
820 */
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200821 private isSectionExpanded = new Map<Category, boolean>();
Ben Rohlfsfee542a2021-02-24 13:24:54 +0100822
Ben Rohlfs99949282021-03-03 19:19:21 +0100823 /**
824 * Keeps track of whether the user intentionally changed the expansion state.
825 * Once this is true the default rule for showing a section expanded or not
826 * is not applied anymore.
827 */
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200828 private isSectionExpandedByUser = new Map<Category, boolean>();
Ben Rohlfs99949282021-03-03 19:19:21 +0100829
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200830 private readonly getViewModel = resolve(this, changeViewModelToken);
831
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000832 private readonly getChangeModel = resolve(this, changeModelToken);
Chris Poucet01422482021-11-30 19:43:28 +0100833
Chris Poucet776a68f2022-01-10 19:17:02 +0100834 private readonly getChecksModel = resolve(this, checksModelToken);
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100835
Ben Rohlfscced9112021-12-22 16:34:54 +0100836 private readonly reporting = getAppContext().reportingService;
837
Chris Poucet5ec77f02022-05-12 11:25:21 +0200838 constructor() {
839 super();
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200840 subscribe(
841 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200842 () => this.getChecksModel().topLevelActionsSelected$,
Ben Rohlfs6e798cf2021-11-25 21:53:14 +0100843 x => (this.actions = x)
844 );
845 subscribe(
846 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200847 () => this.getChecksModel().topLevelLinksSelected$,
Ben Rohlfs6e798cf2021-11-25 21:53:14 +0100848 x => (this.links = x)
849 );
850 subscribe(
851 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200852 () => this.getChecksModel().checksSelectedPatchsetNumber$,
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200853 x => (this.checksPatchsetNumber = x)
854 );
Chris Poucet01422482021-11-30 19:43:28 +0100855 subscribe(
856 this,
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200857 () => this.getChecksModel().checksSelectedAttemptNumber$,
858 x => (this.selectedAttempt = x)
859 );
860 subscribe(
861 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200862 () => this.getChangeModel().latestPatchNum$,
Chris Poucet01422482021-11-30 19:43:28 +0100863 x => (this.latestPatchsetNumber = x)
864 );
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200865 subscribe(
866 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200867 () => this.getChecksModel().someProvidersAreLoadingSelected$,
Ben Rohlfs0ca140a2021-09-03 10:43:37 +0200868 x => (this.someProvidersAreLoading = x)
869 );
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200870 subscribe(
871 this,
872 () => this.getViewModel().checksRunsSelected$,
873 x => (this.selectedRuns = x)
874 );
Ben Rohlfs209f1412022-09-30 12:25:46 +0200875 subscribe(
876 this,
877 () => this.getViewModel().checksResultsFilter$,
878 x => (this.filterRegExp = x)
879 );
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100880 }
881
Gerrit Code Review79031232021-08-19 14:32:41 +0000882 static override get styles() {
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100883 return [
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +0200884 formStyles,
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100885 sharedStyles,
Ben Rohlfsc3f6e722021-05-17 17:13:03 +0200886 spinnerStyles,
Ben Rohlfsaa60f932021-09-13 22:06:50 +0200887 fontStyles,
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100888 css`
889 :host {
890 display: block;
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100891 background-color: var(--background-color-secondary);
892 }
893 .header {
894 display: block;
895 background-color: var(--background-color-primary);
896 padding: var(--spacing-l) var(--spacing-xl) var(--spacing-m)
897 var(--spacing-xl);
898 border-bottom: 1px solid var(--border-color);
899 }
Ben Rohlfsb508b402021-06-04 11:32:14 +0200900 .header.notLatest {
901 background-color: var(--emphasis-color);
902 }
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100903 .headerTopRow,
904 .headerBottomRow {
905 display: flex;
906 justify-content: space-between;
907 align-items: flex-end;
908 }
909 .headerTopRow gr-dropdown-list {
910 border: 1px solid var(--border-color);
911 border-radius: var(--border-radius);
912 padding: 0 var(--spacing-m);
913 }
Ben Rohlfsc3f6e722021-05-17 17:13:03 +0200914 .headerTopRow h2 {
915 display: inline-block;
916 }
917 .headerTopRow .loading {
918 display: inline-block;
919 margin-left: var(--spacing-m);
920 color: var(--deemphasized-text-color);
921 }
922 /* The basics of .loadingSpin are defined in shared styles. */
923 .headerTopRow .loadingSpin {
924 display: inline-block;
925 margin-left: var(--spacing-s);
926 width: 18px;
927 height: 18px;
928 vertical-align: top;
929 }
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100930 .headerBottomRow {
931 margin-top: var(--spacing-s);
932 }
Ben Rohlfsb508b402021-06-04 11:32:14 +0200933 .headerTopRow .right,
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100934 .headerBottomRow .right {
935 display: flex;
936 align-items: center;
937 }
Ben Rohlfsb508b402021-06-04 11:32:14 +0200938 .headerTopRow .right .goToLatest {
939 display: none;
940 }
941 .notLatest .headerTopRow .right .goToLatest {
942 display: block;
943 }
Ben Rohlfsebef2e12022-09-01 17:22:24 +0200944 .headerTopRow .right > * {
945 margin-left: var(--spacing-m);
946 }
Ben Rohlfsb508b402021-06-04 11:32:14 +0200947 .headerTopRow .right .goToLatest gr-button {
Ben Rohlfs4037d2f2021-10-05 22:31:07 +0200948 --gr-button-padding: var(--spacing-s) var(--spacing-m);
Ben Rohlfsb508b402021-06-04 11:32:14 +0200949 }
Chris Poucet1c713862022-07-25 13:12:24 +0200950 .headerBottomRow gr-icon {
Ben Rohlfs0ae33e32021-04-23 10:01:00 +0200951 color: var(--link-color);
Ben Rohlfs37f0c772021-05-26 16:25:18 +0200952 }
953 .headerBottomRow .space {
954 display: inline-block;
955 width: var(--spacing-xl);
956 height: var(--line-height-normal);
957 }
958 .headerBottomRow a {
Ben Rohlfs0ae33e32021-04-23 10:01:00 +0200959 margin-right: var(--spacing-l);
960 }
Chris Poucet1c713862022-07-25 13:12:24 +0200961 #moreActions gr-icon {
Ben Rohlfs6c97c572021-03-22 09:53:03 +0100962 color: var(--link-color);
963 }
964 #moreMessage {
965 display: none;
966 }
967 .body {
968 display: block;
969 padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
970 var(--spacing-xl);
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100971 }
Ben Rohlfs260cce02021-03-05 16:57:09 +0100972 .filterDiv {
973 display: flex;
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100974 margin-top: var(--spacing-s);
Ben Rohlfs260cce02021-03-05 16:57:09 +0100975 align-items: center;
976 }
977 .filterDiv input#filterInput {
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +0100978 padding: var(--spacing-s) var(--spacing-m);
979 min-width: 400px;
980 }
Ben Rohlfs260cce02021-03-05 16:57:09 +0100981 .filterDiv .selection {
982 padding: var(--spacing-s) var(--spacing-m);
983 }
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100984 .categoryHeader {
Ben Rohlfs348368a2021-02-17 11:42:09 +0100985 margin-top: var(--spacing-l);
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100986 margin-left: var(--spacing-l);
Ben Rohlfsfee542a2021-02-24 13:24:54 +0100987 cursor: default;
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100988 }
Ben Rohlfs99949282021-03-03 19:19:21 +0100989 .categoryHeader .title {
990 text-transform: capitalize;
991 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +0100992 .categoryHeader .expandIcon {
993 width: var(--line-height-h3);
994 height: var(--line-height-h3);
995 margin-right: var(--spacing-s);
996 }
Ben Rohlfs5929e4b2021-05-17 14:53:29 +0200997 .categoryHeader .statusIconWrapper {
998 display: inline-block;
999 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001000 .categoryHeader .statusIcon {
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001001 position: relative;
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001002 top: 2px;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001003 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001004 .categoryHeader .statusIcon.error {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001005 color: var(--error-foreground);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001006 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001007 .categoryHeader .statusIcon.warning {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001008 color: var(--warning-foreground);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001009 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001010 .categoryHeader .statusIcon.info {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001011 color: var(--info-foreground);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001012 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001013 .categoryHeader .statusIcon.success {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001014 color: var(--success-foreground);
1015 }
Chris Poucet1c713862022-07-25 13:12:24 +02001016 .categoryHeader.empty gr-icon.statusIcon {
Ben Rohlfsb2f9b1b2021-12-14 10:10:32 +01001017 color: var(--deemphasized-text-color);
1018 }
Ben Rohlfs260cce02021-03-05 16:57:09 +01001019 .categoryHeader .filtered {
1020 color: var(--deemphasized-text-color);
1021 }
Ben Rohlfs99949282021-03-03 19:19:21 +01001022 .collapsed .noResultsMessage,
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001023 .collapsed table {
1024 display: none;
1025 }
1026 .collapsed {
1027 border-bottom: 1px solid var(--border-color);
1028 padding-bottom: var(--spacing-m);
1029 }
Ben Rohlfs99949282021-03-03 19:19:21 +01001030 .noResultsMessage {
1031 width: 100%;
Ben Rohlfs99949282021-03-03 19:19:21 +01001032 margin-top: var(--spacing-m);
1033 background-color: var(--background-color-primary);
1034 box-shadow: var(--elevation-level-1);
1035 padding: var(--spacing-s)
1036 calc(20px + var(--spacing-l) + var(--spacing-m) + var(--spacing-s));
Ben Rohlfs3af24fe2021-02-17 12:25:28 +01001037 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001038 table.resultsTable {
1039 width: 100%;
Ben Rohlfs07209832021-05-18 15:23:06 +02001040 table-layout: fixed;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001041 margin-top: var(--spacing-m);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001042 background-color: var(--background-color-primary);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001043 box-shadow: var(--elevation-level-1);
1044 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001045 tr.headerRow th {
1046 text-align: left;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001047 font-weight: var(--font-weight-bold);
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001048 padding: var(--spacing-s);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001049 }
Ben Rohlfsdaba0e82021-06-11 08:06:17 +02001050 tr.headerRow th.nameCol {
Ben Rohlfs894dd292021-06-16 12:10:20 +02001051 padding-left: var(--spacing-l);
Ben Rohlfsc1a36752021-11-15 12:27:57 +01001052 width: 200px;
1053 }
1054 @media screen and (min-width: 1400px) {
1055 tr.headerRow th.nameCol.longNames {
1056 width: 300px;
1057 }
Ben Rohlfs07209832021-05-18 15:23:06 +02001058 }
Ben Rohlfsdaba0e82021-06-11 08:06:17 +02001059 tr.headerRow th.summaryCol {
Ben Rohlfs07209832021-05-18 15:23:06 +02001060 width: 99%;
1061 }
Ben Rohlfsdaba0e82021-06-11 08:06:17 +02001062 tr.headerRow th.expanderCol {
Ben Rohlfs07209832021-05-18 15:23:06 +02001063 width: 30px;
Ben Rohlfs894dd292021-06-16 12:10:20 +02001064 padding-right: var(--spacing-l);
Ben Rohlfs07209832021-05-18 15:23:06 +02001065 }
1066
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001067 gr-button.showAll {
1068 margin: var(--spacing-m);
1069 }
1070 tr {
1071 border-top: 1px solid var(--border-color);
1072 }
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001073 `,
1074 ];
1075 }
1076
Gerrit Code Review79031232021-08-19 14:32:41 +00001077 protected override updated(changedProperties: PropertyValues) {
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001078 super.updated(changedProperties);
Ben Rohlfs209f1412022-09-30 12:25:46 +02001079 if (changedProperties.has('filterRegExp') && this.filterInput) {
1080 this.filterInput.value = this.filterRegExp;
1081 }
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001082 if (changedProperties.has('tabState') && this.tabState) {
1083 const {statusOrCategory, checkName} = this.tabState;
Ben Rohlfsd58e07e2021-06-24 13:22:24 +02001084 if (isCategory(statusOrCategory)) {
1085 const expanded = this.isSectionExpanded.get(statusOrCategory);
1086 if (!expanded) this.toggleExpanded(statusOrCategory);
1087 }
1088 if (checkName) {
1089 this.scrollElIntoView(`gr-result-row.${charsOnly(checkName)}`);
1090 } else if (
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001091 statusOrCategory &&
1092 statusOrCategory !== RunStatus.RUNNING &&
1093 statusOrCategory !== RunStatus.RUNNABLE
1094 ) {
Ben Rohlfsd58e07e2021-06-24 13:22:24 +02001095 const cat = statusOrCategory.toString().toLowerCase();
Ben Rohlfsea7c3d42021-06-18 12:26:22 +02001096 this.scrollElIntoView(`.categoryHeader.${cat} + table gr-result-row`);
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001097 }
1098 }
1099 }
1100
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001101 private scrollElIntoView(selector: string) {
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001102 this.updateComplete.then(() => {
1103 let el = this.shadowRoot?.querySelector(selector);
Ben Rohlfs6e62a0c2021-06-07 13:13:40 +02001104 // el might be a <gr-result-row> with an empty shadowRoot. Let's wait a
1105 // moment before trying to find a child element in it.
1106 setTimeout(() => {
Ben Rohlfsea7c3d42021-06-18 12:26:22 +02001107 if (el) (el as HTMLElement).focus();
Ben Rohlfsa8bcf742024-06-14 08:27:11 +02001108 // If the target element is a <gr-result-row>, then expand it.
1109 (el as GrResultRow)?.toggleExpanded(true);
Ben Rohlfs6e62a0c2021-06-07 13:13:40 +02001110 // <gr-result-row> has display:contents and cannot be scrolled into view
1111 // itself. Thus we are preferring to scroll the first child into view.
1112 el = el?.shadowRoot?.firstElementChild ?? el;
1113 el?.scrollIntoView({block: 'center'});
1114 }, 0);
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001115 });
1116 }
1117
Gerrit Code Review79031232021-08-19 14:32:41 +00001118 override render() {
Ben Rohlfsece23f92021-09-30 14:58:37 +02001119 const headerClasses = {
Ben Rohlfsb508b402021-06-04 11:32:14 +02001120 header: true,
1121 notLatest: !!this.checksPatchsetNumber,
Ben Rohlfsece23f92021-09-30 14:58:37 +02001122 };
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001123 const attemptItems = this.createAttemptDropdownItems();
Ben Rohlfsece23f92021-09-30 14:58:37 +02001124 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001125 <div class=${classMap(headerClasses)}>
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001126 <div class="headerTopRow">
1127 <div class="left">
1128 <h2 class="heading-2">Results</h2>
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001129 <div class="loading" ?hidden=${!this.someProvidersAreLoading}>
Ben Rohlfsc3f6e722021-05-17 17:13:03 +02001130 <span>Loading results </span>
1131 <span class="loadingSpin"></span>
1132 </div>
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001133 </div>
1134 <div class="right">
Ben Rohlfsb508b402021-06-04 11:32:14 +02001135 <div class="goToLatest">
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001136 <gr-button @click=${this.goToLatestPatchset} link
Ben Rohlfsb508b402021-06-04 11:32:14 +02001137 >Go to latest patchset</gr-button
1138 >
1139 </div>
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001140 ${when(
1141 attemptItems.length > 0,
1142 () => html` <gr-dropdown-list
1143 value=${this.selectedAttempt ?? 0}
1144 .items=${attemptItems}
1145 @value-change=${this.onAttemptSelected}
1146 ></gr-dropdown-list>`
1147 )}
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001148 <gr-dropdown-list
Dhruv Srivastava3ceffa22023-10-30 14:00:17 +01001149 value=${(this.checksPatchsetNumber ||
1150 this.latestPatchsetNumber) ??
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001151 0}
1152 .items=${this.createPatchsetDropdownItems()}
1153 @value-change=${this.onPatchsetSelected}
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001154 ></gr-dropdown-list>
1155 </div>
1156 </div>
1157 <div class="headerBottomRow">
Frank Borden6988bdf2021-04-07 14:42:00 +02001158 <div class="left">${this.renderFilter()}</div>
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001159 <div class="right">${this.renderLinksAndActions()}</div>
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001160 </div>
1161 </div>
1162 <div class="body">
1163 ${this.renderSection(Category.ERROR)}
1164 ${this.renderSection(Category.WARNING)}
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001165 ${this.renderSection(Category.INFO)}
1166 ${this.renderSection(Category.SUCCESS)}
Ben Rohlfsece23f92021-09-30 14:58:37 +02001167 </div>
1168 `;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001169 }
1170
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001171 private renderLinksAndActions() {
Ben Rohlfs37f0c772021-05-26 16:25:18 +02001172 const links = this.links ?? [];
Ben Rohlfs12964842021-06-04 12:39:00 +02001173 const primaryLinks = links
1174 .filter(a => a.primary)
1175 // Showing the same icons twice without text is super confusing.
1176 .filter(
1177 (link: Link, index: number, array: Link[]) =>
1178 array.findIndex(other => link.icon === other.icon) === index
1179 )
1180 .slice(0, 4);
Ben Rohlfs37f0c772021-05-26 16:25:18 +02001181 const overflowLinks = links.filter(a => !primaryLinks.includes(a));
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001182 const overflowLinkItems = overflowLinks.map(link => {
Ben Rohlfs37f0c772021-05-26 16:25:18 +02001183 return {
1184 ...link,
1185 id: link.tooltip,
1186 name: link.tooltip,
1187 target: '_blank',
1188 tooltip: undefined,
1189 };
1190 });
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001191
1192 const actions = this.actions ?? [];
1193 const primaryActions = actions.filter(a => a.primary).slice(0, 2);
1194 const overflowActions = actions.filter(a => !primaryActions.includes(a));
1195 const overflowActionItems = overflowActions.map(action => {
1196 return {...action, id: action.name};
1197 });
1198 const disabledActions = overflowActionItems
1199 .filter(action => action.disabled)
1200 .map(action => action.id);
1201
Ben Rohlfs37f0c772021-05-26 16:25:18 +02001202 return html`
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001203 ${primaryLinks.map(this.renderLink)}
1204 ${primaryLinks.length > 0 && primaryActions.length > 0
1205 ? html`<div class="space"></div>`
1206 : ''}
1207 ${primaryActions.map(this.renderAction)}
1208 ${this.renderOverflow(
1209 [...overflowLinkItems, ...overflowActionItems],
1210 disabledActions
1211 )}
Ben Rohlfs37f0c772021-05-26 16:25:18 +02001212 `;
1213 }
1214
1215 private renderLink(link?: Link) {
1216 if (!link) return;
Ben Rohlfs0ae33e32021-04-23 10:01:00 +02001217 const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
Chris Poucet5ca5e802022-07-18 15:26:29 +02001218 const icon = iconForLink(link.icon);
Ben Rohlfs3630a382023-06-23 12:53:18 +02001219 return html`<a href=${link.url} target="_blank" rel="noopener noreferrer"
Chris Poucet1c713862022-07-25 13:12:24 +02001220 ><gr-icon
1221 icon=${icon.name}
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001222 aria-label=${tooltipText}
Chris Poucet1c713862022-07-25 13:12:24 +02001223 class="link"
1224 ?filled=${icon.filled}
1225 ></gr-icon>
1226 <paper-tooltip offset="5">${tooltipText}</paper-tooltip></a
Ben Rohlfs0ae33e32021-04-23 10:01:00 +02001227 >`;
1228 }
1229
Ben Rohlfs1f2017f2021-09-07 11:47:09 +02001230 private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
1231 if (items.length === 0) return;
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001232 return html`
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001233 <gr-dropdown
1234 id="moreActions"
1235 link=""
1236 vertical-offset="32"
1237 horizontal-align="right"
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001238 @tap-item=${this.handleAction}
1239 .items=${items}
1240 .disabledIds=${disabledIds}
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001241 >
Chris Poucet1c713862022-07-25 13:12:24 +02001242 <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001243 <span id="moreMessage">More</span>
1244 </gr-dropdown>
1245 `;
1246 }
1247
1248 private handleAction(e: CustomEvent<Action>) {
Ben Rohlfscced9112021-12-22 16:34:54 +01001249 this.getChecksModel().triggerAction(
1250 e.detail,
1251 undefined,
1252 'results-dropdown'
1253 );
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001254 }
1255
Ben Rohlfs48330a12021-12-09 15:12:17 +01001256 private handleFilter(e: ChecksResultsFilterEvent) {
Ben Rohlfs48330a12021-12-09 15:12:17 +01001257 const newValue = e.detail.filterRegExp ?? '';
Ben Rohlfs209f1412022-09-30 12:25:46 +02001258 this.getViewModel().updateState({
1259 checksResultsFilter: this.filterRegExp === newValue ? '' : newValue,
1260 });
Ben Rohlfs48330a12021-12-09 15:12:17 +01001261 }
1262
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001263 private renderAction(action?: Action) {
1264 if (!action) return;
Ben Rohlfscced9112021-12-22 16:34:54 +01001265 return html`<gr-checks-action
1266 context="results"
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001267 .action=${action}
Ben Rohlfscced9112021-12-22 16:34:54 +01001268 ></gr-checks-action>`;
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001269 }
1270
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001271 private onAttemptSelected(e: CustomEvent<{value: string | undefined}>) {
1272 const attempt = stringToAttemptChoice(e.detail.value);
1273 assertIsDefined(attempt, `unexpected attempt choice ${e.detail.value}`);
1274 this.getChecksModel().updateStateSetAttempt(attempt);
1275 }
1276
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001277 private onPatchsetSelected(e: CustomEvent<{value: string}>) {
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +02001278 let patchset: number | undefined = Number(e.detail.value);
1279 assert(Number.isInteger(patchset), `patchset must be integer: ${patchset}`);
1280 if (patchset === this.latestPatchsetNumber) patchset = undefined;
1281 this.getChecksModel().updateStateSetPatchset(
1282 patchset as PatchSetNumber | undefined
1283 );
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001284 }
1285
Ben Rohlfsb508b402021-06-04 11:32:14 +02001286 private goToLatestPatchset() {
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +02001287 this.getChecksModel().updateStateSetPatchset(undefined);
Ben Rohlfsb508b402021-06-04 11:32:14 +02001288 }
1289
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001290 private createAttemptDropdownItems() {
1291 if (this.runs.every(run => run.isSingleAttempt)) return [];
1292 const attempts: AttemptChoice[] = this.runs
1293 .map(run => run.attempt ?? 0)
1294 .filter(isAttemptChoice)
1295 .filter(unique);
1296 attempts.push(LATEST_ATTEMPT);
1297 attempts.push(ALL_ATTEMPTS);
1298 const items: DropdownItem[] = attempts.sort(sortAttemptChoices).map(a => {
1299 return {
1300 value: a,
1301 text: attemptChoiceLabel(a),
1302 };
1303 });
1304 return items;
1305 }
1306
Ben Rohlfs6c97c572021-03-22 09:53:03 +01001307 private createPatchsetDropdownItems() {
1308 if (!this.latestPatchsetNumber) return [];
1309 return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
1310 assertIsDefined(this.latestPatchsetNumber, 'latestPatchsetNumber');
1311 const index = this.latestPatchsetNumber - i;
1312 const postfix = index === this.latestPatchsetNumber ? ' (latest)' : '';
1313 return {
1314 value: `${index}`,
1315 text: `Patchset ${index}${postfix}`,
1316 };
1317 });
1318 }
1319
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001320 isRunSelected(run: {checkName: string}) {
Ben Rohlfs20fe8e82022-10-14 11:13:10 +02001321 return this.selectedRuns.size === 0 || this.selectedRuns.has(run.checkName);
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001322 }
1323
1324 renderFilter() {
1325 const runs = this.runs.filter(
1326 run =>
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001327 this.isRunSelected(run) && isAttemptSelected(this.selectedAttempt, run)
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001328 );
Ben Rohlfs209f1412022-09-30 12:25:46 +02001329 if (
Ben Rohlfs20fe8e82022-10-14 11:13:10 +02001330 this.selectedRuns.size === 0 &&
Ben Rohlfs209f1412022-09-30 12:25:46 +02001331 allResults(runs).length <= 3 &&
1332 this.filterRegExp === ''
1333 ) {
Ben Rohlfs88453492021-03-03 11:32:40 +01001334 return;
1335 }
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +01001336 return html`
Ben Rohlfs260cce02021-03-05 16:57:09 +01001337 <div class="filterDiv">
1338 <input
1339 id="filterInput"
1340 type="text"
Ben Rohlfs48330a12021-12-09 15:12:17 +01001341 placeholder="Filter results by tag or regular expression"
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001342 @input=${this.onFilterInputChange}
Ben Rohlfs260cce02021-03-05 16:57:09 +01001343 />
Ben Rohlfs260cce02021-03-05 16:57:09 +01001344 </div>
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +01001345 `;
1346 }
1347
Ben Rohlfs48330a12021-12-09 15:12:17 +01001348 onFilterInputChange() {
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +01001349 assertIsDefined(this.filterInput, 'filter <input> element');
Ben Rohlfscced9112021-12-22 16:34:54 +01001350 this.reporting.reportInteraction(
1351 Interaction.CHECKS_RESULT_FILTER_CHANGED,
1352 {},
1353 {deduping: Deduping.EVENT_ONCE_PER_CHANGE}
1354 );
Ben Rohlfs209f1412022-09-30 12:25:46 +02001355 this.getViewModel().updateState({
1356 checksResultsFilter: this.filterInput.value,
1357 });
Ben Rohlfs2b38e5f2021-02-22 16:43:40 +01001358 }
1359
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001360 renderSection(category: Category) {
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001361 const catString = category.toString().toLowerCase();
Ben Rohlfs8519b5d2021-06-11 08:29:40 +02001362 const isWarningOrError =
1363 category === Category.WARNING || category === Category.ERROR;
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001364 const allRuns = this.runs.filter(run =>
Ben Rohlfsebef2e12022-09-01 17:22:24 +02001365 isAttemptSelected(this.selectedAttempt, run)
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001366 );
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001367 const all = allRuns.reduce(
1368 (results: RunResult[], run) => [
1369 ...results,
Frank Borden6988bdf2021-04-07 14:42:00 +02001370 ...this.computeRunResults(category, run),
1371 ],
1372 []
1373 );
Ben Rohlfs20fe8e82022-10-14 11:13:10 +02001374 const isSelectionActive = this.selectedRuns.size > 0;
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001375 const selected = all.filter(result => this.isRunSelected(result));
Ben Rohlfs209f1412022-09-30 12:25:46 +02001376 const re = new RegExp(this.filterRegExp, 'i');
1377 const filtered = selected.filter(result => matches(result, re));
1378 const isFilterActiveWithResults =
1379 this.filterRegExp !== '' && filtered.length > 0;
1380
1381 // The logic for deciding whether to expand a section by default is a bit
1382 // complicated, but we want to collapse empty and info/success sections by
1383 // default for a clean and focused user experience. However, as soon as the
1384 // user starts selecting or filtering we must take this into account and
1385 // prefer to expand the sections.
Ben Rohlfs99949282021-03-03 19:19:21 +01001386 let expanded = this.isSectionExpanded.get(category);
1387 const expandedByUser = this.isSectionExpandedByUser.get(category) ?? false;
1388 if (!expandedByUser || expanded === undefined) {
Ben Rohlfs209f1412022-09-30 12:25:46 +02001389 // Note that we are using `selected` for `isEmpty` and not `filtered`,
1390 // because if the filter is what makes a section empty, then we want to
1391 // show an expanded section, which contains a message about this.
1392 const isEmpty = selected.length === 0;
1393 expanded =
1394 !isEmpty &&
1395 (isWarningOrError || isSelectionActive || isFilterActiveWithResults);
Ben Rohlfs99949282021-03-03 19:19:21 +01001396 this.isSectionExpanded.set(category, expanded);
1397 }
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001398 const expandedClass = expanded ? 'expanded' : 'collapsed';
Ben Rohlfs209f1412022-09-30 12:25:46 +02001399
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001400 const isShowAll = this.isShowAll.get(category) ?? false;
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001401 const resultCount = filtered.length;
Ben Rohlfsb2f9b1b2021-12-14 10:10:32 +01001402 const empty = resultCount === 0 ? 'empty' : '';
Ben Rohlfsdc766952021-06-17 14:51:45 +02001403 const resultLimit = isShowAll ? 1000 : 20;
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001404 const showAllButton = this.renderShowAllButton(
1405 category,
1406 isShowAll,
Ben Rohlfs8519b5d2021-06-11 08:29:40 +02001407 resultLimit,
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001408 resultCount
1409 );
Chris Poucet5ca5e802022-07-18 15:26:29 +02001410 const icon = iconFor(category);
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001411 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001412 <div class=${expandedClass}>
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001413 <h3
Ben Rohlfsb2f9b1b2021-12-14 10:10:32 +01001414 class="categoryHeader ${catString} ${empty} heading-3"
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001415 @click=${() => this.toggleExpanded(category)}
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001416 >
Chris Poucet1c713862022-07-25 13:12:24 +02001417 <gr-icon
1418 class="expandIcon"
1419 icon=${expanded ? 'expand_less' : 'expand_more'}
1420 ></gr-icon>
Ben Rohlfs5929e4b2021-05-17 14:53:29 +02001421 <div class="statusIconWrapper">
Chris Poucet1c713862022-07-25 13:12:24 +02001422 <gr-icon
1423 icon=${icon.name}
1424 ?filled=${icon.filled}
1425 class="statusIcon ${catString}"
1426 ></gr-icon>
Ben Rohlfs5929e4b2021-05-17 14:53:29 +02001427 <span class="title">${catString}</span>
Ben Rohlfs2b5df092021-06-15 11:20:27 +02001428 <span class="count">${this.renderCount(all, filtered)}</span>
Ben Rohlfs5929e4b2021-05-17 14:53:29 +02001429 <paper-tooltip offset="5"
1430 >${CATEGORY_TOOLTIPS.get(category)}</paper-tooltip
1431 >
1432 </div>
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001433 </h3>
Ben Rohlfs741fa492022-07-06 14:36:20 +02001434 ${when(expanded, () =>
1435 this.renderResults(
1436 all,
1437 selected,
1438 filtered,
1439 resultLimit,
1440 showAllButton
1441 )
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001442 )}
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001443 </div>
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001444 `;
1445 }
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001446
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001447 renderShowAllButton(
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001448 category: Category,
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001449 isShowAll: boolean,
1450 showAllThreshold: number,
1451 resultCount: number
1452 ) {
1453 if (resultCount <= showAllThreshold) return;
1454 const message = isShowAll ? 'Show Less' : `Show All (${resultCount})`;
1455 const handler = () => this.toggleShowAll(category);
1456 return html`
1457 <tr class="showAllRow">
Ben Rohlfsdaba0e82021-06-11 08:06:17 +02001458 <td colspan="3">
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001459 <gr-button class="showAll" link @click=${handler}
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001460 >${message}</gr-button
1461 >
1462 </td>
1463 </tr>
1464 `;
1465 }
1466
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001467 toggleShowAll(category: Category) {
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001468 const current = this.isShowAll.get(category) ?? false;
1469 this.isShowAll.set(category, !current);
Ben Rohlfscced9112021-12-22 16:34:54 +01001470 this.reporting.reportInteraction(
1471 Interaction.CHECKS_RESULT_SECTION_SHOW_ALL,
1472 {
1473 category,
1474 showAll: !current,
1475 }
1476 );
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001477 this.requestUpdate();
1478 }
1479
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001480 renderResults(
1481 all: RunResult[],
1482 selected: RunResult[],
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001483 filtered: RunResult[],
1484 limit: number,
1485 showAll: TemplateResult | undefined
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001486 ) {
1487 if (all.length === 0) {
1488 return html`<div class="noResultsMessage">No results</div>`;
1489 }
1490 if (selected.length === 0) {
Ben Rohlfs260cce02021-03-05 16:57:09 +01001491 return html`<div class="noResultsMessage">
1492 No results for this filtered view
1493 </div>`;
1494 }
Ben Rohlfs99949282021-03-03 19:19:21 +01001495 if (filtered.length === 0) {
Ben Rohlfs260cce02021-03-05 16:57:09 +01001496 return html`<div class="noResultsMessage">
1497 No results match the regular expression
1498 </div>`;
Ben Rohlfs99949282021-03-03 19:19:21 +01001499 }
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001500 filtered = filtered.slice(0, limit);
Ben Rohlfsc1a36752021-11-15 12:27:57 +01001501 // Some hosts/plugins use really long check names. If we have space and the
1502 // check names are indeed very long, then set a more generous nameCol width.
1503 const longestNameLength = Math.max(...all.map(r => r.checkName.length));
1504 const nameColClasses = {nameCol: true, longNames: longestNameLength > 25};
Ben Rohlfs99949282021-03-03 19:19:21 +01001505 return html`
1506 <table class="resultsTable">
1507 <thead>
1508 <tr class="headerRow">
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001509 <th class=${classMap(nameColClasses)}>Run</th>
Ben Rohlfs99949282021-03-03 19:19:21 +01001510 <th class="summaryCol">Summary</th>
1511 <th class="expanderCol"></th>
1512 </tr>
1513 </thead>
Ben Rohlfs48330a12021-12-09 15:12:17 +01001514 <tbody @checks-results-filter=${this.handleFilter}>
Ben Rohlfsa684e012021-03-22 13:53:57 +01001515 ${repeat(
1516 filtered,
Frank Borden2be1c8d2023-11-30 12:25:32 +01001517 // @ts-ignore: temporarily unblock typescript 5.3 migration
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +02001518 result => result.internalResultId,
Chris Poucetca77c662023-02-23 13:52:19 +01001519 (result?: RunResult) => html`
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001520 <gr-result-row
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001521 class=${charsOnly(result!.checkName)}
1522 .result=${result}
Ben Rohlfsed7d5db2021-03-05 15:12:18 +01001523 ></gr-result-row>
1524 `
Ben Rohlfs99949282021-03-03 19:19:21 +01001525 )}
Ben Rohlfsfabe7492021-04-23 11:21:16 +02001526 ${showAll}
Ben Rohlfs99949282021-03-03 19:19:21 +01001527 </tbody>
1528 </table>
1529 `;
1530 }
1531
Ben Rohlfs2b5df092021-06-15 11:20:27 +02001532 renderCount(all: RunResult[], filtered: RunResult[]) {
Ben Rohlfs99949282021-03-03 19:19:21 +01001533 if (all.length === filtered.length) {
1534 return html`(${all.length})`;
Ben Rohlfs99949282021-03-03 19:19:21 +01001535 }
Ben Rohlfse6a4c472021-04-22 13:06:09 +02001536 return html`(${filtered.length} of ${all.length})`;
Ben Rohlfs99949282021-03-03 19:19:21 +01001537 }
1538
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001539 toggleExpanded(category: Category) {
Ben Rohlfs99949282021-03-03 19:19:21 +01001540 const expanded = this.isSectionExpanded.get(category);
1541 assertIsDefined(expanded, 'expanded must have been set in initial render');
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001542 this.isSectionExpanded.set(category, !expanded);
Ben Rohlfs99949282021-03-03 19:19:21 +01001543 this.isSectionExpandedByUser.set(category, true);
Ben Rohlfscced9112021-12-22 16:34:54 +01001544 this.reporting.reportInteraction(Interaction.CHECKS_RESULT_SECTION_TOGGLE, {
1545 expanded: !expanded,
1546 category,
1547 });
Ben Rohlfsfee542a2021-02-24 13:24:54 +01001548 this.requestUpdate();
1549 }
1550
Ben Rohlfs68b47232023-06-26 12:53:41 +02001551 computeRunResults(category: Category, run: CheckRun): RunResult[] {
Ben Rohlfs4a5ac592021-05-19 15:11:03 +02001552 if (category === Category.SUCCESS && hasCompletedWithoutResults(run)) {
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001553 return [this.computeSuccessfulRunResult(run)];
1554 }
Ben Rohlfs99949282021-03-03 19:19:21 +01001555 return (
1556 run.results
1557 ?.filter(result => result.category === category)
Ben Rohlfs68b47232023-06-26 12:53:41 +02001558 .map(result => runResult(run, result)) ?? []
Ben Rohlfs99949282021-03-03 19:19:21 +01001559 );
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001560 }
1561
Ben Rohlfs99949282021-03-03 19:19:21 +01001562 computeSuccessfulRunResult(run: CheckRun): RunResult {
Ben Rohlfs68b47232023-06-26 12:53:41 +02001563 const adaptedRun: RunResult = runResult(run, {
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +02001564 internalResultId: run.internalRunId + '-0',
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +02001565 category: Category.SUCCESS,
Ben Rohlfs23a466c2021-02-17 12:03:53 +01001566 summary: run.statusDescription ?? '',
Ben Rohlfs68b47232023-06-26 12:53:41 +02001567 });
Ben Rohlfs5e5c89e2021-02-25 11:09:33 +01001568 if (!run.statusDescription) {
1569 const start = run.scheduledTimestamp ?? run.startedTimestamp;
1570 const end = run.finishedTimestamp;
1571 let duration = '';
1572 if (start && end) {
1573 duration = ` in ${durationString(start, end, true)}`;
1574 }
1575 adaptedRun.message = `Completed without results${duration}.`;
1576 }
1577 if (run.statusLink) {
1578 adaptedRun.links = [
1579 {
1580 url: run.statusLink,
1581 primary: true,
1582 icon: LinkIcon.EXTERNAL,
1583 },
1584 ];
1585 }
Ben Rohlfs99949282021-03-03 19:19:21 +01001586 return adaptedRun;
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001587 }
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001588}
1589
1590declare global {
1591 interface HTMLElementTagNameMap {
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +01001592 'gr-result-row': GrResultRow;
Ben Rohlfse6c57d02021-01-25 11:22:39 +01001593 'gr-result-expanded': GrResultExpanded;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001594 'gr-checks-results': GrChecksResults;
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001595 }
1596}