blob: 18cc0762f6f7b7aa553cf3428d2b164c9db33bc3 [file] [log] [blame]
Ben Rohlfsc9f90982020-12-13 22:00:11 +01001/**
2 * @license
3 * Copyright (C) 2020 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Ben Rohlfs85a866b2021-04-15 11:20:02 +020017import {
18 Action,
19 Category,
Ben Rohlfs85a866b2021-04-15 11:20:02 +020020 CheckResult as CheckResultApi,
Ben Rohlfsf457f892021-06-18 10:57:44 +020021 CheckRun as CheckRunApi,
22 Link,
Ben Rohlfs85a866b2021-04-15 11:20:02 +020023 LinkIcon,
24 RunStatus,
25} from '../../api/checks';
Ben Rohlfs913ab762021-02-05 12:52:00 +010026import {assertNever} from '../../utils/common-util';
Ben Rohlfsa684e012021-03-22 13:53:57 +010027import {CheckResult, CheckRun} from './checks-model';
Ben Rohlfsc9f90982020-12-13 22:00:11 +010028
Ben Rohlfs345e9462021-04-19 15:18:27 +020029export function iconForLink(linkIcon?: LinkIcon) {
30 if (linkIcon === undefined) return 'launch';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020031 switch (linkIcon) {
32 case LinkIcon.EXTERNAL:
33 return 'launch';
34 case LinkIcon.IMAGE:
35 return 'insert-photo';
36 case LinkIcon.HISTORY:
37 return 'restore';
38 case LinkIcon.DOWNLOAD:
39 return 'download';
40 case LinkIcon.DOWNLOAD_MOBILE:
41 return 'system-update';
42 case LinkIcon.HELP_PAGE:
43 return 'help-outline';
44 case LinkIcon.REPORT_BUG:
45 return 'bug';
Ben Rohlfs7d262be2021-06-02 14:59:03 +020046 case LinkIcon.CODE:
47 return 'code';
Ben Rohlfs17b19b32021-06-22 11:14:36 +020048 case LinkIcon.FILE_PRESENT:
49 return 'file-present';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020050 default:
Ben Rohlfs5d92c392021-04-20 10:14:57 +020051 // We don't throw an assertion error here, because plugins don't have to
52 // be written in TypeScript, so we may encounter arbitrary strings for
53 // linkIcon.
54 return 'launch';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020055 }
56}
57
Ben Rohlfs5d92c392021-04-20 10:14:57 +020058export function tooltipForLink(linkIcon?: LinkIcon) {
59 if (linkIcon === undefined) return 'Link to details';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020060 switch (linkIcon) {
61 case LinkIcon.EXTERNAL:
62 return 'Link to details';
63 case LinkIcon.IMAGE:
64 return 'Link to image';
65 case LinkIcon.HISTORY:
66 return 'Link to result history';
67 case LinkIcon.DOWNLOAD:
68 return 'Download';
69 case LinkIcon.DOWNLOAD_MOBILE:
70 return 'Download';
71 case LinkIcon.HELP_PAGE:
72 return 'Link to help page';
73 case LinkIcon.REPORT_BUG:
74 return 'Link for reporting a problem';
Ben Rohlfs17b19b32021-06-22 11:14:36 +020075 case LinkIcon.CODE:
76 return 'Link to code';
77 case LinkIcon.FILE_PRESENT:
78 return 'Link to file';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020079 default:
Ben Rohlfs5d92c392021-04-20 10:14:57 +020080 // We don't throw an assertion error here, because plugins don't have to
81 // be written in TypeScript, so we may encounter arbitrary strings for
82 // linkIcon.
83 return 'Link to details';
Ben Rohlfsa8bb0da2021-04-12 08:56:20 +020084 }
85}
86
Ben Rohlfsc9f90982020-12-13 22:00:11 +010087export function worstCategory(run: CheckRun) {
Ben Rohlfs913ab762021-02-05 12:52:00 +010088 if (hasResultsOf(run, Category.ERROR)) return Category.ERROR;
89 if (hasResultsOf(run, Category.WARNING)) return Category.WARNING;
90 if (hasResultsOf(run, Category.INFO)) return Category.INFO;
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +020091 if (hasResultsOf(run, Category.SUCCESS)) return Category.SUCCESS;
Ben Rohlfsc9f90982020-12-13 22:00:11 +010092 return undefined;
93}
94
Ben Rohlfsd58e07e2021-06-24 13:22:24 +020095export function isCategory(
96 catStat?: Category | RunStatus
97): catStat is Category {
98 return (
99 catStat === Category.ERROR ||
100 catStat === Category.WARNING ||
101 catStat === Category.INFO ||
102 catStat === Category.SUCCESS
103 );
104}
105
106export function isStatus(catStat?: Category | RunStatus): catStat is RunStatus {
Ben Rohlfsf457f892021-06-18 10:57:44 +0200107 return (
108 catStat === RunStatus.COMPLETED ||
109 catStat === RunStatus.RUNNABLE ||
110 catStat === RunStatus.RUNNING
111 );
112}
113
114export function labelFor(catStat: Category | RunStatus) {
115 switch (catStat) {
116 case Category.ERROR:
117 return 'error';
118 case Category.INFO:
119 return 'info';
120 case Category.WARNING:
121 return 'warning';
122 case Category.SUCCESS:
123 return 'success';
124 case RunStatus.COMPLETED:
125 return 'completed';
126 case RunStatus.RUNNABLE:
127 return 'runnable';
128 case RunStatus.RUNNING:
129 return 'running';
130 default:
131 assertNever(catStat, `Unsupported category/status: ${catStat}`);
132 }
133}
134
135export function iconFor(catStat: Category | RunStatus) {
136 switch (catStat) {
Ben Rohlfs913ab762021-02-05 12:52:00 +0100137 case Category.ERROR:
138 return 'error';
139 case Category.INFO:
140 return 'info-outline';
141 case Category.WARNING:
142 return 'warning';
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200143 case Category.SUCCESS:
Ben Rohlfsfee542a2021-02-24 13:24:54 +0100144 return 'check-circle-outline';
Ben Rohlfsf457f892021-06-18 10:57:44 +0200145 // Note that this is only for COMPLETED without results!
146 case RunStatus.COMPLETED:
147 return 'check-circle-outline';
148 case RunStatus.RUNNABLE:
149 return 'placeholder';
150 case RunStatus.RUNNING:
151 return 'timelapse';
Ben Rohlfs913ab762021-02-05 12:52:00 +0100152 default:
Ben Rohlfsf457f892021-06-18 10:57:44 +0200153 assertNever(catStat, `Unsupported category/status: ${catStat}`);
Ben Rohlfs913ab762021-02-05 12:52:00 +0100154 }
155}
156
Ben Rohlfs5ee0cd42021-05-27 12:13:54 +0200157export enum PRIMARY_STATUS_ACTIONS {
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100158 RERUN = 'rerun',
159 RUN = 'run',
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100160}
161
162export function toCanonicalAction(action: Action, status: RunStatus) {
163 let name = action.name.toLowerCase();
164 if (status === RunStatus.COMPLETED && (name === 'run' || name === 're-run')) {
165 name = PRIMARY_STATUS_ACTIONS.RERUN;
166 }
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100167 return {...action, name};
168}
169
Ben Rohlfs24743b1d2021-09-23 10:02:46 +0200170export function headerForStatus(status: RunStatus) {
171 switch (status) {
172 case RunStatus.COMPLETED:
173 return 'Completed';
174 case RunStatus.RUNNABLE:
175 return 'Not run';
176 case RunStatus.RUNNING:
177 return 'Running';
178 default:
179 assertNever(status, `Unsupported status: ${status}`);
180 }
181}
182
Ben Rohlfsc50ac6e2021-09-07 11:00:08 +0200183function primaryActionName(status: RunStatus) {
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100184 switch (status) {
185 case RunStatus.COMPLETED:
186 return PRIMARY_STATUS_ACTIONS.RERUN;
187 case RunStatus.RUNNABLE:
188 return PRIMARY_STATUS_ACTIONS.RUN;
189 case RunStatus.RUNNING:
Ben Rohlfsc50ac6e2021-09-07 11:00:08 +0200190 return undefined;
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100191 default:
192 assertNever(status, `Unsupported status: ${status}`);
193 }
194}
195
Ben Rohlfs5ee0cd42021-05-27 12:13:54 +0200196export function primaryRunAction(run?: CheckRun): Action | undefined {
197 if (!run) return undefined;
Ben Rohlfs586c4ef2021-04-22 18:05:29 +0200198 return runActions(run).filter(
Ben Rohlfsd70a3372021-05-18 12:52:12 +0200199 action => !action.disabled && action.name === primaryActionName(run.status)
Ben Rohlfs586c4ef2021-04-22 18:05:29 +0200200 )[0];
201}
202
203export function runActions(run?: CheckRun): Action[] {
204 if (!run?.actions) return [];
205 return run.actions.map(action => toCanonicalAction(action, run.status));
Ben Rohlfsa77fd1d2021-02-15 10:18:45 +0100206}
207
Ben Rohlfs913ab762021-02-05 12:52:00 +0100208export function iconForRun(run: CheckRun) {
Ben Rohlfse5100672021-02-25 13:05:15 +0100209 if (run.status !== RunStatus.COMPLETED) {
Ben Rohlfsf457f892021-06-18 10:57:44 +0200210 return iconFor(run.status);
Ben Rohlfse5100672021-02-25 13:05:15 +0100211 } else {
212 const category = worstCategory(run);
Ben Rohlfsf457f892021-06-18 10:57:44 +0200213 return category ? iconFor(category) : iconFor(run.status);
Ben Rohlfs913ab762021-02-05 12:52:00 +0100214 }
215}
216
Ben Rohlfsc0c2ea52021-01-21 09:45:16 +0100217export function hasCompleted(run: CheckRun) {
218 return run.status === RunStatus.COMPLETED;
219}
220
221export function isRunning(run: CheckRun) {
222 return run.status === RunStatus.RUNNING;
223}
224
225export function isRunningOrHasCompleted(run: CheckRun) {
226 return run.status === RunStatus.COMPLETED || run.status === RunStatus.RUNNING;
227}
228
229export function hasCompletedWithoutResults(run: CheckRun) {
230 return run.status === RunStatus.COMPLETED && (run.results ?? []).length === 0;
231}
232
Ben Rohlfs913ab762021-02-05 12:52:00 +0100233export function hasCompletedWith(run: CheckRun, category: Category) {
234 return hasCompleted(run) && hasResultsOf(run, category);
235}
236
Ben Rohlfs88453492021-03-03 11:32:40 +0100237export function hasResults(run: CheckRun): boolean {
238 return (run.results ?? []).length > 0;
239}
240
241export function allResults(runs: CheckRun[]): CheckResult[] {
Frank Borden6988bdf2021-04-07 14:42:00 +0200242 return runs.reduce(
243 (results: CheckResult[], run: CheckRun) => [
244 ...results,
245 ...(run.results ?? []),
246 ],
247 []
248 );
Ben Rohlfs88453492021-03-03 11:32:40 +0100249}
250
Ben Rohlfs913ab762021-02-05 12:52:00 +0100251export function hasResultsOf(run: CheckRun, category: Category) {
252 return getResultsOf(run, category).length > 0;
253}
254
255export function getResultsOf(run: CheckRun, category: Category) {
256 return (run.results ?? []).filter(r => r.category === category);
257}
258
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100259export function compareByWorstCategory(a: CheckRun, b: CheckRun) {
260 return level(worstCategory(b)) - level(worstCategory(a));
261}
262
263export function level(cat?: Category) {
264 if (!cat) return -1;
265 switch (cat) {
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200266 case Category.SUCCESS:
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100267 return 0;
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200268 case Category.INFO:
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100269 return 1;
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200270 case Category.WARNING:
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100271 return 2;
Ben Rohlfs6cc7e7b2021-05-10 13:54:38 +0200272 case Category.ERROR:
273 return 3;
Ben Rohlfsc9f90982020-12-13 22:00:11 +0100274 }
275}
Ben Rohlfs05da4242021-02-19 15:30:57 +0100276
277export interface ActionTriggeredEventDetail {
278 action: Action;
279 run?: CheckRun;
280}
281
282export type ActionTriggeredEvent = CustomEvent<ActionTriggeredEventDetail>;
283
284declare global {
285 interface HTMLElementEventMap {
286 'action-triggered': ActionTriggeredEvent;
287 }
288}
289
Ben Rohlfs85a866b2021-04-15 11:20:02 +0200290export interface AttemptDetail {
291 attempt: number | undefined;
292 icon: string;
293}
294
295export interface AttemptInfo {
296 latestAttempt: number | undefined;
297 isSingleAttempt: boolean;
298 attempts: AttemptDetail[];
299}
300
301export function createAttemptMap(runs: CheckRunApi[]) {
302 const map = new Map<string, AttemptInfo>();
303 for (const run of runs) {
304 const value = map.get(run.checkName);
305 const detail = {
306 attempt: run.attempt,
307 icon: iconForRun(fromApiToInternalRun(run)),
308 };
309 if (value === undefined) {
310 map.set(run.checkName, {
311 latestAttempt: run.attempt,
312 isSingleAttempt: true,
313 attempts: [detail],
314 });
315 continue;
316 }
317 if (!run.attempt || !value.latestAttempt) {
318 throw new Error(
319 'If multiple run attempts are provided, ' +
320 'then each run must have the "attempt" property set.'
321 );
322 }
323 value.isSingleAttempt = false;
324 if (run.attempt > value.latestAttempt) {
325 value.latestAttempt = run.attempt;
326 }
327 value.attempts.push(detail);
328 }
329 return map;
330}
331
332export function fromApiToInternalRun(run: CheckRunApi): CheckRun {
333 return {
334 ...run,
Ben Rohlfsc767e662021-08-05 15:23:32 +0200335 pluginName: 'fake',
Ben Rohlfs85a866b2021-04-15 11:20:02 +0200336 internalRunId: 'fake',
337 isSingleAttempt: false,
338 isLatestAttempt: false,
339 attemptDetails: [],
340 results: (run.results ?? []).map(fromApiToInternalResult),
341 };
342}
343
344export function fromApiToInternalResult(result: CheckResultApi): CheckResult {
345 return {
346 ...result,
347 internalResultId: 'fake',
348 };
349}
Ben Rohlfsb04d8562021-05-26 13:57:15 +0200350
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200351function allPrimaryLinks(result?: CheckResultApi): Link[] {
Ben Rohlfs3bc81282021-06-02 13:48:17 +0200352 return (result?.links ?? []).filter(link => link.primary);
Ben Rohlfsb04d8562021-05-26 13:57:15 +0200353}
354
Ben Rohlfs3bc81282021-06-02 13:48:17 +0200355export function firstPrimaryLink(result?: CheckResultApi): Link | undefined {
356 return allPrimaryLinks(result).find(link => link.icon === LinkIcon.EXTERNAL);
357}
358
359export function otherPrimaryLinks(result?: CheckResultApi): Link[] {
360 const first = firstPrimaryLink(result);
Ben Rohlfsfb8e8482021-06-02 14:39:11 +0200361 return allPrimaryLinks(result).filter(link => link !== first);
Ben Rohlfs3bc81282021-06-02 13:48:17 +0200362}
363
364export function secondaryLinks(result?: CheckResultApi): Link[] {
365 return (result?.links ?? []).filter(link => !link.primary);
Ben Rohlfsb04d8562021-05-26 13:57:15 +0200366}