blob: 15841f3c70e2dcd3bcea5bf2e1e1fb73e57c221b [file] [log] [blame]
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Action,
Category,
CheckRun as CheckRunApi,
CheckResult as CheckResultApi,
LinkIcon,
RunStatus,
} from '../../api/checks';
import {assertNever} from '../../utils/common-util';
import {CheckResult, CheckRun} from './checks-model';
export function iconForLink(linkIcon?: LinkIcon) {
if (linkIcon === undefined) return 'launch';
switch (linkIcon) {
case LinkIcon.EXTERNAL:
return 'launch';
case LinkIcon.IMAGE:
return 'insert-photo';
case LinkIcon.HISTORY:
return 'restore';
case LinkIcon.DOWNLOAD:
return 'download';
case LinkIcon.DOWNLOAD_MOBILE:
return 'system-update';
case LinkIcon.HELP_PAGE:
return 'help-outline';
case LinkIcon.REPORT_BUG:
return 'bug';
default:
// We don't throw an assertion error here, because plugins don't have to
// be written in TypeScript, so we may encounter arbitrary strings for
// linkIcon.
return 'launch';
}
}
export function tooltipForLink(linkIcon?: LinkIcon) {
if (linkIcon === undefined) return 'Link to details';
switch (linkIcon) {
case LinkIcon.EXTERNAL:
return 'Link to details';
case LinkIcon.IMAGE:
return 'Link to image';
case LinkIcon.HISTORY:
return 'Link to result history';
case LinkIcon.DOWNLOAD:
return 'Download';
case LinkIcon.DOWNLOAD_MOBILE:
return 'Download';
case LinkIcon.HELP_PAGE:
return 'Link to help page';
case LinkIcon.REPORT_BUG:
return 'Link for reporting a problem';
default:
// We don't throw an assertion error here, because plugins don't have to
// be written in TypeScript, so we may encounter arbitrary strings for
// linkIcon.
return 'Link to details';
}
}
export function worstCategory(run: CheckRun) {
if (hasResultsOf(run, Category.ERROR)) return Category.ERROR;
if (hasResultsOf(run, Category.WARNING)) return Category.WARNING;
if (hasResultsOf(run, Category.INFO)) return Category.INFO;
return undefined;
}
export function iconForCategory(category: Category | 'SUCCESS') {
switch (category) {
case Category.ERROR:
return 'error';
case Category.INFO:
return 'info-outline';
case Category.WARNING:
return 'warning';
case 'SUCCESS':
return 'check-circle-outline';
default:
assertNever(category, `Unsupported category: ${category}`);
}
}
enum PRIMARY_STATUS_ACTIONS {
RERUN = 'rerun',
RUN = 'run',
CANCEL = 'cancel',
}
export function toCanonicalAction(action: Action, status: RunStatus) {
let name = action.name.toLowerCase();
if (status === RunStatus.COMPLETED && (name === 'run' || name === 're-run')) {
name = PRIMARY_STATUS_ACTIONS.RERUN;
}
if (status === RunStatus.RUNNING && name === 'stop') {
name = PRIMARY_STATUS_ACTIONS.CANCEL;
}
return {...action, name};
}
export function primaryActionName(status: RunStatus) {
switch (status) {
case RunStatus.COMPLETED:
return PRIMARY_STATUS_ACTIONS.RERUN;
case RunStatus.RUNNABLE:
return PRIMARY_STATUS_ACTIONS.RUN;
case RunStatus.RUNNING:
return PRIMARY_STATUS_ACTIONS.CANCEL;
default:
assertNever(status, `Unsupported status: ${status}`);
}
}
export function primaryRunAction(run: CheckRun): Action | undefined {
return runActions(run).filter(
action => action.name === primaryActionName(run.status)
)[0];
}
export function runActions(run?: CheckRun): Action[] {
if (!run?.actions) return [];
return run.actions.map(action => toCanonicalAction(action, run.status));
}
export function iconForRun(run: CheckRun) {
if (run.status !== RunStatus.COMPLETED) {
return iconForStatus(run.status);
} else {
const category = worstCategory(run);
return category ? iconForCategory(category) : iconForStatus(run.status);
}
}
export function iconForStatus(status: RunStatus) {
switch (status) {
// Note that this is only for COMPLETED without results!
case RunStatus.COMPLETED:
return 'check-circle-outline';
case RunStatus.RUNNABLE:
return 'placeholder';
case RunStatus.RUNNING:
return 'timelapse';
default:
assertNever(status, `Unsupported status: ${status}`);
}
}
export function hasCompleted(run: CheckRun) {
return run.status === RunStatus.COMPLETED;
}
export function isRunning(run: CheckRun) {
return run.status === RunStatus.RUNNING;
}
export function isRunningOrHasCompleted(run: CheckRun) {
return run.status === RunStatus.COMPLETED || run.status === RunStatus.RUNNING;
}
export function hasCompletedWithoutResults(run: CheckRun) {
return run.status === RunStatus.COMPLETED && (run.results ?? []).length === 0;
}
export function hasCompletedWith(run: CheckRun, category: Category) {
return hasCompleted(run) && hasResultsOf(run, category);
}
export function hasResults(run: CheckRun): boolean {
return (run.results ?? []).length > 0;
}
export function allResults(runs: CheckRun[]): CheckResult[] {
return runs.reduce(
(results: CheckResult[], run: CheckRun) => [
...results,
...(run.results ?? []),
],
[]
);
}
export function hasResultsOf(run: CheckRun, category: Category) {
return getResultsOf(run, category).length > 0;
}
export function getResultsOf(run: CheckRun, category: Category) {
return (run.results ?? []).filter(r => r.category === category);
}
export function compareByWorstCategory(a: CheckRun, b: CheckRun) {
return level(worstCategory(b)) - level(worstCategory(a));
}
export function level(cat?: Category) {
if (!cat) return -1;
switch (cat) {
case Category.INFO:
return 0;
case Category.WARNING:
return 1;
case Category.ERROR:
return 2;
}
}
export interface ActionTriggeredEventDetail {
action: Action;
run?: CheckRun;
}
export type ActionTriggeredEvent = CustomEvent<ActionTriggeredEventDetail>;
declare global {
interface HTMLElementEventMap {
'action-triggered': ActionTriggeredEvent;
}
}
export function fireActionTriggered(
target: EventTarget,
action: Action,
run?: CheckRun
) {
target.dispatchEvent(
new CustomEvent('action-triggered', {
detail: {action, run},
composed: true,
bubbles: true,
})
);
}
export interface AttemptDetail {
attempt: number | undefined;
icon: string;
}
export interface AttemptInfo {
latestAttempt: number | undefined;
isSingleAttempt: boolean;
attempts: AttemptDetail[];
}
export function createAttemptMap(runs: CheckRunApi[]) {
const map = new Map<string, AttemptInfo>();
for (const run of runs) {
const value = map.get(run.checkName);
const detail = {
attempt: run.attempt,
icon: iconForRun(fromApiToInternalRun(run)),
};
if (value === undefined) {
map.set(run.checkName, {
latestAttempt: run.attempt,
isSingleAttempt: true,
attempts: [detail],
});
continue;
}
if (!run.attempt || !value.latestAttempt) {
throw new Error(
'If multiple run attempts are provided, ' +
'then each run must have the "attempt" property set.'
);
}
value.isSingleAttempt = false;
if (run.attempt > value.latestAttempt) {
value.latestAttempt = run.attempt;
}
value.attempts.push(detail);
}
return map;
}
export function fromApiToInternalRun(run: CheckRunApi): CheckRun {
return {
...run,
internalRunId: 'fake',
isSingleAttempt: false,
isLatestAttempt: false,
attemptDetails: [],
results: (run.results ?? []).map(fromApiToInternalResult),
};
}
export function fromApiToInternalResult(result: CheckResultApi): CheckResult {
return {
...result,
internalResultId: 'fake',
};
}