blob: 1be8f41751493c90960c674a0621c8bf6891923a [file] [log] [blame]
/**
* @module modules/details-dialog-sk
* @description <h2><code>details-dialog-sk</code></h2>
*
* @property repo {string} - The repo associated with the tasks/taskspecs/commits that will be
* displayed.
*/
import { define } from 'elements-sk/define';
import { errorMessage } from 'elements-sk/errorMessage';
import { html, TemplateResult } from 'lit-html';
import { until } from 'lit-html/directives/until.js';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { $$ } from 'common-sk/modules/dom';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { Login } from '../../../infra-sk/modules/login';
import { escapeAndLinkify } from '../../../infra-sk/modules/linkify';
import { Commit } from '../util';
import { Task, Comment } from '../rpc';
import { CommentData } from '../comments-sk/comments-sk';
import {
logsUrl, revisionUrlTemplate, swarmingUrl, taskSchedulerUrl,
} from '../settings';
import '../comments-sk';
import 'elements-sk/styles/buttons';
import 'elements-sk/icon/close-icon-sk';
import 'elements-sk/icon/content-copy-icon-sk';
import 'elements-sk/icon/launch-icon-sk';
import '../../../infra-sk/modules/task-driver-sk';
// Type defining the text and action of the upper-right button of the dialog.
// For reverts of commits and re-running of tasks.
interface Action {
buttonText: string;
handler: ()=> void;
}
export class DetailsDialogSk extends ElementSk {
// This template is essentially a title section with optional action button, an optional details
// section, and a comments-sk section. Task, TaskSpec, and Commits set the appropriate sections
// before rendering.
private static template = (el: DetailsDialogSk) => html`
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
<header>
<div>${el.titleSection}</div>
<div class=spacer></div>
${
el.actionButton
? html`<button class="action" @click=${el.actionButton.handler}>
${el.actionButton.buttonText}
</button>`
: html``
}
<button class=close @click=${() => el.close()}><close-icon-sk></close-icon-sk></button>
</header>
${
el.detailsSection
? [
el.detailsSection,
html`
<br />
<hr />
`,
]
: html``
}
<div>
<comments-sk
.commentData=${el.commentData}
.allowAdd=${true}
.allowDelete=${true}
.showIgnoreFailure=${el.showCommentsIgnoreFailure}
.showFlaky=${el.showCommentsFlaky}
.editRights=${el.canEditComments}
></comments-sk>
</div>
</div>
`;
private titleSection: TemplateResult = html``;
private detailsSection: TemplateResult | null = null;
private actionButton: Action | null = null;
private showCommentsIgnoreFailure: boolean = false;
private showCommentsFlaky: boolean = false;
private canEditComments = false;
private _repo: string = '';
private commentData?: CommentData;
constructor() {
super(DetailsDialogSk.template);
this._upgradeProperty('repo');
}
connectedCallback() {
super.connectedCallback();
Login.then((res: any) => {
this.canEditComments = res.Email !== '';
this._render();
});
this._render();
}
private open() {
this._render();
(<HTMLElement> this).style.display = 'block';
document.addEventListener('keydown', this.keydown);
}
close() {
(<HTMLElement> this).style.display = 'none';
document.removeEventListener('keydown', this.keydown);
}
private keydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.close();
}
};
// reset clears the computed templates to ensure we don't have any leftovers if switching from
// e.g.a task view to a taskspec view.
private reset() {
this.actionButton = null;
this.titleSection = html``;
this.detailsSection = null;
this.showCommentsFlaky = false;
this.showCommentsIgnoreFailure = false;
const commentInput = $$('comments-sk input-sk', this) as HTMLInputElement;
if (commentInput) {
commentInput.value = '';
}
}
displayTask(task: Task, comments: Array<Comment>, commitsByHash: Map<string, Commit>) {
this.reset();
this.commentData = {
comments: comments,
taskId: task.id,
commit: '',
taskSpec: '',
repo: this.repo,
};
const td = fetch(`/json/td/${task.id}`)
.then(jsonOrThrow)
.then((td) => html`<br /><task-driver-sk id="tdStatus" .data=${td} embedded></task-driver-sk>`);
// We don't catch failures, since we don't want the promise to resolve (and be used below)
// unless the task-driver-sk has data.
this.titleSection = html`${until(
td,
html`
<h3>
<span>${task.name}</span
><a target="_blank" rel="noopener noreferrer" href="${logsUrl(task.swarmingTaskId)}"
><launch-icon-sk></launch-icon-sk>
</a>
</h3>
<div>
<table>
<tr>
<td>Status:</td>
<td class=${`task-${(task.status || 'PENDING').toLowerCase()}`}>${task.status}</td>
</tr>
<tr>
<td>Context:</td>
<td>
<a href=${this.taskUrl(task)} target="_blank" rel="noopener noreferrer">
View on Task Scheduler
</a>
</td>
</tr>
<tr>
<td>This Task:</td>
<td>
<a
target="_blank"
rel="noopener noreferrer"
href="${swarmingUrl()}/task?id=${task.swarmingTaskId}"
>
View on Swarming
</a>
</td>
</tr>
<tr>
<td>Other Tasks Like This:</td>
<td>
<a target="_blank" rel="noopener noreferrer" href=${this.swarmingUrl(task.name)}>
View on Swarming
</a>
</td>
</tr>
</table>
</div>
`,
)}`;
this.detailsSection = html`
<h3>Blamelist</h3>
<table class="blamelist">
${task.commits?.map((hash: string) => {
const commit = commitsByHash.get(hash);
return html`
<tr>
<td>
<a href="${revisionUrlTemplate(this.repo)}${hash}">${commit?.shortHash || ''}</a>
</td>
<td>${commit?.shortAuthor || ''}</td>
<td>${commit?.shortSubject || ''}</td>
</tr>
`;
})}
</table>
`;
this.actionButton = { buttonText: 'Re-run Job', handler: () => this.rerunJob(task) };
this.open();
}
displayTaskSpec(taskspec: string, comments: Comment[]) {
this.reset();
this.showCommentsFlaky = true;
this.showCommentsIgnoreFailure = true;
this.commentData = {
comments: comments,
taskId: '',
commit: '',
taskSpec: taskspec,
repo: this.repo,
};
this.titleSection = html` <h3>
<a href="${this.swarmingUrl(taskspec)}" target="_blank" rel="noopener noreferrer">
${taskspec}
</a>
</h3>`;
this.open();
}
displayCommit(commit: Commit, comments: Array<Comment>) {
this.reset();
this.showCommentsIgnoreFailure = true;
this.commentData = {
comments: comments,
taskId: '',
commit: commit.hash,
taskSpec: '',
repo: this.repo,
};
this.titleSection = html`
<p>
<a
href="${revisionUrlTemplate(this.repo)}${commit.hash}"
target="_blank"
rel="noopener noreferrer"
>
${commit.hash}
</a>
<content-copy-icon-sk
class="small-icon clickable"
@click=${() => {
navigator.clipboard.writeText(commit.hash);
}}
></content-copy-icon-sk>
<br />
${commit.author}
<br />
<span title="${commit.timestamp!}">${this.humanDate(commit.timestamp!)}</span>
</p>
`;
this.detailsSection = html`
<h3>${escapeAndLinkify(commit.subject)}</h3>
<p>${escapeAndLinkify(commit.body)}</p>
`;
if (commit.issue) {
this.actionButton = { buttonText: 'Revert', handler: () => this.revertCommit(commit) };
}
this.open();
}
private rerunJob(task: Task) {
if (!task || !task.name || !task.revision) {
errorMessage("Invalid task, can't be re-run");
}
// TODO(borenet): This is not correct in some cases. Now that we have a
// link from Task to Job in the scheduler, we should be able to come up
// with a better way to "re-open" a failed Job, essentially resetting
// the attempt counts so that we can retry the failed task(s).
let job = task.name;
const uploadPrefix = 'Upload-';
if (job.indexOf(uploadPrefix) == 0) {
job = job.substring(uploadPrefix.length);
}
const url = `${taskSchedulerUrl()}/trigger?submit=true&job=${job}@${task.revision}`;
const win = window.open(url, '_blank') as Window;
win.focus();
}
private revertCommit(commit: Commit) {
const url = commit.patchStorage === 'gerrit'
? `https://skia-review.googlesource.com/c/${commit.issue}/?revert`
: `https://codereview.chromium.org/${commit.issue}/revert`;
const win = window.open(url, '_blank') as Window;
win.focus();
}
private taskUrl(task: Task) {
const url = taskSchedulerUrl();
if (!task || !task.id || !url) {
return '';
}
return `${url}/task/${task.id}`;
}
private swarmingUrl(taskSpec: string) {
return `${swarmingUrl()}/tasklist?f=sk_name%3A${taskSpec}`;
}
private humanDate(timestamp: string) {
const date = new Date(timestamp);
const str = date.toString();
return `${date.toLocaleString()} ${str.substring(str.indexOf('('))}`;
}
get repo(): string {
return this._repo;
}
set repo(value) {
this._repo = value;
}
}
define('details-dialog-sk', DetailsDialogSk);