Merge "Implement filtering and lazy loading of checks"
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 34945a9..e448374 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -15,7 +15,14 @@
* limitations under the License.
*/
import {html} from 'lit-html';
-import {css, customElement, property, PropertyValues} from 'lit-element';
+import {
+ css,
+ customElement,
+ internalProperty,
+ property,
+ PropertyValues,
+ query,
+} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
import {Category, CheckRun, Link, RunStatus, Tag} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
@@ -26,6 +33,8 @@
iconForCategory,
isRunning,
} from '../../services/checks/checks-util';
+import {assertIsDefined} from '../../utils/common-util';
+import {whenVisible} from '../../utils/dom-util';
@customElement('gr-result-row')
class GrResultRow extends GrLitElement {
@@ -38,6 +47,9 @@
@property({type: Boolean, reflect: true})
isExpandable = false;
+ @property()
+ shouldRender = false;
+
static get styles() {
return [
sharedStyles,
@@ -126,8 +138,26 @@
super.update(changedProperties);
}
+ firstUpdated() {
+ const loading = this.shadowRoot?.querySelector('.container');
+ assertIsDefined(loading, '"Loading" element');
+ whenVisible(loading, () => this.setAttribute('shouldRender', 'true'), 200);
+ }
+
render() {
if (!this.result) return '';
+ if (!this.shouldRender) {
+ return html`
+ <tr class="container">
+ <td class="iconCol"></td>
+ <td class="nameCol">
+ <div><span class="loading">Loading...</span></div>
+ </td>
+ <td class="summaryCol"></td>
+ <td class="expanderCol"></td>
+ </tr>
+ `;
+ }
return html`
<tr class="container" @click="${this.toggleExpanded}">
<td class="iconCol">
@@ -256,6 +286,12 @@
@customElement('gr-checks-results')
export class GrChecksResults extends GrLitElement {
+ @query('#filterInput')
+ filterInput?: HTMLInputElement;
+
+ @internalProperty()
+ filterRegExp = new RegExp('');
+
@property()
runs: CheckRun[] = [];
@@ -267,6 +303,11 @@
display: block;
padding: var(--spacing-xl);
}
+ input#filterInput {
+ margin-top: var(--spacing-s);
+ padding: var(--spacing-s) var(--spacing-m);
+ min-width: 400px;
+ }
.categoryHeader {
margin-top: var(--spacing-l);
margin-left: var(--spacing-l);
@@ -310,12 +351,30 @@
render() {
return html`
<div><h2 class="heading-2">Results</h2></div>
- ${this.renderNoCompleted()} ${this.renderSection(Category.ERROR)}
+ ${this.renderFilter()} ${this.renderNoCompleted()}
+ ${this.renderSection(Category.ERROR)}
${this.renderSection(Category.WARNING)}
${this.renderSection(Category.INFO)} ${this.renderSuccess()}
`;
}
+ renderFilter() {
+ if (this.runs.length === 0) return;
+ return html`
+ <input
+ id="filterInput"
+ type="text"
+ placeholder="Filter results by regular expression"
+ @input="${this.onInput}"
+ />
+ `;
+ }
+
+ onInput() {
+ assertIsDefined(this.filterInput, 'filter <input> element');
+ this.filterRegExp = new RegExp(this.filterInput.value, 'i');
+ }
+
renderNoCompleted() {
if (this.runs.some(hasCompleted)) return;
let text = 'No results';
@@ -358,6 +417,11 @@
renderRun(category: Category, run: CheckRun) {
return html`${run.results
?.filter(result => result.category === category)
+ .filter(
+ result =>
+ this.filterRegExp.test(run.checkName) ||
+ this.filterRegExp.test(result.summary)
+ )
.map(
result =>
html`<gr-result-row .result="${{...run, ...result}}"></gr-result-row>`
@@ -365,7 +429,9 @@
}
renderSuccess() {
- const runs = this.runs.filter(hasCompletedWithoutResults);
+ const runs = this.runs
+ .filter(hasCompletedWithoutResults)
+ .filter(r => this.filterRegExp.test(r.checkName));
if (runs.length === 0) return;
return html`
<h3 class="categoryHeader heading-3">
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 5b7428b..71ad041 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -16,7 +16,13 @@
*/
import {html} from 'lit-html';
import {classMap} from 'lit-html/directives/class-map';
-import {css, customElement, property} from 'lit-element';
+import {
+ css,
+ customElement,
+ internalProperty,
+ property,
+ query,
+} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
import {Action, CheckRun, RunStatus} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
@@ -35,15 +41,14 @@
fakeRun4,
updateStateSetResults,
} from '../../services/checks/checks-model';
+import {assertIsDefined, toggleSetMembership} from '../../utils/common-util';
+import {whenVisible} from '../../utils/dom-util';
-/* The RunSelectedEvent is only used locally to communicate from <gr-checks-run>
- to its <gr-checks-runs> parent. */
-
-interface RunSelectedEventDetail {
+export interface RunSelectedEventDetail {
checkName: string;
}
-type RunSelectedEvent = CustomEvent<RunSelectedEventDetail>;
+export type RunSelectedEvent = CustomEvent<RunSelectedEventDetail>;
declare global {
interface HTMLElementEventMap {
@@ -55,8 +60,8 @@
target.dispatchEvent(
new CustomEvent('run-selected', {
detail: {checkName},
- composed: false,
- bubbles: false,
+ composed: true,
+ bubbles: true,
})
);
}
@@ -124,9 +129,21 @@
background-color: var(--selected-background);
padding-left: calc(var(--spacing-m) + var(--thick-border) - 1px);
}
+ div.chip.deselected {
+ border: 1px solid var(--gray-foreground);
+ background-color: transparent;
+ padding-left: calc(var(--spacing-m) + var(--thick-border) - 1px);
+ }
div.chip.selected iron-icon {
color: var(--selected-foreground);
}
+ div.chip.deselected iron-icon {
+ color: var(--gray-foreground);
+ }
+ .chip.selected gr-button.action,
+ .chip.deselected gr-button.action {
+ display: none;
+ }
gr-button.action {
--padding: var(--spacing-xs) var(--spacing-m);
/* The button should fit into the 20px line-height. The negative
@@ -139,15 +156,40 @@
];
}
+ @query('.chip')
+ chipElement?: HTMLElement;
+
@property()
run!: CheckRun;
@property()
selected = false;
+ @property()
+ deselected = false;
+
+ @property()
+ shouldRender = false;
+
+ firstUpdated() {
+ assertIsDefined(this.chipElement, 'chip element');
+ whenVisible(
+ this.chipElement,
+ () => this.setAttribute('shouldRender', 'true'),
+ 200
+ );
+ }
+
render() {
- const icon = this.selected ? 'check-circle' : iconForRun(this.run);
- const classes = {chip: true, [icon]: true, selected: this.selected};
+ if (!this.shouldRender) return html`<div class="chip">Loading ...</div>`;
+
+ const icon = this.selected ? 'filter' : iconForRun(this.run);
+ const classes = {
+ chip: true,
+ [icon]: true,
+ selected: this.selected,
+ deselected: this.deselected,
+ };
const action = primaryRunAction(this.run);
return html`
@@ -185,6 +227,12 @@
@customElement('gr-checks-runs')
export class GrChecksRuns extends GrLitElement {
+ @query('#filterInput')
+ filterInput?: HTMLInputElement;
+
+ @internalProperty()
+ filterRegExp = new RegExp('');
+
@property()
runs: CheckRun[] = [];
@@ -207,6 +255,11 @@
padding-top: var(--spacing-l);
text-transform: capitalize;
}
+ input#filterInput {
+ margin-top: var(--spacing-s);
+ padding: var(--spacing-s) var(--spacing-m);
+ width: 100%;
+ }
.testing {
margin-top: var(--spacing-xxl);
color: var(--deemphasized-text-color);
@@ -227,6 +280,12 @@
render() {
return html`
<h2 class="heading-2">Runs</h2>
+ <input
+ id="filterInput"
+ type="text"
+ placeholder="Filter runs by regular expression"
+ @input="${this.onInput}"
+ />
${this.renderSection(RunStatus.COMPLETED)}
${this.renderSection(RunStatus.RUNNING)}
${this.renderSection(RunStatus.RUNNABLE)}
@@ -253,6 +312,11 @@
`;
}
+ onInput() {
+ assertIsDefined(this.filterInput, 'filter <input> element');
+ this.filterRegExp = new RegExp(this.filterInput.value, 'i');
+ }
+
none() {
updateStateSetResults('f0', []);
updateStateSetResults('f1', []);
@@ -277,6 +341,7 @@
renderSection(status: RunStatus) {
const runs = this.runs
.filter(r => r.status === status)
+ .filter(r => this.filterRegExp.test(r.checkName))
.sort(compareByWorstCategory);
if (runs.length === 0) return;
return html`
@@ -289,20 +354,17 @@
renderRun(run: CheckRun) {
const selected = this.selectedRuns.has(run.checkName);
+ const deselected = !selected && this.selectedRuns.size > 0;
return html`<gr-checks-run
.run="${run}"
.selected="${selected}"
+ .deselected="${deselected}"
@run-selected="${this.handleRunSelected}"
></gr-checks-run>`;
}
handleRunSelected(e: RunSelectedEvent) {
- const checkName = e.detail.checkName;
- if (this.selectedRuns.has(checkName)) {
- this.selectedRuns.delete(checkName);
- } else {
- this.selectedRuns.add(checkName);
- }
+ toggleSetMembership(this.selectedRuns, e.detail.checkName);
this.requestUpdate();
}
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 61f8bd1..0ce81ed 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -32,7 +32,11 @@
ActionTriggeredEvent,
fireActionTriggered,
} from '../../services/checks/checks-util';
-import {checkRequiredProperty} from '../../utils/common-util';
+import {
+ checkRequiredProperty,
+ toggleSetMembership,
+} from '../../utils/common-util';
+import {RunSelectedEvent} from './gr-checks-runs';
/**
* The "Checks" tab on the Gerrit change page. Gets its data from plugins that
@@ -53,6 +57,8 @@
@property()
changeNum: NumericChangeId | undefined = undefined;
+ private selectedRuns = new Set<string>();
+
constructor() {
super();
this.subscribe('runs', allRuns$);
@@ -100,6 +106,9 @@
render() {
const ps = `Patchset ${this.currentPatchNum} (Latest)`;
+ const filteredRuns = this.runs.filter(
+ r => this.selectedRuns.size === 0 || this.selectedRuns.has(r.checkName)
+ );
return html`
<div class="header">
<div class="left">
@@ -118,10 +127,14 @@
</div>
</div>
<div class="container">
- <gr-checks-runs class="runs" .runs="${this.runs}"></gr-checks-runs>
+ <gr-checks-runs
+ class="runs"
+ .runs="${this.runs}"
+ @run-selected="${this.handleRunSelected}"
+ ></gr-checks-runs>
<gr-checks-results
class="results"
- .runs="${this.runs}"
+ .runs="${filteredRuns}"
></gr-checks-results>
</div>
`;
@@ -148,6 +161,11 @@
action.name
);
}
+
+ handleRunSelected(e: RunSelectedEvent) {
+ toggleSetMembership(this.selectedRuns, e.detail.checkName);
+ this.requestUpdate();
+ }
}
@customElement('gr-checks-top-level-action')
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 0a3ef5b..7745da8 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -128,6 +128,8 @@
<g id="message"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#launch-->
<g id="launch"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></g>
+ <!-- This SVG is a copy from material.io https://material.io/icons/#filter-->
+ <g id="filter"><path d="M0,0h24 M24,24H0" fill="none"/><path d="M4.25,5.61C6.27,8.2,10,13,10,13v6c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1v-6c0,0,3.72-4.8,5.74-7.39 C20.25,4.95,19.78,4,18.95,4H5.04C4.21,4,3.74,4.95,4.25,5.61z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
<!-- This is just a placeholder, i.e. an empty icon that has the same size as a normal icon. -->
<g id="placeholder"><path d="M0 0h24v24H0z" fill="none"/></g>
</defs>
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 2246251..f4d6d51 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -111,3 +111,14 @@
}
return true;
}
+
+/**
+ * Add value, if the set does not contain it. Otherwise remove it.
+ */
+export function toggleSetMembership<T>(set: Set<T>, value: T): void {
+ if (set.has(value)) {
+ set.delete(value);
+ } else {
+ set.add(value);
+ }
+}
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index aa83173..32014d7 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -16,6 +16,7 @@
*/
import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {check} from './common-util';
/**
* Event emitted from polymer elements.
@@ -258,3 +259,22 @@
(/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream)
);
}
+
+export function whenVisible(
+ element: Element,
+ callback: () => void,
+ marginPx = 0
+) {
+ const observer = new IntersectionObserver(
+ (entries: IntersectionObserverEntry[]) => {
+ check(entries.length === 1, 'Expected one intersection observer entry.');
+ const entry = entries[0];
+ if (entry.isIntersecting) {
+ observer.unobserve(entry.target);
+ callback();
+ }
+ },
+ {rootMargin: `${marginPx}px`}
+ );
+ observer.observe(element);
+}