| /** |
| * @module module/cluster-lastn-page-sk |
| * @description <h2><code>cluster-lastn-page-sk</code></h2> |
| * |
| * Allows trying out an alert by clustering over a range of commits. |
| */ |
| import dialogPolyfill from 'dialog-polyfill'; |
| import { define } from 'elements-sk/define'; |
| import { fromObject } from 'common-sk/modules/query'; |
| import { html } from 'lit-html'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| import { stateReflector } from 'common-sk/modules/stateReflector'; |
| import { HintableObject } from 'common-sk/modules/hintable'; |
| import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk'; |
| import { errorMessage } from '../errorMessage'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| |
| import 'elements-sk/spinner-sk'; |
| import 'elements-sk/styles/buttons'; |
| |
| import '../cluster-summary2-sk'; |
| import '../commit-detail-sk'; |
| import '../triage-status-sk'; |
| import '../alert-config-sk'; |
| import '../domain-picker-sk'; |
| import { |
| ParamSet, |
| Alert, |
| FrameResponse, |
| RegressionAtCommit, |
| Direction, |
| ClusterSummary, |
| RegressionDetectionRequest, |
| AlertUpdateResponse, |
| progress, |
| } from '../json'; |
| import { DomainPickerState } from '../domain-picker-sk/domain-picker-sk'; |
| import { AlertConfigSk } from '../alert-config-sk/alert-config-sk'; |
| import { TriageStatusSkStartTriageEventDetails } from '../triage-status-sk/triage-status-sk'; |
| import { ClusterSummary2SkOpenKeysEventDetail } from '../cluster-summary2-sk/cluster-summary2-sk'; |
| import { DomainPickerSk } from '../domain-picker-sk/domain-picker-sk'; |
| import { messagesToErrorString, startRequest } from '../progress/progress'; |
| |
| export class ClusterLastNPageSk extends ElementSk { |
| // The range of commits over which we are clustering. |
| private domain: DomainPickerState = { |
| begin: 0, |
| end: Math.floor(Date.now() / 1000), |
| num_commits: 200, |
| request_type: 1, |
| }; |
| |
| // True if the Alert is being saved to the database. |
| private writingAlert: boolean = false; |
| |
| // The paramsets for the alert config. |
| private paramset: ParamSet = {}; |
| |
| // The id of the currently running request. |
| private requestId: string = ''; |
| |
| // The text status of the currently running request. |
| private runningStatus = ''; |
| |
| // The fetch in connectedCallback will fill this is with the defaults. |
| private state: Alert | null = null; |
| |
| // The regressions detected from the dryrun. |
| private regressions: (RegressionAtCommit | null)[] = []; |
| |
| private alertDialog: HTMLDialogElement | null = null; |
| |
| private triageDialog: HTMLDialogElement | null = null; |
| |
| private alertConfig: AlertConfigSk | null = null; |
| |
| private runSpinner: SpinnerSk | null = null; |
| |
| // The state of the cluster-summary2-sk dialog. |
| private dialogState: Partial<TriageStatusSkStartTriageEventDetails> | null = { |
| full_summary: null, |
| triage: undefined, |
| }; |
| |
| /** Is true if the previous Run has returned an error. */ |
| private hasError: boolean = false; |
| |
| constructor() { |
| super(ClusterLastNPageSk.template); |
| if (window.sk.perf.demo) { |
| this.domain.end = Math.floor(new Date(2020, 4, 1).valueOf() / 1000); |
| } |
| } |
| |
| private static template = (ele: ClusterLastNPageSk) => html` |
| <dialog id="alert-config-dialog"> |
| <alert-config-sk |
| .config=${ele.state} |
| .paramset=${ele.paramset} |
| .key_order=${window.sk.perf.key_order} |
| ></alert-config-sk> |
| <div class="buttons"> |
| <button @click=${ele.alertClose}>Cancel</button> |
| <button @click=${ele.alertAccept}>Accept</button> |
| </div> |
| </dialog> |
| <div class="controls"> |
| <p> |
| Use this page to test out an Alert configuration. Configure the Alert by |
| pressing the button below. |
| </p> |
| <button @click=${ele.alertEdit}> |
| ${ClusterLastNPageSk.configTitle(ele)} |
| </button> |
| <p> |
| You can optionally change the range of commits over which the Alert is run: |
| </p> |
| <details> |
| <summary> |
| Range |
| </summary> |
| <domain-picker-sk |
| id="range" |
| .state=${ele.domain} |
| force_request_type="dense" |
| ></domain-picker-sk> |
| </details> |
| <p> |
| Once configured, you can run the Alert and see the regressions it |
| detects. |
| </p> |
| <div class="running"> |
| <button |
| class="action" |
| ?disabled=${!ele.state!.query || !!ele.requestId} |
| @click=${ele.run} |
| > |
| Run |
| </button> |
| <spinner-sk id=run-spinner></spinner-sk> |
| <pre class="messages ${ClusterLastNPageSk.classIfError(ele.hasError)}">${ele.runningStatus}</pre> |
| </div> |
| <div class="saving"> |
| <p> |
| Once satisfied with the Alert you can save it to be run periodically. |
| </p> |
| <button |
| @click=${ele.writeAlert} |
| class="action" |
| ?disabled=${!ele.state!.query} |
| > |
| ${ClusterLastNPageSk.writeAlertTitle(ele)} |
| </button> |
| <spinner-sk ?active=${ele.writingAlert}></spinner-sk> |
| </div> |
| </div> |
| <hr /> |
| |
| <dialog id="triage-cluster-dialog" @open-keys=${ele.openKeys}> |
| <cluster-summary2-sk |
| .full_summary=${ele.dialogState!.full_summary} |
| .triage=${ele.dialogState!.triage} |
| .alert=${ele.state} |
| notriage |
| ></cluster-summary2-sk> |
| <div class="buttons"> |
| <button @click=${ele.triageClose}>Close</button> |
| </div> |
| </dialog> |
| |
| ${ClusterLastNPageSk.table(ele)} |
| `; |
| |
| /** The classname to add to an element if an error has occurred. */ |
| private static classIfError(hasError: boolean): string { |
| return hasError ? 'error' : ''; |
| } |
| |
| private static stepUpAt(dir: Direction) { |
| return dir === 'UP' || dir === 'BOTH'; |
| } |
| |
| private static stepDownAt(dir: Direction) { |
| return dir === 'DOWN' || dir === 'BOTH'; |
| } |
| |
| private static notBoth(dir: Direction) { |
| return dir !== 'BOTH'; |
| } |
| |
| private static tableHeader = (ele: ClusterLastNPageSk) => { |
| const ret = [html` <th></th> `]; |
| if (ClusterLastNPageSk.stepDownAt(ele.state!.direction)) { |
| ret.push(html` <th>Low</th> `); |
| } |
| if (ClusterLastNPageSk.stepUpAt(ele.state!.direction)) { |
| ret.push(html` <th>High</th> `); |
| } |
| if (ClusterLastNPageSk.notBoth(ele.state!.direction)) { |
| ret.push(html` <th></th> `); |
| } |
| return ret; |
| }; |
| |
| private static fullSummary(frame: FrameResponse, summary: ClusterSummary) { |
| return { |
| frame, |
| summary, |
| }; |
| } |
| |
| private static low = (ele: ClusterLastNPageSk, reg: RegressionAtCommit) => { |
| if (!ClusterLastNPageSk.stepDownAt(ele.state!.direction)) { |
| return html``; |
| } |
| if (reg.regression!.low) { |
| return html` |
| <td class="cluster"> |
| <triage-status-sk |
| .alert=${ele.state} |
| cluster_type="low" |
| .full_summary=${ClusterLastNPageSk.fullSummary( |
| reg.regression!.frame!, |
| reg.regression!.low, |
| )} |
| .triage=${reg.regression!.low_status} |
| ></triage-status-sk> |
| </td> |
| `; |
| } |
| return html` <td class="cluster"></td> `; |
| }; |
| |
| private static high = (ele: ClusterLastNPageSk, reg: RegressionAtCommit) => { |
| if (!ClusterLastNPageSk.stepUpAt(ele.state!.direction)) { |
| return html``; |
| } |
| if (reg.regression!.high) { |
| return html` |
| <td class="cluster"> |
| <triage-status-sk |
| .alert=${ele.state} |
| cluster_type="high" |
| .full_summary=${ClusterLastNPageSk.fullSummary( |
| reg.regression!.frame!, |
| reg.regression!.high, |
| )} |
| .triage=${reg.regression!.high_status} |
| ></triage-status-sk> |
| </td> |
| `; |
| } |
| return html` <td class="cluster"></td> `; |
| }; |
| |
| private static filler = (ele: ClusterLastNPageSk) => { |
| if (ClusterLastNPageSk.notBoth(ele.state!.direction)) { |
| return html` <td></td> `; |
| } |
| return html``; |
| }; |
| |
| private static tableRows = (ele: ClusterLastNPageSk) => ele.regressions.map( |
| (reg) => html` |
| <tr> |
| <td class="fixed"> |
| <commit-detail-sk .cid=${reg!.cid}></commit-detail-sk> |
| </td> |
| |
| ${ClusterLastNPageSk.low(ele, reg!)} |
| ${ClusterLastNPageSk.high(ele, reg!)} |
| ${ClusterLastNPageSk.filler(ele)} |
| </tr> |
| `, |
| ); |
| |
| private static configTitle = (ele: ClusterLastNPageSk) => { |
| // Original style regression detection is indicated by the empty string for |
| // backwards compatibility, so calculate a display value in that case. |
| let detection: string = ele.state!.step; |
| if (ele.state!.step === '') { |
| detection = 'original'; |
| } |
| return html` |
| Algo: ${detection}/${ele.state!.algo} - Radius: ${ele.state!.radius} - Sparse: |
| ${ele.state!.sparse} - Threshold: ${ele.state!.interesting} |
| `; |
| }; |
| |
| private static writeAlertTitle = (ele: ClusterLastNPageSk) => { |
| if (ele.state?.id_as_string === '-1') { |
| return 'Create Alert'; |
| } |
| return 'Update Alert'; |
| }; |
| |
| private static table = (ele: ClusterLastNPageSk) => { |
| if (ele.requestId && !ele.regressions.length) { |
| return html` No regressions found yet. `; |
| } |
| return html` |
| <table @start-triage=${ele.triageStart}> |
| <tr> |
| <th>Commit</th> |
| <th colspan="2">Regressions</th> |
| </tr> |
| <tr> ${ClusterLastNPageSk.tableHeader(ele)} </tr> |
| ${ClusterLastNPageSk.tableRows(ele)} |
| </table> |
| `; |
| }; |
| |
| connectedCallback(): void { |
| super.connectedCallback(); |
| |
| const init = fetch('/_/initpage/') |
| .then(jsonOrThrow) |
| .then((json: FrameResponse) => { |
| this.paramset = json.dataframe!.paramset; |
| }); |
| |
| const alertNew = fetch('/_/alert/new') |
| .then(jsonOrThrow) |
| .then((json: Alert) => { |
| this.state = json; |
| }); |
| |
| Promise.all([init, alertNew]) |
| .then(() => { |
| this._render(); |
| this.alertDialog = this.querySelector('#alert-config-dialog'); |
| this.triageDialog = this.querySelector('#triage-cluster-dialog'); |
| dialogPolyfill.registerDialog(this.alertDialog!); |
| dialogPolyfill.registerDialog(this.triageDialog!); |
| this.alertConfig = this.querySelector('alert-config-sk'); |
| this.runSpinner = this.querySelector('#run-spinner'); |
| this.stateHasChanged = stateReflector( |
| () => (this.state as unknown) as HintableObject, |
| (state) => { |
| this.state = (state as unknown) as Alert; |
| this._render(); |
| }, |
| ); |
| }) |
| .catch(errorMessage); |
| } |
| |
| // Call this anytime something in private state is changed. Will be replaced |
| // with the real function once stateReflector has been setup. |
| // eslint-disable-next-line @typescript-eslint/no-empty-function |
| private stateHasChanged = () => {}; |
| |
| private alertEdit() { |
| this.alertDialog!.showModal(); |
| } |
| |
| private writeAlert() { |
| this.writingAlert = true; |
| this._render(); |
| // Post the config. |
| fetch('/_/alert/update', { |
| method: 'POST', |
| body: JSON.stringify(this.state), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(jsonOrThrow) |
| .then((json: AlertUpdateResponse) => { |
| this.state!.id_as_string = json.IDAsString; |
| this.writingAlert = false; |
| this._render(); |
| }) |
| .catch((msg) => { |
| this.writingAlert = false; |
| this._render(); |
| errorMessage(msg); |
| }); |
| } |
| |
| private alertClose() { |
| this.alertDialog!.close(); |
| } |
| |
| private alertAccept() { |
| this.alertDialog!.close(); |
| this.state = this.alertConfig!.config; |
| this.stateHasChanged(); |
| this._render(); |
| } |
| |
| private triageStart(e: CustomEvent<TriageStatusSkStartTriageEventDetails>) { |
| this.dialogState = e.detail; |
| this._render(); |
| this.triageDialog!.show(); |
| } |
| |
| private triageClose() { |
| this.triageDialog!.close(); |
| } |
| |
| private openKeys(e: CustomEvent<ClusterSummary2SkOpenKeysEventDetail>) { |
| const query = { |
| keys: e.detail.shortcut, |
| begin: e.detail.begin, |
| end: e.detail.end, |
| xbaroffset: e.detail.xbar.offset, |
| }; |
| window.open(`/e/?${fromObject(query)}`, '_blank'); |
| } |
| |
| private catch(msg: string) { |
| this.hasError = true; |
| this.requestId = ''; |
| this.runningStatus = msg; |
| this._render(); |
| if (msg) { |
| errorMessage(msg); |
| } |
| } |
| |
| private async run() { |
| this.hasError = false; |
| if (this.requestId) { |
| errorMessage('There is a pending query already running.'); |
| return; |
| } |
| this.domain = this.querySelector<DomainPickerSk>('#range')!.state; |
| const body: RegressionDetectionRequest = { |
| domain: { |
| n: this.domain.num_commits, |
| offset: 0, |
| end: new Date(this.domain.end * 1000).toISOString(), |
| }, |
| alert: this.state!, |
| step: 0, |
| total_queries: 1, |
| }; |
| |
| try { |
| this.requestId = 'running'; |
| const finalProg = await startRequest('/_/dryrun/start', body, 300, this.runSpinner!, (prog: progress.SerializedProgress) => { |
| if (prog.results) { |
| this.regressions = prog.results; |
| } |
| this.runningStatus = prog.messages.map(((msg) => `${msg.key}: ${msg.value}`)).join('\n'); |
| this._render(); |
| }); |
| if (finalProg.status !== 'Finished') { |
| throw (new Error(messagesToErrorString(finalProg.messages))); |
| } |
| this.regressions = finalProg.results; |
| this.runningStatus = ''; |
| } catch (error) { |
| this.catch(error); |
| } finally { |
| this.requestId = ''; |
| this._render(); |
| } |
| } |
| } |
| |
| define('cluster-lastn-page-sk', ClusterLastNPageSk); |