blob: 6a743ffeb77c926726e04c79cd851762f49bb219 [file] [log] [blame]
/**
* @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 { errorMessage } from 'elements-sk/errorMessage';
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 { 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,
DryRunStatus,
Direction,
ClusterSummary,
RegressionDetectionRequest,
StartDryRunResponse,
AlertUpdateResponse,
} 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';
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}
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.runSpinner!.active = false;
this._render();
if (msg) {
errorMessage(msg, 10000);
}
}
private run() {
this.hasError = false;
if (this.requestId) {
errorMessage('There is a pending query already running.');
return;
}
this.runSpinner!.active = true;
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!,
query: this.state!.query,
step: 0,
total_queries: 1,
};
fetch('/_/dryrun/start', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
})
.then(jsonOrThrow)
.then((json: StartDryRunResponse) => {
this.requestId = json.id;
this._render();
this.checkDryRunStatus((regressions: (RegressionAtCommit | null)[]) => {
this.regressions = regressions;
this._render();
});
})
.catch((msg) => this.catch(msg));
}
private checkDryRunStatus(
cb: (regressions: (RegressionAtCommit | null)[])=> void,
) {
fetch(`/_/dryrun/status/${this.requestId}`)
.then(jsonOrThrow)
.then((json: DryRunStatus) => {
this.runningStatus = json.message;
if (!json.finished) {
window.setTimeout(() => this.checkDryRunStatus(cb), 300);
} else {
this.runSpinner!.active = false;
this.requestId = '';
}
// json.regressions will get filled in incrementally, so display them
// as they arrive.
cb(json.regressions!);
})
.catch((msg) => this.catch(msg));
}
}
define('cluster-lastn-page-sk', ClusterLastNPageSk);