blob: 5d1c48174a7cf57696ae8484d50a868a5b959737 [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';
const template = (ele) => 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) => testRow(row, ele))}
</tbody>
</table>
</sort-toggle-sk>
<query-dialog-sk @edit=${ele._currentQueryChanged}></query-dialog-sk>
`;
const testRow = (row, ele) => {
// Returns a HintableObject for building the GET parameters to the legacy search page.
//
// TODO(lovisolo): Delete after the legacy search page has been removed.
const makeOldSearchCriteria = (opts) => ({
query: {'name': [row.name], 'source_type': [ele._currentCorpus]},
pos: opts.positive,
neg: opts.negative,
unt: opts.untriaged,
head: ele._showAllDigests ? 'false' : 'true',
include: ele._disregardIgnoreRules ? 'true' : 'false',
});
// Returns a HintableObject for building the GET parameters to the lit-html search page.
const makeNewSearchCriteria = (opts) => SearchCriteriaToHintableObject({
corpus: ele._currentCorpus,
leftHandTraceFilter: {'name': [row.name]},
includePositiveDigests: opts.positive,
includeNegativeDigests: opts.negative,
includeUntriagedDigests: opts.untriaged,
includeDigestsNotAtHead: ele._showAllDigests ? 'true' : 'false',
includeIgnoredDigests: ele._disregardIgnoreRules ? 'true' : 'false',
});
const searchPageHref = (opts) => {
const oldSearchCriteria = makeOldSearchCriteria(opts);
const newSearchCriteria = makeNewSearchCriteria(opts);
// Delete the "sort" parameter, which the legacy and lit-html versions of the search page read
// in incompatible ways: asc/desc vs. ascending/descending, respectively. Will default to
// descending if absent.
delete newSearchCriteria['sort'];
const oldQueryParameters = fromObject(oldSearchCriteria);
const newQueryParameters = fromObject(newSearchCriteria);
return `/search?${newQueryParameters}&${oldQueryParameters}`;
};
const clusterPageHref = () => {
const hintableObject = {
...makeNewSearchCriteria({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>`;
};
const searchQuery = (corpus, query) => {
if (!query) {
return `source_type=${corpus}`;
}
return `source_type=${corpus}, \n${query.split('&').join(',\n')}`;
};
define('list-page-sk', class extends ElementSk {
constructor() {
super(template);
this._corpora = [];
this._paramset = {};
this._currentQuery = '';
this._currentCorpus = '';
this._showAllDigests = false;
this._disregardIgnoreRules = false;
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 || false;
this._disregardIgnoreRules = newState.disregard_ignores || false;
this._currentCorpus = newState.corpus || defaultCorpus();
this._currentQuery = newState.query || '';
this._fetch();
this._render();
},
);
this._byTestCounts = [];
// Allows us to abort fetches if we fetch again.
this._fetchController = null;
}
connectedCallback() {
super.connectedCallback();
this._render();
}
_currentCorpusChanged(e) {
e.stopPropagation();
this._currentCorpus = e.detail;
this._stateChanged();
this._render();
this._fetch();
}
_currentQueryChanged(e) {
e.stopPropagation();
this._currentQuery = e.detail;
this._stateChanged();
this._render();
this._fetch();
}
_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((jsonList) => {
this._byTestCounts = jsonList;
this._byTestCounts.forEach((row) => {
row.total_digests = row.positive_digests + row.negative_digests + row.untriaged_digests;
});
this._render();
// By default, sort the data by name in ascending order (to match the direction set above).
$$('#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) => {
// 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'));
}
_showQueryDialog() {
$$('query-dialog-sk').open(this._paramset, this._currentQuery);
}
_toggleAllDigests(e) {
e.preventDefault();
this._showAllDigests = !this._showAllDigests;
this._stateChanged();
this._render();
this._fetch();
}
_toggleIgnoreRules(e) {
e.preventDefault();
this._disregardIgnoreRules = !this._disregardIgnoreRules;
this._stateChanged();
this._render();
this._fetch();
}
});