blob: d0ca9d006093718e510d9952b3c0bf64dcf10a99 [file] [log] [blame]
/**
* @module module/cluster-page-sk
* @description <h2><code>cluster-page-sk</code></h2>
*
* The top level element for clustering traces.
*
*/
import { define } from 'elements-sk/define';
import { fromObject, toParamSet } 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/checkbox-sk';
import 'elements-sk/styles/buttons';
import '../../../infra-sk/modules/paramset-sk';
import '../../../infra-sk/modules/sort-sk';
import '../../../infra-sk/modules/query-sk';
import '../algo-select-sk';
import '../cluster-summary2-sk';
import '../commit-detail-picker-sk';
import '../day-range-sk';
import '../query-count-sk';
import {
FrameResponse,
ParamSet,
RegressionDetectionRequest,
ClusterAlgo,
Commit,
FullSummary,
RegressionDetectionResponse,
} from '../json';
import { AlgoSelectAlgoChangeEventDetail } from '../algo-select-sk/algo-select-sk';
import { QuerySkQueryChangeEventDetail } from '../../../infra-sk/modules/query-sk/query-sk';
import { ClusterSummary2SkOpenKeysEventDetail } from '../cluster-summary2-sk/cluster-summary2-sk';
import { CommitDetailPanelSkCommitSelectedDetails } from '../commit-detail-panel-sk/commit-detail-panel-sk';
import { messagesToErrorString, startRequest } from '../progress/progress';
// The state that gets reflected to the URL.
class State {
begin: number = Math.floor(Date.now() / 1000 - 24 * 60 * 60);
end: number = Math.floor(Date.now() / 1000);
offset: number = -1;
radius: number = window.sk.perf.radius;
query: string = '';
k: number = 0;
algo: ClusterAlgo = 'kmeans';
interesting: number = window.sk.perf.interesting;
sparse: boolean = false;
constructor() {
if (window.sk.perf.demo) {
this.begin = Math.floor(new Date(2020, 4, 1).valueOf() / 1000);
this.end = Math.floor(new Date(2020, 5, 1).valueOf() / 1000);
}
}
}
export class ClusterPageSk extends ElementSk {
// The state to be reflected to the URL.
private state = new State();
private paramset: ParamSet = {};
// The computed clusters.
private summaries: FullSummary[] = [];
// The commits to choose from.
private cids: Commit[] = [];
// The id of the current cluster request. Will be the empty string if
// there is no pending request.
private requestId: string = '';
// The status of a running request.
private status: string = '';
// The spinner we display when waiting for results.
private spinner: SpinnerSk | null = null;
constructor() {
super(ClusterPageSk.template);
}
private static template = (ele: ClusterPageSk) => html`
<h2>Commit</h2>
<commit-detail-picker-sk
@commit-selected=${ele.commitSelected}
.selection=${ele.state.offset}
id="commit"
></commit-detail-picker-sk>
<h2>Algorithm</h2>
<algo-select-sk
algo=${ele.state.algo}
@algo-change=${ele.algoChange}
></algo-select-sk>
<h2>Query</h2>
<div class="query-action">
<query-sk
@query-change=${ele.queryChanged}
.key_order=${window.sk.perf.key_order}
.paramset=${ele.paramset}
current_query=${ele.state.query}
></query-sk>
<div id="selections">
<h3>Selections</h3>
<paramset-sk
id="summary"
.paramsets=${[toParamSet(ele.state.query)]}
></paramset-sk>
<div>
Matches:
<query-count-sk
url="/_/count/"
current_query=${ele.state.query}
@paramset-changed=${ele.paramsetChanged}
></query-count-sk>
</div>
<button
@click=${ele.start}
class="action"
id="start"
?disabled=${!!ele.requestId || ele.state.offset === -1}
>
Run
</button>
<div>
<spinner-sk id=run-spinner></spinner-sk>
<span>${ele.status}</span>
</div>
</div>
</div>
<details>
<summary id="advanced">
Advanced
</summary>
<div id="inputs">
<label>
K (A value of 0 means the server chooses).
<input .value=${ele.state.k.toString()} @input=${ele.kChange} />
</label>
<label>
Number of commits to include on either side.
<input .value=${ele.state.radius.toString()} @input=${ele.radiusChange} />
</label>
<label>
Clusters are interesting if regression score &gt;= this.
<input
.value=${ele.state.interesting.toString()}
@input=${ele.interestingChange}
/>
</label>
<checkbox-sk
?checked=${ele.state.sparse}
label="Data is sparse, so only include commits that have data."
@input=${ele.sparseChange}
></checkbox-sk>
</div>
</details>
<h2>Results</h2>
<sort-sk target="clusters">
<button data-key="clustersize">Cluster Size</button>
<button data-key="stepregression" data-default="up">Regression</button>
<button data-key="stepsize">Step Size</button>
<button data-key="steplse">Least Squares</button>
</sort-sk>
<div id="clusters" @open-keys=${ele.openKeys}>
${ClusterPageSk._summaryRows(ele)}
</div>
`;
private static _summaryRows = (ele: ClusterPageSk) => {
const ret = ele.summaries.map(
(summary) => html`
<cluster-summary2-sk
.full_summary=${summary}
notriage
></cluster-summary2-sk>
`,
);
if (!ret.length) {
ret.push(html`<p class="info"> No clusters found. </p>`);
}
return ret;
};
connectedCallback(): void {
super.connectedCallback();
this._render();
this.spinner = this.querySelector('#run-spinner');
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
fetch(`/_/initpage/?tz=${tz}`)
.then(jsonOrThrow)
.then((json: FrameResponse) => {
this.paramset = json.dataframe!.paramset;
this._render();
})
.catch(errorMessage);
this.stateHasChanged = stateReflector(
() => (this.state as unknown) as HintableObject,
(state) => {
this.state = (state as unknown) as State;
this._render();
},
);
}
// 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 algoChange(e: CustomEvent<AlgoSelectAlgoChangeEventDetail>) {
this.state.algo = e.detail.algo;
this.stateHasChanged();
}
private kChange(e: InputEvent) {
this.state.k = +(e.target! as HTMLInputElement).value;
this.stateHasChanged();
}
private radiusChange(e: InputEvent) {
this.state.radius = +(e.target! as HTMLInputElement).value;
this.stateHasChanged();
}
private interestingChange(e: InputEvent) {
this.state.interesting = +(e.target! as HTMLInputElement).value;
this.stateHasChanged();
}
private sparseChange(e: InputEvent) {
this.state.sparse = (e.target! as HTMLInputElement).checked;
this.stateHasChanged();
}
private queryChanged(e: CustomEvent<QuerySkQueryChangeEventDetail>) {
this.state.query = e.detail.q;
this.stateHasChanged();
this._render();
}
private paramsetChanged(e: CustomEvent<ParamSet>) {
this.paramset = e.detail;
this._render();
}
private openKeys(e: CustomEvent<ClusterSummary2SkOpenKeysEventDetail>) {
const query = {
keys: e.detail.shortcut,
begin: e.detail.begin,
end: e.detail.end,
xbaroffset: e.detail.xbar.offset,
num_commits: 50,
request_type: 1,
};
window.open(`/e/?${fromObject(query)}`, '_blank');
}
private commitSelected(
e: CustomEvent<CommitDetailPanelSkCommitSelectedDetails>,
) {
this.state.offset = ((e.detail.commit as unknown) as Commit).offset;
this.stateHasChanged();
}
private catch(msg: string) {
this.requestId = '';
this.status = '';
if (msg) {
errorMessage(msg);
}
this._render();
}
private async start() {
if (this.requestId) {
errorMessage('There is a pending query already running.');
return;
}
const body: RegressionDetectionRequest = {
step: 0,
total_queries: 0,
alert: {
id_as_string: '-1',
display_name: '',
radius: +this.state.radius,
query: this.state.query,
k: +this.state.k,
algo: this.state.algo,
interesting: +this.state.interesting,
sparse: this.state.sparse,
step: '',
alert: '',
bug_uri_template: '',
state: 'ACTIVE',
owner: '',
step_up_only: false,
direction: 'BOTH',
group_by: '',
minimum_num: 0,
category: '',
},
domain: {
offset: +this.state.offset,
n: 0,
end: new Date().toISOString(),
},
};
this.summaries = [];
// Set a value for _requestId so the spinner starts, and we don't start
// another request too soon.
this.requestId = 'pending';
this._render();
try {
const prog = await startRequest('/_/cluster/start', body, 300, this.spinner!, null);
if (prog.status === 'Error') {
throw new Error(messagesToErrorString(prog.messages));
}
this.summaries = [];
const regressionDetectionResponse = prog.results as RegressionDetectionResponse;
regressionDetectionResponse.summary!.Clusters!.forEach(
(clusterSummary) => {
this.summaries.push({
summary: clusterSummary!,
frame: regressionDetectionResponse.frame!,
triage: {
status: '',
message: '',
},
});
},
);
} catch (error) {
this.catch(error);
} finally {
this.requestId = '';
this._render();
}
}
}
define('cluster-page-sk', ClusterPageSk);