| /** |
| * @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, render } from 'lit-html' |
| |
| import { $$ } from 'common-sk/modules/dom'; |
| import { diffDate, localeTime } from 'common-sk/modules/human'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| |
| 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'; |
| |
| /** Truncate the given string to the given length. If the string was |
| * shortened, change the last three characters to ellipsis. |
| * |
| * TODO(borenet): Move this somewhere common so that it can be shared. |
| */ |
| export function truncate(str: string, len: number) { |
| if (str.length > len) { |
| const ellipsis = "..." |
| return str.substring(0, len - ellipsis.length) + ellipsis; |
| } |
| return str |
| } |
| |
| export class Config { |
| parentWaterfall: string = ""; |
| supportsManualRolls: boolean = false; |
| timeWindow: string = ""; |
| } |
| |
| export class ManualRollRequest { |
| dry_run: boolean = false; |
| emails: string[] | null = null; |
| id: string = ""; |
| no_resolve_revision: boolean = false; |
| requester: string = ""; |
| result: string = ""; |
| resultDetails: string = ""; |
| revision: string = ""; |
| rollerName: string = ""; |
| status: string = ""; |
| timestamp: string = ""; |
| url: string = ""; |
| } |
| |
| export class Mode { |
| message: string = ""; |
| mode: string = ""; |
| time: string = ""; |
| user: string = ""; |
| } |
| |
| export class TryResult { |
| builder: string = ""; |
| category: string = ""; |
| created_ts: string = ""; |
| result: string = ""; |
| status: string = ""; |
| url: string = ""; |
| } |
| |
| export class Roll { |
| closed: boolean = false; |
| commitQueue: boolean = false; |
| committed: boolean = false; |
| cqDryRun: boolean = false; |
| created: string = ""; |
| issue: number = 0; |
| modified: string = ""; |
| subject: string = ""; |
| result: string = ""; |
| rollingFrom: string = ""; |
| rollingTo: string = ""; |
| tryResults: TryResult[] | null = null; |
| } |
| |
| export class Revision { |
| author: string = ""; |
| bugs: { [key:string]: string[] } | null = null; |
| dependencies: { [key:string]: string } | null = null; |
| id: string = ""; |
| description: string = ""; |
| details: string = ""; |
| display: string = ""; |
| invalidReason: string = ""; |
| tests: string[] | null = null; |
| time: string = ""; |
| url: string = ""; |
| } |
| |
| export class RollCandidate{ |
| revision: Revision = new Revision(); |
| roll: ManualRollRequest = new ManualRollRequest(); |
| } |
| |
| export class Strategy { |
| message: string = ""; |
| strategy: string = ""; |
| time: string = ""; |
| user: string = ""; |
| } |
| |
| export class Status { |
| config: Config = new Config(); |
| currentRoll: Roll | null = null; |
| currentRollRev: string = ""; |
| error: string = ""; |
| fullHistoryUrl: string = ""; |
| issueUrlBase: string = ""; |
| lastRoll: Roll | null = null; |
| lastRollRev: string = ""; |
| manualRequests: ManualRollRequest[] | null = null; |
| mode: Mode = new Mode(); |
| notRolledRevs: Revision[] | null = null; |
| numBehind: number = 0; |
| numFailed: number = 0; |
| recent: Roll[] | null = null; |
| status: string = ""; |
| strategy: Strategy = new Strategy(); |
| throttledUntil: number = 0; |
| validModes: string[] = []; |
| validStrategies: string[] = []; |
| } |
| |
| class ARBStatusSk extends ElementSk { |
| private static template = (ele: ARBStatusSk) => 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}</span> |
| </td> |
| </tr> |
| <tr> |
| <td class="nowrap">Set By:</td> |
| <td class="nowrap unknown"> |
| ${ele.status.mode.user}${ele.status.mode.message |
| ? html`: ${ele.status.mode.message}`: html``} |
| </td> |
| </tr> |
| <tr> |
| <td class="nowrap">Change Mode:</td> |
| <td class="nowrap"> |
| ${ele.status.validModes.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.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 * 1000))}</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.tryResults ? |
| ele.status.currentRoll.tryResults.map((tryResult) => html` |
| <div class="trybot"> |
| ${tryResult.url ? html` |
| <a href="${tryResult.url}" |
| class="${ele.trybotClass(tryResult)}" |
| target="_blank"> |
| ${tryResult.builder} |
| </a> |
| ` : html` |
| <span class="nowrap" |
| class="${ele.trybotClass(tryResult)}"> |
| ${tryResult.builder} |
| </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.recent?.map((roll: Roll) => 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}"> |
| ${ele.status.validStrategies.map((strategy: string) => html` |
| <option |
| value="${strategy}" |
| ?selected="${strategy == ele.status.strategy.strategy}"> |
| ${strategy} |
| </option> |
| `)} |
| </select> |
| </td> |
| </tr> |
| <tr> |
| <td class="nowrap">Set By:</td> |
| <td class="nowrap unknown"> |
| ${ele.status.strategy.user}${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>${localeTime(new Date(rollCandidate.revision.time))}</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.reqResultClass(rollCandidate.roll)}"> |
| ${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 selectedMode: string = ""; |
| private status: Status = new Status(); |
| private strategyChangePending: boolean = false; |
| private timeout: number = 0; |
| |
| constructor() { |
| super(ARBStatusSk.template); |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| this._render(); |
| LoginTo("/loginstatus/").then((loginstatus: any) => { |
| this.editRights = loginstatus.IsAGoogler; |
| this._render(); |
| }); |
| this.reload(); |
| } |
| |
| private modeButtonPressed(mode: string) { |
| if (mode == this.status.mode.mode) { |
| return; |
| } |
| this.selectedMode = mode; |
| $$<HTMLDialogElement>("#modeChangeDialog", this)!.showModal(); |
| } |
| |
| private fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<any> { |
| this.dispatchEvent(new CustomEvent('begin-task', { bubbles: true })); |
| return fetch(input, init).then(jsonOrThrow) |
| .then((v: any) => { |
| this.dispatchEvent(new CustomEvent('end-task', { bubbles: true })); |
| return v; |
| }, (err: any) => { |
| this.dispatchEvent(new CustomEvent('fetch-error', { |
| detail: { |
| error: err, |
| loading: input, |
| }, |
| bubbles: true, |
| })); |
| return err; |
| }) |
| .catch((err: any) => { |
| this.dispatchEvent(new CustomEvent('fetch-error', { |
| detail: { |
| error: err, |
| loading: input, |
| }, |
| bubbles: true, |
| })); |
| throw err; |
| }) |
| } |
| |
| private changeMode(submit: boolean) { |
| $$<HTMLDialogElement>("#modeChangeDialog", this)!.close(); |
| if (!submit) { |
| this.selectedMode = ""; |
| return; |
| } |
| const modeChangeMsgInput = <HTMLInputElement>$$("#modeChangeMsgInput", this); |
| if (!modeChangeMsgInput) { |
| return; |
| } |
| this.modeChangePending = true; |
| const mode = new Mode(); |
| mode.message = modeChangeMsgInput.value; |
| mode.mode = this.selectedMode; |
| const url = window.location.pathname + "/json/mode"; |
| this.fetch(url, { |
| method: "POST", |
| body: JSON.stringify(mode), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }).then((json) => { |
| this.modeChangePending = false; |
| modeChangeMsgInput.value = ""; |
| this.update(json); |
| }, (err) => { |
| 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) { |
| strategySelect.value = this.status.strategy.strategy; |
| } |
| return; |
| } |
| if (!strategyChangeMsgInput || !strategySelect) { |
| return; |
| } |
| this.strategyChangePending = true; |
| const strategy = new Strategy(); |
| strategy.message = strategyChangeMsgInput.value; |
| strategy.strategy = strategySelect.value; |
| const url = window.location.pathname + "/json/strategy"; |
| this.fetch(url, { |
| method: "POST", |
| body: JSON.stringify(strategy), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }).then((json) => { |
| this.strategyChangePending = false; |
| strategyChangeMsgInput.value = ""; |
| this.update(json); |
| }, (err) => { |
| this.strategyChangePending = false; |
| 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: Config) { |
| if (!config || !config.timeWindow) { |
| return ""; |
| } |
| // 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) { |
| return "unknown; expected format \"D hh:mm\", not " + dayTimeWindow; |
| } |
| const dayExpr = windowSplit[0].trim(); |
| const timeExpr = windowSplit[1].trim(); |
| |
| // Parse the starting and ending times. |
| const timeExprSplit = timeExpr.split("-"); |
| if (timeExprSplit.length !== 2) { |
| return "unknown; expected format \"hh:mm-hh:mm\", not " + timeExpr; |
| } |
| 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) { |
| return "Unknown day " + rangeSplit[0]; |
| } |
| days.push(day); |
| } else if (rangeSplit.length === 2) { |
| const startDay = allDays.indexOf(rangeSplit[0]); |
| if (startDay === -1) { |
| return "Unknown day " + rangeSplit[0]; |
| } |
| let endDay = allDays.indexOf(rangeSplit[1]); |
| if (endDay === -1) { |
| return "Unknown day " + rangeSplit[1]; |
| } |
| if (endDay < startDay) { |
| endDay += 7; |
| } |
| for (let day = startDay; day <= endDay; day++) { |
| days.push(day % 7); |
| } |
| } else { |
| return "Invalid day expression " + rangesSplit[i]; |
| } |
| } |
| } |
| |
| // 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()); |
| return openTimes[0].toLocaleString(); |
| } |
| |
| private issueURL(roll: Roll): string { |
| if (roll) { |
| return this.status.issueUrlBase + roll.issue; |
| } |
| return ""; |
| } |
| |
| private getModeButtonLabel(currentMode: string, mode: string) { |
| // TODO(borenet): This is a hack; it doesn't respect this.validModes. |
| const modeButtonLabels: {[key:string]: {[key:string]: string}} = { |
| "running": { |
| "stopped": "stop", |
| "dry run": "switch to dry run", |
| }, |
| "stopped": { |
| "running": "resume", |
| "dry run": "switch to dry run", |
| }, |
| "dry run": { |
| "running": "switch to normal mode", |
| "stopped": "stop", |
| }, |
| }; |
| return modeButtonLabels[currentMode][mode]; |
| } |
| |
| 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() { |
| const url = window.location.pathname + "/json/status"; |
| console.log("Loading status from " + url); |
| this.fetch(url).then((json) => { |
| this.update(json); |
| this.resetTimeout(); |
| }).catch((err) => { |
| this.resetTimeout(); |
| }); |
| } |
| |
| private reqResultClass(req: ManualRollRequest) { |
| if (!req) { |
| return ""; |
| } |
| const manualRequestResultClass: {[key:string]:string} = { |
| "SUCCESS": "fg-success", |
| "FAILURE": "fg-failure", |
| } |
| return manualRequestResultClass[req.result]; |
| } |
| |
| private requestManualRoll(rev: string) { |
| const url = window.location.pathname + "/json/manual"; |
| const req: ManualRollRequest = { |
| dry_run: false, |
| emails: null, |
| id: "", |
| no_resolve_revision: false, |
| result: "", |
| resultDetails: "", |
| revision: rev, |
| requester: "", |
| rollerName: "", |
| status: "", |
| timestamp: "", |
| url: "", |
| }; |
| this.fetch(url, { |
| method: "POST", |
| body: JSON.stringify(req), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }).then((json) => { |
| const req = <ManualRollRequest>json; |
| const exist = this.rollCandidates.find((r) => r.revision.id == req.revision); |
| if (!!exist) { |
| exist.roll = req; |
| } else { |
| this.rollCandidates.push({ |
| revision: { |
| author: "", |
| bugs: null, |
| dependencies: null, |
| description: "", |
| details: "", |
| display: req.revision, |
| id: req.revision, |
| invalidReason: "", |
| tests: null, |
| time: "", |
| url: "", |
| }, |
| roll: req, |
| }); |
| } |
| const manualRollRevInput = <HTMLInputElement>$$("#manualRollRevInput"); |
| if (!!manualRollRevInput) { |
| manualRollRevInput.value = ""; |
| } |
| this._render(); |
| }); |
| } |
| |
| private rollClass(roll: Roll) { |
| if (!roll) { |
| return "unknown"; |
| } |
| const rollClassMap: {[key:string]: string} = { |
| "succeeded": "fg-success", |
| "failed": "fg-failure", |
| "in progress": "fg-unknown", |
| "dry run succeeded": "fg-success", |
| "dry run failed": "fg-failure", |
| "dry run in progress": "fg-unknown", |
| }; |
| return rollClassMap[roll.result] || "fg-unknown"; |
| } |
| |
| private rollResult(roll: Roll) { |
| if (!roll) { |
| return "unknown"; |
| } |
| return roll.result; |
| } |
| |
| private statusClass(status: string) { |
| 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(trybot: TryResult) { |
| if (trybot.status == "STARTED") { |
| return "fg-unknown"; |
| } else if (trybot.status == "COMPLETED") { |
| const trybotClass: {[key:string]:string} = { |
| "CANCELED": "fg-failure", |
| "FAILURE": "fg-failure", |
| "SUCCESS": "fg-success", |
| } |
| return trybotClass[trybot.result] || ""; |
| } else { |
| return "fg-unknown"; |
| } |
| } |
| |
| private unthrottle() { |
| const url = window.location.pathname + "/json/unthrottle"; |
| this.fetch(url, {method: "POST"}); |
| } |
| |
| private update(status: Status) { |
| const rollCandidates: RollCandidate[] = []; |
| const manualByRev: {[key:string]:ManualRollRequest} = {}; |
| if (status.notRolledRevs) { |
| if (status.manualRequests) { |
| for (let i = 0; i < status.manualRequests.length; i++) { |
| const req = status.manualRequests[i]; |
| manualByRev[req.revision] = req; |
| } |
| } |
| for (let i = 0; i < status.notRolledRevs.length; i++) { |
| const rev = status.notRolledRevs[i]; |
| const candidate = new RollCandidate(); |
| candidate.revision = rev; |
| let req = manualByRev[rev.id]; |
| delete manualByRev[rev.id]; |
| if (!req && status.currentRoll && status.currentRoll.rollingTo == rev.id) { |
| req = new ManualRollRequest(); |
| req.requester = "autoroller"; |
| req.result = ""; |
| req.revision = ""; |
| req.status = "STARTED", |
| req.timestamp = status.currentRoll.created; |
| req.url = this.issueURL(status.currentRoll); |
| } |
| candidate.roll = req; |
| rollCandidates.push(candidate); |
| } |
| } |
| for (const key in manualByRev) { |
| const req = manualByRev[key]; |
| const rev = new Revision(); |
| rev.id = req.revision; |
| rev.display = req.revision; |
| rollCandidates.push({ |
| revision: rev, |
| roll: req, |
| }); |
| }; |
| this.lastLoaded = new Date(); |
| this.rollCandidates = rollCandidates; |
| this.rollWindowStart = this.computeRollWindowStart(status.config); |
| this.status = status; |
| console.log("Reloaded status."); |
| this._render(); |
| } |
| } |
| |
| define('arb-status-sk', ARBStatusSk); |