blob: e52c339943f9d0232c3fcc8daf276ffb4936bce5 [file] [log] [blame]
/**
* @fileoverview A custom element that loads the CT pending tasks queue and
* displays it as a table.
*/
import 'elements-sk/icon/delete-icon-sk';
import 'elements-sk/icon/cancel-icon-sk';
import 'elements-sk/icon/check-circle-icon-sk';
import 'elements-sk/icon/help-icon-sk';
import 'elements-sk/toast-sk';
import { $$, DomReady } from 'common-sk/modules/dom';
import { fromObject } from 'common-sk/modules/query';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { define } from 'elements-sk/define';
import { errorMessage } from 'elements-sk/errorMessage';
import { html } from 'lit-html';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import {
getFormattedTimestamp, taskDescriptors, getTimestamp, getCtDbTimestamp, TaskDescriptor,
} from '../ctfe_utils';
import {
CommonCols,
DeleteTaskRequest,
GetTasksResponse,
} from '../json';
function hideDialog(e: Event) {
const classList = (e.target as HTMLElement).classList;
if (classList.contains('dialog-background')) {
classList.add('hidden');
}
}
function formatTask(task: CommonCols) {
return JSON.stringify(task, null, 4);
}
export class TaskQueueSk extends ElementSk {
private _pendingTasks: CommonCols[] = [];
private _running: boolean = false;
constructor() {
super(TaskQueueSk.template);
}
private static template = (el: TaskQueueSk) => html`
<table class="runssummary surface-themes-sk secondary-links" id=queue>
<tr>
<th>Queue Position</th>
<th>Added</th>
<th>Task Type</th>
<th>User</th>
<th>Swarming Logs</th>
<th>Request</th>
</tr>
${el._pendingTasks.map((task: CommonCols, index: number) => TaskQueueSk.taskRowTemplate(el, task, index))}
</table>
${el._pendingTasks.map((task, index) => TaskQueueSk.taskDetailDialogTemplate(task, index))}
<toast-sk id=confirm_toast duration=5000></toast-sk>
`;
private static taskRowTemplate = (el: TaskQueueSk, task: CommonCols, index: number) => html`
<tr>
<td class=nowrap>
${index + 1}
<delete-icon-sk title="Delete this task" alt=Delete ?hidden=${!task.can_delete}
@click=${() => el.confirmDeleteTask(index)}></delete-icon-sk>
</td>
<td>
${getFormattedTimestamp(task.ts_added)}
${task.future_date
? html`</br><span class=error-themes-sk>(scheduled in the future)</span>`
: ''}
</td>
<td>${task.task_type}</td>
<td>${task.username}</td>
<td class=nowrap>${
task.future_date
? html`N/A`
: task.swarming_logs
? html`<a href="${task.swarming_logs}" rel=noopener target=_blank>Swarming Logs</a>`
: html`No Swarming Logs`}</td>
<td class=nowrap>
<a href=# class=details
@click=${() => el.showDetailsDialog(index)}>Task Details</a>
</td>
</tr>`;
private static taskDetailDialogTemplate = (task: CommonCols, index: number) => html`
<div id=${`detailsDialog${index}`} class="dialog-background hidden overlay-themes-sk"
@click=${hideDialog}>
<div class="dialog-content surface-themes-sk">
<pre>${formatTask(task)}</pre>
</div>
</div>
`;
connectedCallback(): void {
super.connectedCallback();
if (this._running) {
return;
}
this._running = true;
// We wait for everything to load so scaffolding event handlers are
// attached.
DomReady.then(() => {
this.dispatchEvent(new CustomEvent('begin-task', { bubbles: true }));
this._render();
this.loadTaskQueue().then(() => {
this._render();
this._running = false;
this.dispatchEvent(new CustomEvent('end-task', { bubbles: true }));
});
});
}
showDetailsDialog(index: number): void {
($$(`#detailsDialog${index}`, this) as HTMLElement).classList.remove('hidden');
}
// Dispatch requests to fetch tasks in queue. Returns a promise that resolves
// when all task data fetching/updating is complete.
loadTaskQueue(): Promise<void[]> {
this._pendingTasks = [];
// TODO(rmistry):
// This should really use the task_common.QueryParams type and
// the parameters should be POST'ed instead of sent via queryStr.
const queryParams = {
size: 100,
not_completed: true,
include_future_runs: false,
};
let queryStr = `?${fromObject(queryParams)}`;
const allPromises = [];
for (const obj of taskDescriptors) {
allPromises.push(fetch(obj.get_url + queryStr, { method: 'POST' })
.then(jsonOrThrow)
.then((json) => {
this.updatePendingTasks(json, obj);
})
.catch(errorMessage));
}
// Find all tasks scheduled in the future.
const futureQueryParams = {
include_future_runs: true,
};
queryStr = `?${fromObject(futureQueryParams)}`;
for (const obj of taskDescriptors) {
allPromises.push(fetch(obj.get_url + queryStr, { method: 'POST' })
.then(jsonOrThrow)
.then((json) => {
this.updatePendingTasks(json, obj);
})
.catch(errorMessage));
}
return Promise.all(allPromises);
}
// Add responses to pending tasks list.
updatePendingTasks(json: GetTasksResponse, taskDescriptor: TaskDescriptor): void {
const tasks = json.data;
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i] as CommonCols;
task.can_delete = json.permissions![i].DeleteAllowed;
task.id = json.ids![i];
task.task_type = taskDescriptor.type;
task.get_url = taskDescriptor.get_url;
task.delete_url = taskDescriptor.delete_url;
// Check if this is a completed task set to repeat.
if (task.repeat_after_days !== 0 && task.task_done) {
// Calculate the future date.
const timestamp = getTimestamp(task.ts_added);
timestamp.setDate(timestamp.getDate() + task.repeat_after_days);
task.future_date = true;
task.ts_added = +getCtDbTimestamp(new Date(timestamp));
}
}
this._pendingTasks = this._pendingTasks.concat(tasks);
// Sort pending tasks according to TsAdded.
this._pendingTasks.sort((a, b) => a.ts_added - b.ts_added);
}
confirmDeleteTask(index: number): void {
const confirmed = window.confirm('Delete this task?');
if (confirmed) {
this.deleteTask(index);
}
}
deleteTask(index: number): void {
const pendingTask = this._pendingTasks[index];
const params: DeleteTaskRequest = { id: pendingTask.id };
// params.id = pendingTask.Id;
fetch(pendingTask.delete_url, { method: 'POST', body: JSON.stringify(params) })
.then((res) => {
if (res.ok) {
this._pendingTasks.splice(index, 1);
window.alert(`Deleted ${pendingTask.task_type} task ${pendingTask.id}`);
return;
}
// Non-OK status. Read the response and punt it to the catch.
res.text().then((text) => { throw new Error(`Failed to delete the task: ${text}`); });
})
.then(() => {
this._render();
})
.catch(errorMessage);
}
}
define('task-queue-sk', TaskQueueSk);