blob: 35afb7609de48a56f2fb85363bdaec9bf8bbb8e1 [file] [log] [blame]
/**
* @module autoroll/modules/arb-status-sk
* @description <h2><code>arb-status-sk</code></h2>
*
* <p>
* This element displays the status of a single Autoroller.
* </p>
*/
import { html } from 'lit-html';
import { $$ } from 'common-sk/modules/dom';
import { diffDate, localeTime } from 'common-sk/modules/human';
import { define } from 'elements-sk/define';
import 'elements-sk/styles/buttons';
import 'elements-sk/styles/select';
import 'elements-sk/styles/table';
import 'elements-sk/tabs-panel-sk';
import 'elements-sk/tabs-sk';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { LoginTo } from '../../../infra-sk/modules/login';
import { truncate } from '../../../infra-sk/modules/string';
import {
AutoRollConfig,
AutoRollCL,
AutoRollService,
AutoRollStatus,
CreateManualRollResponse,
GetAutoRollService,
ManualRoll,
ManualRoll_Result,
ManualRoll_Status,
Mode,
Revision,
Strategy,
TryJob,
SetModeResponse,
SetStrategyResponse,
GetStatusResponse,
AutoRollCL_Result,
TryJob_Result,
} from '../rpc';
interface RollCandidate {
revision: Revision;
roll: ManualRoll | null;
}
export class ARBStatusSk extends ElementSk {
private static template = (ele: ARBStatusSk) =>
!ele.status
? html``
: html`
<tabs-sk>
<button value="status">Roller Status</button>
<button value="manual">Trigger Manual Rolls</button>
</tabs-sk>
${
!ele.editRights
? html` <div id="pleaseLoginMsg" class="big">${ele.pleaseLoginMsg}</div> `
: html``
}
<tabs-panel-sk selected="0">
<div class="status">
<div id="loadstatus">
Reload (s)
<input
id="refreshInterval"
type="number"
value="${ele.refreshInterval}"
label="Reload (s)"
@input=${ele.reloadChanged}
></input>
Last loaded at <span>${localeTime(ele.lastLoaded)}</span>
</div>
<table>
${
ele.status.config?.parentWaterfall
? html`
<tr>
<td class="nowrap">Parent Repo Build Status</td>
<td class="nowrap unknown">
<span>
<a
href="${ele.status.config.parentWaterfall}"
target="_blank"
>
${ele.status.config.parentWaterfall}
</a>
</span>
</td>
</tr>
`
: html``
}
<tr>
<td class="nowrap">Current Mode:</td>
<td class="nowrap unknown">
<span class="big">${ele.status.mode?.mode
.toLowerCase()
.replace('_', ' ')}</span>
</td>
</tr>
<tr>
<td class="nowrap">Set By:</td>
<td class="nowrap unknown">
${ele.status.mode?.user}
${
ele.status.mode
? 'at ' + localeTime(new Date(ele.status.mode!.time!))
: html``
}
${
ele.status.mode?.message
? html`: ${ele.status.mode.message}`
: html``
}
</td>
</tr>
<tr>
<td class="nowrap">Change Mode:</td>
<td class="nowrap">
${Object.keys(Mode).map((mode: string) =>
mode === ele.status?.mode?.mode
? ''
: html`
<button
@click="${() => {
ele.modeButtonPressed(mode);
}}"
?disabled="${!ele.editRights || ele.modeChangePending}"
title="${ele.editRights
? 'Change the mode.'
: ele.pleaseLoginMsg}"
value="${mode}"
>
${ele.status?.mode?.mode
? ele.getModeButtonLabel(ele.status.mode.mode, mode)
: ''}
</button>
`
)}
</td>
</tr>
<tr>
<td class="nowrap">Status:</td>
<td class="nowrap">
<span class="${ele.statusClass(ele.status.status)}">
<span class="big">${ele.status.status}</span>
</span>
${
ele.status.status.indexOf('throttle') >= 0
? html`
<span
>until
${localeTime(new Date(ele.status.throttledUntil!))}</span
>
<button
@click="${ele.unthrottle}"
?disabled="${!ele.editRights}"
title="${ele.editRights
? 'Unthrottle the roller.'
: ele.pleaseLoginMsg}"
>
Force Unthrottle
</button>
`
: html``
}
${
ele.status.status.indexOf('waiting for roll window') >= 0
? html` <span>until ${localeTime(ele.rollWindowStart)}</span> `
: html``
}
</td>
</tr>
${
ele.editRights && ele.status.error
? html`
<tr>
<td class="nowrap">Error:</td>
<td><pre>${ele.status.error}</pre></td>
</tr>
`
: html``
}
<tr>
<td class="nowrap">Current Roll:</td>
<td>
<div>
${
ele.status.currentRoll
? html`
<a
href="${ele.issueURL(ele.status.currentRoll)}"
class="big"
target="_blank"
>
${ele.status.currentRoll.subject}
</a>
`
: html`<span>(none)</span>`
}
</div>
<div>
${
ele.status.currentRoll && ele.status.currentRoll.tryJobs
? ele.status.currentRoll.tryJobs.map(
(tryResult) => html`
<div class="trybot">
${tryResult.url
? html`
<a
href="${tryResult.url}"
class="${ele.trybotClass(tryResult)}"
target="_blank"
>
${tryResult.name}
</a>
`
: html`
<span
class="nowrap"
class="${ele.trybotClass(tryResult)}"
>
${tryResult.name}
</span>
`}
${tryResult.category === 'cq'
? html``
: html`
<span class="nowrap small"
>(${tryResult.category})</span
>
`}
</div>
`
)
: html``
}
</div>
</td>
</tr>
${
ele.status.lastRoll
? html`
<tr>
<td class="nowrap">Previous roll result:</td>
<td>
<span class="${ele.rollClass(ele.status.lastRoll)}">
${ele.rollResult(ele.status.lastRoll)}
</span>
<a
href="${ele.issueURL(ele.status.lastRoll)}"
target="_blank"
class="small"
>
(detail)
</a>
</td>
</tr>
`
: html``
}
<tr>
<td class="nowrap">History:</td>
<td>
<table>
<tr>
<th>Roll</th>
<th>Last Modified</th>
<th>Result</th>
</tr>
${ele.status.recentRolls?.map(
(roll: AutoRollCL) => html`
<tr>
<td>
<a href="${ele.issueURL(roll)}" target="_blank"
>${roll.subject}</a
>
</td>
<td>${diffDate(roll.modified!)} ago</td>
<td>
<span class="${ele.rollClass(roll)}"
>${ele.rollResult(roll)}</span
>
</td>
</tr>
`
)}
</table>
</td>
</tr>
<tr>
<td class="nowrap">Full History:</td>
<td>
<a href="${ele.status.fullHistoryUrl}" target="_blank">
${ele.status.fullHistoryUrl}
</a>
</td>
</tr>
<tr>
<td class="nowrap">Strategy for choosing next roll revision:</td>
<td class="nowrap">
<select
id="strategySelect"
?disabled="${!ele.editRights || ele.strategyChangePending}"
title="${
ele.editRights
? 'Change the strategy for choosing the next revision to roll.'
: ele.pleaseLoginMsg
}"
@change="${ele.selectedStrategyChanged}">
${Object.keys(Strategy).map(
(strategy: string) => html`
<option
value="${strategy}"
?selected="${strategy === ele.status?.strategy?.strategy}"
>
${strategy.toLowerCase().replace('_', ' ')}
</option>
`
)}
</select>
</td>
</tr>
<tr>
<td class="nowrap">Set By:</td>
<td class="nowrap unknown">
${ele.status.strategy?.user}
${
ele.status.strategy
? 'at ' + localeTime(new Date(ele.status.strategy!.time!))
: html``
}
${
ele.status.strategy?.message
? html`: ${ele.status.strategy.message}`
: html``
}
</td>
</tr>
</table>
</div>
<div class="manual">
<table>
${
ele.status.config?.supportsManualRolls
? html`
${
!ele.rollCandidates
? html`
The roller is up to date; there are no revisions which could
be manually rolled.
`
: html``
}
<tr>
<th>Revision</th>
<th>Description</th>
<th>Timestamp</th>
<th>Requester</th>
<th>Requested at</th>
<th>Roll</th>
</tr>
${ele.rollCandidates.map(
(rollCandidate) => html`
<tr class="rollCandidate">
<td>
${rollCandidate.revision.url
? html`
<a href="${rollCandidate.revision.url}" target="_blank">
${rollCandidate.revision.display}
</a>
`
: html` ${rollCandidate.revision.display} `}
</td>
<td>
${!!rollCandidate.revision.description
? truncate(rollCandidate.revision.description, 100)
: html``}
</td>
<td>
${!!rollCandidate.revision.time
? localeTime(new Date(rollCandidate.revision.time!))
: html``}
</td>
<td>
${rollCandidate.roll ? rollCandidate.roll.requester : html``}
</td>
<td>
${rollCandidate.roll
? localeTime(new Date(rollCandidate.roll.timestamp!))
: html``}
</td>
<td>
${rollCandidate.roll && rollCandidate.roll.url
? html`
<a href="${rollCandidate.roll.url}" , target="_blank">
${rollCandidate.roll.url}
</a>
`
: html``}
${!!rollCandidate.roll &&
!rollCandidate.roll.url &&
rollCandidate.roll.status
? rollCandidate.roll.status
: html``}
${!rollCandidate.roll
? html`
<button
@click="${() => {
ele.requestManualRoll(rollCandidate.revision.id);
}}"
class="requestRoll"
?disabled=${!ele.editRights}
title="${ele.editRights
? 'Request a roll to this revision.'
: ele.pleaseLoginMsg}"
>
Request Roll
</button>
`
: html``}
${!!rollCandidate.roll && !!rollCandidate.roll.result
? html`
<span
class="${ele.manualRollResultClass(
rollCandidate.roll
)}"
>
${rollCandidate.roll.result ==
ManualRoll_Result.UNKNOWN
? html``
: rollCandidate.roll.result}
</span>
`
: html``}
</td>
</tr>
`
)}
<tr class="rollCandidate">
<td>
<input id="manualRollRevInput" label="type revision/ref"></input>
</td>
<td><!-- no description --></td>
<td><!-- no revision timestamp --></td>
<td><!-- no requester --></td>
<td><!-- no request timestamp --></td>
<td>
<button
@click="${() => {
ele.requestManualRoll(
$$<HTMLInputElement>('#manualRollRevInput')!.value
);
}}"
class="requestRoll"
?disabled=${!ele.editRights}
title="${
ele.editRights
? 'Request a roll to this revision.'
: ele.pleaseLoginMsg
}">
Request Roll
</button>
</td>
</tr>
`
: html`
This roller does not support manual rolls. If you want this
feature, update the config file for the roller to enable it.
Note that some rollers cannot support manual rolls for technical
reasons.
`
}
</table>
</div>
</tabs-panel-sk>
<dialog id="modeChangeDialog" class=surface-themes-sk>
<h2>Enter a message:</h2>
<input type="text" id="modeChangeMsgInput"></input>
<button @click="${() => {
ele.changeMode(false);
}}">Cancel</button>
<button @click="${() => {
ele.changeMode(true);
}}">Submit</button>
</dialog>
<dialog id="strategyChangeDialog" class=surface-themes-sk>
<h2>Enter a message:</h2>
<input type="text" id="strategyChangeMsgInput"></input>
<button @click="${() => {
ele.changeStrategy(false);
}}">Cancel</button>
<button @click="${() => {
ele.changeStrategy(true);
}}">Submit</button>
</dialog>
`;
private editRights: boolean = false;
private lastLoaded: Date = new Date(0);
private modeChangePending: boolean = false;
private readonly pleaseLoginMsg = 'Please login to make changes.';
private refreshInterval = 60;
private rollCandidates: RollCandidate[] = [];
private rollWindowStart: Date = new Date(0);
private rpc: AutoRollService = GetAutoRollService(this);
private selectedMode: string = '';
private status: AutoRollStatus | null = null;
private strategyChangePending: boolean = false;
private timeout: number = 0;
constructor() {
super(ARBStatusSk.template);
}
connectedCallback() {
super.connectedCallback();
this._upgradeProperty('roller');
this._render();
LoginTo('/loginstatus/').then((loginstatus: any) => {
this.editRights = loginstatus.IsAGoogler;
this._render();
});
this.reload();
}
get roller() {
return this.getAttribute('roller') || '';
}
set roller(v: string) {
this.setAttribute('roller', v);
this.reload();
}
private modeButtonPressed(mode: string) {
if (mode === this.status?.mode?.mode) {
return;
}
this.selectedMode = mode;
$$<HTMLDialogElement>('#modeChangeDialog', this)!.showModal();
}
private changeMode(submit: boolean) {
$$<HTMLDialogElement>('#modeChangeDialog', this)!.close();
if (!submit) {
this.selectedMode = '';
return;
}
const modeChangeMsgInput = <HTMLInputElement>(
$$('#modeChangeMsgInput', this)
);
if (!modeChangeMsgInput) {
return;
}
this.modeChangePending = true;
this.rpc
.setMode({
message: modeChangeMsgInput.value,
mode: Mode[<keyof typeof Mode>this.selectedMode],
rollerId: this.roller,
})
.then(
(resp: SetModeResponse) => {
this.modeChangePending = false;
modeChangeMsgInput.value = '';
this.update(resp.status!);
},
() => {
this.modeChangePending = false;
this._render();
}
);
}
private changeStrategy(submit: boolean) {
$$<HTMLDialogElement>('#strategyChangeDialog', this)!.close();
const strategySelect = <HTMLSelectElement>$$('#strategySelect');
const strategyChangeMsgInput = <HTMLInputElement>(
$$('#strategyChangeMsgInput')
);
if (!submit) {
if (!!strategySelect && !!this.status?.strategy) {
strategySelect.value = this.status?.strategy.strategy;
}
return;
}
if (!strategyChangeMsgInput || !strategySelect) {
return;
}
this.strategyChangePending = true;
this.rpc
.setStrategy({
message: strategyChangeMsgInput.value,
rollerId: this.roller,
strategy: Strategy[<keyof typeof Strategy>strategySelect.value],
})
.then(
(resp: SetStrategyResponse) => {
this.strategyChangePending = false;
strategyChangeMsgInput.value = '';
this.update(resp.status!);
},
() => {
this.strategyChangePending = false;
if (this.status?.strategy?.strategy) {
strategySelect!.value = this.status.strategy.strategy;
}
this._render();
}
);
}
// computeRollWindowStart returns a string indicating when the configured
// roll window will start. If errors are encountered, in particular those
// relating to parsing the roll window, the returned string will contain
// the error.
private computeRollWindowStart(config: AutoRollConfig): Date {
if (!config || !config.timeWindow) {
return new Date();
}
// TODO(borenet): This duplicates code in the go/time_window package.
// parseDayTime returns a 2-element array containing the hour and
// minutes as ints. Throws an error (string) if the given string cannot
// be parsed as hours and minutes.
const parseDayTime = function (s: string) {
const timeSplit = s.split(':');
if (timeSplit.length !== 2) {
throw 'Expected time format "hh:mm", not ' + s;
}
const hours = parseInt(timeSplit[0]);
if (hours < 0 || hours >= 24) {
throw 'Hours must be between 0-23, not ' + timeSplit[0];
}
const minutes = parseInt(timeSplit[1]);
if (minutes < 0 || minutes >= 60) {
throw 'Minutes must be between 0-59, not ' + timeSplit[1];
}
return [hours, minutes];
};
// Parse multiple day/time windows, eg. M-W 00:00-04:00; Th-F 00:00-02:00
const windows = [];
const split = config.timeWindow.split(';');
for (let i = 0; i < split.length; i++) {
const dayTimeWindow = split[i].trim();
// Parse individual day/time window, eg. M-W 00:00-04:00
const windowSplit = dayTimeWindow.split(' ');
if (windowSplit.length !== 2) {
console.error('expected format "D hh:mm", not ' + dayTimeWindow);
return new Date();
}
const dayExpr = windowSplit[0].trim();
const timeExpr = windowSplit[1].trim();
// Parse the starting and ending times.
const timeExprSplit = timeExpr.split('-');
if (timeExprSplit.length !== 2) {
console.error('expected format "hh:mm-hh:mm", not ' + timeExpr);
return new Date();
}
let startTime;
try {
startTime = parseDayTime(timeExprSplit[0]);
} catch (e) {
return e;
}
let endTime;
try {
endTime = parseDayTime(timeExprSplit[1]);
} catch (e) {
return e;
}
// Parse the day(s).
const allDays = ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa'];
const days = [];
// "*" means every day.
if (dayExpr === '*') {
days.push(...allDays.map((_, i) => i));
} else {
const rangesSplit = dayExpr.split(',');
for (let i = 0; i < rangesSplit.length; i++) {
const rangeSplit = rangesSplit[i].split('-');
if (rangeSplit.length === 1) {
const day = allDays.indexOf(rangeSplit[0]);
if (day === -1) {
console.error('Unknown day ' + rangeSplit[0]);
return new Date();
}
days.push(day);
} else if (rangeSplit.length === 2) {
const startDay = allDays.indexOf(rangeSplit[0]);
if (startDay === -1) {
console.error('Unknown day ' + rangeSplit[0]);
return new Date();
}
let endDay = allDays.indexOf(rangeSplit[1]);
if (endDay === -1) {
console.error('Unknown day ' + rangeSplit[1]);
return new Date();
}
if (endDay < startDay) {
endDay += 7;
}
for (let day = startDay; day <= endDay; day++) {
days.push(day % 7);
}
} else {
console.error('Invalid day expression ' + rangesSplit[i]);
return new Date();
}
}
}
// Add the windows to the list.
for (let i = 0; i < days.length; i++) {
windows.push({
day: days[i],
start: startTime,
end: endTime,
});
}
}
// For each window, find the timestamp at which it opens next.
const now = new Date().getTime();
const openTimes = windows.map((w) => {
let next = new Date(now);
next.setUTCHours(w.start[0], w.start[1], 0, 0);
const dayOffsetMs = (w.day - next.getUTCDay()) * 24 * 60 * 60 * 1000;
next = new Date(next.getTime() + dayOffsetMs);
if (next.getTime() < now) {
// If we've missed this week's window, bump forward a week.
next = new Date(next.getTime() + 7 * 24 * 60 * 60 * 1000);
}
return next;
});
// Pick the next window.
openTimes.sort((a, b) => a.getTime() - b.getTime());
const rollWindowStart = openTimes[0].toString();
return openTimes[0];
}
private issueURL(roll: AutoRollCL): string {
if (roll) {
return (this.status?.issueUrlBase || '') + roll.id;
}
return '';
}
private getModeButtonLabel(currentMode: Mode, mode: string) {
switch (currentMode) {
case Mode.RUNNING:
switch (mode) {
case Mode.STOPPED:
return 'stop';
case Mode.DRY_RUN:
return 'switch to dry run';
}
case Mode.STOPPED:
switch (mode) {
case Mode.RUNNING:
return 'resume';
case Mode.DRY_RUN:
return 'switch to dry run';
}
case Mode.DRY_RUN:
switch (mode) {
case Mode.RUNNING:
return 'switch to normal mode';
case Mode.STOPPED:
return 'stop';
}
}
}
private reloadChanged() {
const refreshIntervalInput = <HTMLInputElement>(
$$('refreshIntervalInput', this)
);
if (refreshIntervalInput) {
this.refreshInterval = refreshIntervalInput.valueAsNumber;
this.resetTimeout();
}
}
private resetTimeout() {
if (this.timeout) {
window.clearTimeout(this.timeout);
}
if (this.refreshInterval > 0) {
this.timeout = window.setTimeout(() => {
this.reload();
}, this.refreshInterval * 1000);
}
}
private reload() {
if (!this.roller) {
return;
}
console.log('Loading status for ' + this.roller + '...');
this.rpc
.getStatus({
rollerId: this.roller,
})
.then((resp: GetStatusResponse) => {
this.update(resp.status!);
this.resetTimeout();
})
.catch((err: any) => {
this.resetTimeout();
});
}
private manualRollResultClass(req: ManualRoll) {
if (!req) {
return '';
}
switch (req.result) {
case ManualRoll_Result.SUCCESS:
return 'fg-success';
case ManualRoll_Result.FAILURE:
return 'fg-failure';
default:
return '';
}
}
private requestManualRoll(rev: string) {
this.rpc
.createManualRoll({
revision: rev,
rollerId: this.roller,
})
.then((resp: CreateManualRollResponse) => {
const exist = this.rollCandidates.find(
(r) => r.revision.id === resp.roll!.revision
);
if (!!exist) {
exist.roll = resp.roll!;
} else {
this.rollCandidates.push({
revision: {
description: '',
display: resp.roll!.revision,
id: resp.roll!.revision,
time: '',
url: '',
},
roll: resp.roll!,
});
}
const manualRollRevInput = <HTMLInputElement>$$('#manualRollRevInput');
if (!!manualRollRevInput) {
manualRollRevInput.value = '';
}
this._render();
});
}
private rollClass(roll: AutoRollCL) {
if (!roll) {
return 'unknown';
}
switch (roll.result) {
case AutoRollCL_Result.SUCCESS:
return 'fg-success';
case AutoRollCL_Result.FAILURE:
return 'fg-failure';
case AutoRollCL_Result.IN_PROGRESS:
return 'fg-unknown';
case AutoRollCL_Result.DRY_RUN_SUCCESS:
return 'fg-success';
case AutoRollCL_Result.DRY_RUN_FAILURE:
return 'fg-failure';
case AutoRollCL_Result.DRY_RUN_IN_PROGRESS:
return 'fg-unknown';
default:
return 'fg-unknown';
}
}
private rollResult(roll: AutoRollCL) {
if (!roll) {
return 'unknown';
}
return roll.result.toLowerCase().replace('_', ' ');
}
private statusClass(status: string) {
// TODO(borenet): Status could probably be an enum.
const statusClassMap: { [key: string]: string } = {
idle: 'fg-unknown',
active: 'fg-unknown',
success: 'fg-success',
failure: 'fg-failure',
throttled: 'fg-failure',
'dry run idle': 'fg-unknown',
'dry run active': 'fg-unknown',
'dry run success': 'fg-success',
'dry run success; leaving open': 'fg-success',
'dry run failure': 'fg-failure',
'dry run throttled': 'fg-failure',
stopped: 'fg-failure',
};
return statusClassMap[status] || '';
}
private selectedStrategyChanged() {
if (
$$<HTMLSelectElement>('#strategySelect', this)!.value ===
this.status?.strategy?.strategy
) {
return;
}
$$<HTMLDialogElement>('#strategyChangeDialog', this)!.showModal();
}
private trybotClass(tryjob: TryJob) {
switch (tryjob.result) {
case TryJob_Result.SUCCESS:
return 'fg-success';
case TryJob_Result.FAILURE:
return 'fg-failure';
case TryJob_Result.CANCELED:
return 'fg-failure';
default:
return 'fg-unknown';
}
}
private unthrottle() {
this.rpc.unthrottle({
rollerId: this.roller,
});
}
private update(status: AutoRollStatus) {
const rollCandidates: RollCandidate[] = [];
const manualByRev: { [key: string]: ManualRoll } = {};
if (status.notRolledRevisions) {
if (status.manualRolls) {
for (let i = 0; i < status.manualRolls.length; i++) {
const req = status.manualRolls[i];
manualByRev[req.revision] = req;
}
}
for (let i = 0; i < status.notRolledRevisions.length; i++) {
const rev = status.notRolledRevisions[i];
const candidate: RollCandidate = {
revision: rev,
roll: null,
};
let req = manualByRev[rev.id];
delete manualByRev[rev.id];
if (
!req &&
status.currentRoll &&
status.currentRoll.rollingTo === rev.id
) {
req = {
dryRun: false,
id: '',
noEmail: false,
noResolveRevision: false,
requester: 'autoroller',
result: ManualRoll_Result.UNKNOWN,
rollerId: this.roller,
revision: '',
status: ManualRoll_Status.PENDING,
timestamp: status.currentRoll.created,
url: this.issueURL(status.currentRoll),
};
}
candidate.roll = req;
rollCandidates.push(candidate);
}
}
for (const key in manualByRev) {
const req = manualByRev[key];
const rev: Revision = {
description: '',
display: req.revision,
id: req.revision,
time: '',
url: '',
};
rollCandidates.push({
revision: rev,
roll: req,
});
}
this.lastLoaded = new Date();
this.rollCandidates = rollCandidates;
if (status.config) {
this.rollWindowStart = this.computeRollWindowStart(status.config);
}
this.status = status;
console.log('Loaded status.');
this._render();
}
}
define('arb-status-sk', ARBStatusSk);