blob: 4c1a84f1e5e790396ddb14aa50740581f28081af [file] [log] [blame]
/**
* @module module/list-page-sk
* @description <h2><code>list-page-sk</code></h2>
*
* This page summarizes the outputs of various tests. It shows the amount of digests produced,
* as well as a few options to configure what range of traces to enumerate.
*
* It is a top level element.
*/
import { define } from 'elements-sk/define';
import { html } from 'lit-html';
import { $$ } from 'common-sk/modules/dom';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { stateReflector } from 'common-sk/modules/stateReflector';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
import { defaultCorpus } from '../settings';
import '../corpus-selector-sk';
import '../query-dialog-sk';
import '../sort-toggle-sk';
import 'elements-sk/checkbox-sk';
import 'elements-sk/icon/group-work-icon-sk';
import 'elements-sk/icon/tune-icon-sk';
import { SearchCriteriaToHintableObject } from '../search-controls-sk';
import { fromObject } from 'common-sk/modules/query';
import { QueryDialogSk } from '../query-dialog-sk/query-dialog-sk';
import { SortToggleSk } from '../sort-toggle-sk/sort-toggle-sk';
import { SearchCriteriaHintableObject } from '../search-controls-sk/search-controls-sk';
import { HintableObject } from 'common-sk/modules/hintable';
import { ListTestsResponse, ParamSet, TestSummary } from '../rpc_types';
const searchQuery = (corpus: string, query: string): string => {
if (!query) {
return `source_type=${corpus}`;
}
return `source_type=${corpus}, \n${query.split('&').join(',\n')}`;
};
export class ListPageSk extends ElementSk {
private static template = (ele: ListPageSk) => html`
<div>
<corpus-selector-sk .corpora=${ele.corpora}
.selectedCorpus=${ele.currentCorpus} @corpus-selected=${ele.currentCorpusChanged}>
</corpus-selector-sk>
<div class=query_params>
<button class=show_query_dialog @click=${ele.showQueryDialog}>
<tune-icon-sk></tune-icon-sk>
</button>
<pre>${searchQuery(ele.currentCorpus, ele.currentQuery)}</pre>
<checkbox-sk label="Digests at Head Only" class=head_only
?checked=${!ele.showAllDigests} @click=${ele.toggleAllDigests}></checkbox-sk>
<checkbox-sk label="Disregard Ignore Rules" class=ignore_rules
?checked=${ele.disregardIgnoreRules} @click=${ele.toggleIgnoreRules}></checkbox-sk>
</div>
</div>
<!-- lit-html (or maybe html in general) doesn't like sort-toggle-sk to go inside the table.-->
<sort-toggle-sk id=sort_table .data=${ele.byTestCounts} @sort-changed=${ele._render}>
<table>
<thead>
<tr>
<th data-key=name data-sort-toggle-sk=up>Test name</th>
<th data-key=positive_digests>Positive</th>
<th data-key=negative_digests>Negative</th>
<th data-key=untriaged_digests>Untriaged</th>
<th data-key=total_digests>Total</th>
<th>Cluster View</th>
</tr>
</thead>
<tbody>
<!-- repeat was tested here; map is about twice as fast as using the repeat directive
(which moves the existing elements). This is because reusing the existing templates
is pretty fast because there isn't a lot to change.-->
${ele.byTestCounts.map((row) => ListPageSk.testRow(row, ele))}
</tbody>
</table>
</sort-toggle-sk>
<query-dialog-sk @edit=${ele.currentQueryChanged}></query-dialog-sk>
`;
private static testRow = (row: TestSummary, ele: ListPageSk) => {
interface MakeSearchCriteriaOpts {
positive: boolean;
negative: boolean;
untriaged: boolean;
}
// Returns a HintableObject for building the GET parameters to the search page.
const makeSearchCriteria = (opts: MakeSearchCriteriaOpts): SearchCriteriaHintableObject =>
SearchCriteriaToHintableObject({
corpus: ele.currentCorpus,
leftHandTraceFilter: {'name': [row.name]},
includePositiveDigests: opts.positive,
includeNegativeDigests: opts.negative,
includeUntriagedDigests: opts.untriaged,
includeDigestsNotAtHead: ele.showAllDigests,
includeIgnoredDigests: ele.disregardIgnoreRules,
});
const searchPageHref = (opts: MakeSearchCriteriaOpts) => {
const searchCriteria = makeSearchCriteria(opts);
const queryParameters = fromObject(searchCriteria as HintableObject);
return `/search?${queryParameters}`;
};
const clusterPageHref = () => {
const hintableObject: HintableObject = {
...makeSearchCriteria({
positive: true,
negative: true,
untriaged: true
}),
left_filter: '',
grouping: row.name,
};
return `/cluster?${fromObject(hintableObject)}`;
}
return html`
<tr>
<td>
<a href="${searchPageHref({positive: true, negative: true, untriaged: true})}"
target=_blank rel=noopener>
${row.name}
</a>
</td>
<td class=center>
<a href="${searchPageHref({positive: true, negative: false, untriaged: false})}"
target=_blank rel=noopener>
${row.positive_digests}
</a>
</td>
<td class=center>
<a href="${searchPageHref({positive: false, negative: true, untriaged: false})}"
target=_blank rel=noopener>
${row.negative_digests}
</a>
</td>
<td class=center>
<a href="${searchPageHref({positive: false, negative: false, untriaged: true})}"
target=_blank rel=noopener>
${row.untriaged_digests}
</a>
</td>
<td class=center>
<a href="${searchPageHref({positive: true, negative: true, untriaged: true})}"
target=_blank rel=noopener>
${row.total_digests}
</a>
</td>
<td class=center>
<a href="${clusterPageHref()}" target=_blank rel=noopener>
<group-work-icon-sk></group-work-icon-sk>
</a>
</td>
</tr>
`;
};
private corpora: string[] = [];
private paramset: ParamSet = {};
private currentQuery = '';
private currentCorpus = '';
private showAllDigests = false;
private disregardIgnoreRules = false;
private byTestCounts: TestSummary[] = [];
private readonly stateChanged: () => void;
// Allows us to abort fetches if we fetch again.
private fetchController?: AbortController;
constructor() {
super(ListPageSk.template);
this.stateChanged = stateReflector(
/* getState */() => ({
// provide empty values
all_digests: this.showAllDigests,
disregard_ignores: this.disregardIgnoreRules,
corpus: this.currentCorpus,
query: this.currentQuery,
}), /* setState */(newState) => {
if (!this._connected) {
return;
}
// default values if not specified.
this.showAllDigests = newState.all_digests as boolean || false;
this.disregardIgnoreRules = newState.disregard_ignores as boolean || false;
this.currentCorpus = newState.corpus as string || defaultCorpus();
this.currentQuery = newState.query as string || '';
this.fetch();
this._render();
},
);
}
connectedCallback() {
super.connectedCallback();
this._render();
}
private currentCorpusChanged(e: CustomEvent<string>) {
e.stopPropagation();
this.currentCorpus = e.detail;
this.stateChanged();
this._render();
this.fetch();
}
private currentQueryChanged(e: CustomEvent<string>) {
e.stopPropagation();
this.currentQuery = e.detail;
this.stateChanged();
this._render();
this.fetch();
}
private fetch() {
if (this.fetchController) {
// Kill any outstanding requests
this.fetchController.abort();
}
// Make a fresh abort controller for each set of fetches.
// They cannot be re-used once aborted.
this.fetchController = new AbortController();
const extra = {
signal: this.fetchController.signal,
};
sendBeginTask(this);
sendBeginTask(this);
let url = `/json/v1/list?corpus=${encodeURIComponent(this.currentCorpus)}`;
if (!this.showAllDigests) {
url += '&at_head_only=true';
}
if (this.disregardIgnoreRules) {
url += '&include_ignored_traces=true';
}
if (this.currentQuery) {
url += `&trace_values=${encodeURIComponent(this.currentQuery)}`;
}
fetch(url, extra)
.then(jsonOrThrow)
.then((response: ListTestsResponse) => {
this.byTestCounts = response.tests || [];
this._render();
// By default, sort the data by name in ascending order (to match the direction set
// above).
$$<SortToggleSk<TestSummary>>('#sort_table', this)!.sort('name', 'up');
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'list'));
// TODO(kjlubick) when the search page gets a makeover to have just the params for the given
// corpus show up, we should do the same here. First idea is to have a separate corpora
// endpoint and then make paramset take a corpus.
fetch('/json/v1/paramset', extra)
.then(jsonOrThrow)
.then((paramset: ParamSet) => {
// We split the paramset into a list of corpora...
this.corpora = paramset.source_type || [];
// ...and the rest of the keys. This is to make it so the layout is
// consistent with other pages (e.g. the search page, the by blame page, etc).
delete paramset.source_type;
this.paramset = paramset;
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'paramset'));
}
private showQueryDialog() {
$$<QueryDialogSk>('query-dialog-sk')!.open(this.paramset, this.currentQuery);
}
private toggleAllDigests(e: Event) {
e.preventDefault();
this.showAllDigests = !this.showAllDigests;
this.stateChanged();
this._render();
this.fetch();
}
private toggleIgnoreRules(e: Event) {
e.preventDefault();
this.disregardIgnoreRules = !this.disregardIgnoreRules;
this.stateChanged();
this._render();
this.fetch();
}
}
define('list-page-sk', ListPageSk);