Merge "Fix download dialog input UI in dark theme"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index ce633c8..b079dec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -2152,7 +2152,6 @@
const state: ChecksTabState = {};
detail.tabState = {checksTab: state};
if (this.params?.filter) state.filter = this.params.filter;
- if (this.params?.select) state.select = this.params.select;
if (this.params?.attempt) state.attempt = this.params.attempt;
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 578e771..cb299d0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -7,7 +7,14 @@
import {classMap} from 'lit/directives/class-map.js';
import {repeat} from 'lit/directives/repeat.js';
import {ifDefined} from 'lit/directives/if-defined.js';
-import {LitElement, css, html, PropertyValues, TemplateResult} from 'lit';
+import {
+ LitElement,
+ css,
+ html,
+ PropertyValues,
+ TemplateResult,
+ nothing,
+} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import './gr-checks-action';
import './gr-hovercard-run';
@@ -23,6 +30,13 @@
import {sharedStyles} from '../../styles/shared-styles';
import {CheckRun, RunResult} from '../../models/checks/checks-model';
import {
+ ALL_ATTEMPTS,
+ AttemptChoice,
+ attemptChoiceLabel,
+ isAttemptChoice,
+ LATEST_ATTEMPT,
+ sortAttemptChoices,
+ stringToAttemptChoice,
allResults,
createFixAction,
firstPrimaryLink,
@@ -34,7 +48,7 @@
secondaryLinks,
tooltipForLink,
} from '../../models/checks/checks-util';
-import {assertIsDefined, assert} from '../../utils/common-util';
+import {assertIsDefined, assert, unique} from '../../utils/common-util';
import {modifierPressed, toggleClass, whenVisible} from '../../utils/dom-util';
import {durationString} from '../../utils/date-util';
import {charsOnly} from '../../utils/string-util';
@@ -66,6 +80,8 @@
import {when} from 'lit/directives/when.js';
import {KnownExperimentId} from '../../services/flags/flags';
import {HtmlPatched} from '../../utils/lit-util';
+import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
+import './gr-checks-attempt';
/**
* Firing this event sets the regular expression of the results filter.
@@ -104,6 +120,9 @@
@state()
latestPatchNum?: PatchSetNumber;
+ @state()
+ selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
+
private getChangeModel = resolve(this, changeModelToken);
private getChecksModel = resolve(this, checksModelToken);
@@ -124,6 +143,11 @@
() => this.getChangeModel().latestPatchNum$,
x => (this.latestPatchNum = x)
);
+ subscribe(
+ this,
+ () => this.getChecksModel().checksSelectedAttemptNumber$,
+ x => (this.selectedAttempt = x)
+ );
}
static override get styles() {
@@ -353,6 +377,7 @@
>
${this.result.checkName}
</div>
+ ${this.renderAttempt()}
<div class="space"></div>
</div>
</td>
@@ -394,6 +419,11 @@
`;
}
+ private renderAttempt() {
+ if (this.selectedAttempt !== ALL_ATTEMPTS) return nothing;
+ return html`<gr-checks-attempt .run=${this.result}></gr-checks-attempt>`;
+ }
+
private renderExpanded() {
if (!this.isExpanded) return;
return html`<gr-result-expanded
@@ -771,12 +801,8 @@
@state()
latestPatchsetNumber: PatchSetNumber | undefined = undefined;
- /** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @property({attribute: false})
- selectedAttempts: Map<string, number | undefined> = new Map<
- string,
- number | undefined
- >();
+ @state()
+ selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
/** Maintains the state of which result sections should show all results. */
@state()
@@ -828,6 +854,11 @@
);
subscribe(
this,
+ () => this.getChecksModel().checksSelectedAttemptNumber$,
+ x => (this.selectedAttempt = x)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().latestPatchNum$,
x => (this.latestPatchsetNumber = x)
);
@@ -899,8 +930,10 @@
.notLatest .headerTopRow .right .goToLatest {
display: block;
}
+ .headerTopRow .right > * {
+ margin-left: var(--spacing-m);
+ }
.headerTopRow .right .goToLatest gr-button {
- margin-right: var(--spacing-m);
--gr-button-padding: var(--spacing-s) var(--spacing-m);
}
.headerBottomRow gr-icon {
@@ -1071,6 +1104,7 @@
header: true,
notLatest: !!this.checksPatchsetNumber,
};
+ const attemptItems = this.createAttemptDropdownItems();
return html`
<div class=${classMap(headerClasses)}>
<div class="headerTopRow">
@@ -1087,6 +1121,14 @@
>Go to latest patchset</gr-button
>
</div>
+ ${when(
+ attemptItems.length > 0,
+ () => html` <gr-dropdown-list
+ value=${this.selectedAttempt ?? 0}
+ .items=${attemptItems}
+ @value-change=${this.onAttemptSelected}
+ ></gr-dropdown-list>`
+ )}
<gr-dropdown-list
value=${this.checksPatchsetNumber ??
this.latestPatchsetNumber ??
@@ -1211,6 +1253,12 @@
></gr-checks-action>`;
}
+ private onAttemptSelected(e: CustomEvent<{value: string | undefined}>) {
+ const attempt = stringToAttemptChoice(e.detail.value);
+ assertIsDefined(attempt, `unexpected attempt choice ${e.detail.value}`);
+ this.getChecksModel().updateStateSetAttempt(attempt);
+ }
+
private onPatchsetSelected(e: CustomEvent<{value: string}>) {
const patchset = Number(e.detail.value);
assert(!isNaN(patchset), 'selected patchset must be a number');
@@ -1221,6 +1269,23 @@
this.getChecksModel().setPatchset(undefined);
}
+ private createAttemptDropdownItems() {
+ if (this.runs.every(run => run.isSingleAttempt)) return [];
+ const attempts: AttemptChoice[] = this.runs
+ .map(run => run.attempt ?? 0)
+ .filter(isAttemptChoice)
+ .filter(unique);
+ attempts.push(LATEST_ATTEMPT);
+ attempts.push(ALL_ATTEMPTS);
+ const items: DropdownItem[] = attempts.sort(sortAttemptChoices).map(a => {
+ return {
+ value: a,
+ text: attemptChoiceLabel(a),
+ };
+ });
+ return items;
+ }
+
private createPatchsetDropdownItems() {
if (!this.latestPatchsetNumber) return [];
return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
@@ -1244,7 +1309,7 @@
renderFilter() {
const runs = this.runs.filter(
run =>
- this.isRunSelected(run) && isAttemptSelected(this.selectedAttempts, run)
+ this.isRunSelected(run) && isAttemptSelected(this.selectedAttempt, run)
);
if (this.selectedRuns.length === 0 && allResults(runs).length <= 3) {
if (this.filterRegExp.source.length > 0) {
@@ -1279,7 +1344,7 @@
const isWarningOrError =
category === Category.WARNING || category === Category.ERROR;
const allRuns = this.runs.filter(run =>
- isAttemptSelected(this.selectedAttempts, run)
+ isAttemptSelected(this.selectedAttempt, run)
);
const all = allRuns.reduce(
(results: RunResult[], run) => [
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
index 5fcab03..f8addc5 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
@@ -12,8 +12,9 @@
import {fakeRun0, setAllFakeRuns} from '../../models/checks/checks-fakes';
import {resolve} from '../../models/dependency';
import {createLabelInfo} from '../../test/test-data-generators';
-import {queryAndAssert, query} from '../../utils/common-util';
+import {queryAndAssert, query, assertIsDefined} from '../../utils/common-util';
import {PatchSetNumber} from '../../api/rest-api';
+import {GrDropdownList} from '../shared/gr-dropdown-list/gr-dropdown-list';
suite('gr-result-row test', () => {
let element: GrResultRow;
@@ -136,11 +137,39 @@
html`<gr-checks-results></gr-checks-results>`
);
const getChecksModel = resolve(element, checksModelToken);
+ getChecksModel().allRunsSelectedPatchset$.subscribe(
+ runs => (element.runs = runs)
+ );
setAllFakeRuns(getChecksModel());
+ await element.updateComplete;
+ });
+
+ test('attempt dropdown items', async () => {
+ const attemptDropdown = queryAndAssert<GrDropdownList>(
+ element,
+ 'gr-dropdown-list'
+ );
+ assertIsDefined(attemptDropdown.items);
+ assert.equal(attemptDropdown.items.length, 42);
+ assert.deepEqual(attemptDropdown.items[0], {
+ text: 'Latest Attempt',
+ value: 'latest',
+ });
+ assert.deepEqual(attemptDropdown.items[1], {
+ text: 'All Attempts',
+ value: 'all',
+ });
+ assert.deepEqual(attemptDropdown.items[2], {
+ text: 'Attempt 0',
+ value: 0,
+ });
+ assert.deepEqual(attemptDropdown.items[41], {
+ text: 'Attempt 40',
+ value: 40,
+ });
});
test('renders', async () => {
- await element.updateComplete;
assert.shadowDom.equal(
element,
/* HTML */ `
@@ -157,11 +186,20 @@
<div class="goToLatest">
<gr-button link=""> Go to latest patchset </gr-button>
</div>
+ <gr-dropdown-list value="latest"> </gr-dropdown-list>
<gr-dropdown-list value="0"> </gr-dropdown-list>
</div>
</div>
<div class="headerBottomRow">
- <div class="left"></div>
+ <div class="left">
+ <div class="filterDiv">
+ <input
+ id="filterInput"
+ placeholder="Filter results by tag or regular expression"
+ type="text"
+ />
+ </div>
+ </div>
<div class="right">
<a href="https://www.google.com" target="_blank">
<gr-icon
@@ -212,36 +250,67 @@
</div>
</div>
<div class="body">
- <div class="collapsed">
- <h3 class="categoryHeader empty error heading-3">
- <gr-icon icon="expand_more" class="expandIcon"></gr-icon>
+ <div class="expanded">
+ <h3 class="categoryHeader error heading-3">
+ <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
<div class="statusIconWrapper">
<gr-icon icon="error" filled class="error statusIcon"></gr-icon>
<span class="title"> error </span>
- <span class="count"> (0) </span>
+ <span class="count"> (3) </span>
<paper-tooltip offset="5"> </paper-tooltip>
</div>
</h3>
+ <gr-result-row
+ class="FAKEErrorFinderFinderFinderFinderFinderFinderFinder"
+ >
+ </gr-result-row>
+ <gr-result-row
+ isexpandable
+ class="FAKEErrorFinderFinderFinderFinderFinderFinderFinder"
+ >
+ </gr-result-row>
+ <gr-result-row isexpandable class="FAKESuperCheck"> </gr-result-row>
+ <table class="resultsTable">
+ <thead>
+ <tr class="headerRow">
+ <th class="longNames nameCol">Run</th>
+ <th class="summaryCol">Summary</th>
+ <th class="expanderCol"></th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+ </table>
</div>
- <div class="collapsed">
- <h3 class="categoryHeader empty heading-3 warning">
- <gr-icon icon="expand_more" class="expandIcon"></gr-icon>
+ <div class="expanded">
+ <h3 class="categoryHeader heading-3 warning">
+ <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
<div class="statusIconWrapper">
<gr-icon icon="warning" filled class="warning statusIcon">
</gr-icon>
<span class="title"> warning </span>
- <span class="count"> (0) </span>
+ <span class="count"> (1) </span>
<paper-tooltip offset="5"> </paper-tooltip>
</div>
</h3>
+ <gr-result-row class="FAKESuperCheck" isexpandable> </gr-result-row>
+ <table class="resultsTable">
+ <thead>
+ <tr class="headerRow">
+ <th class="nameCol">Run</th>
+ <th class="summaryCol">Summary</th>
+ <th class="expanderCol"></th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+ </table>
</div>
<div class="collapsed">
- <h3 class="categoryHeader empty heading-3 info">
+ <h3 class="categoryHeader heading-3 info">
<gr-icon icon="expand_more" class="expandIcon"></gr-icon>
<div class="statusIconWrapper">
<gr-icon icon="info" class="info statusIcon"></gr-icon>
<span class="title"> info </span>
- <span class="count"> (0) </span>
+ <span class="count"> (3) </span>
<paper-tooltip offset="5"> </paper-tooltip>
</div>
</h3>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 99ae21e..addbfc0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -12,6 +12,10 @@
import {Action, Link, RunStatus} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
import {
+ ALL_ATTEMPTS,
+ AttemptChoice,
+ attemptChoiceLabel,
+ LATEST_ATTEMPT,
AttemptDetail,
compareByWorstCategory,
headerForStatus,
@@ -40,11 +44,7 @@
} from '../../models/checks/checks-fakes';
import {assertIsDefined} from '../../utils/common-util';
import {modifierPressed, whenVisible} from '../../utils/dom-util';
-import {
- fireAttemptSelected,
- fireRunSelected,
- fireRunSelectionReset,
-} from './gr-checks-util';
+import {fireRunSelected, fireRunSelectionReset} from './gr-checks-util';
import {ChecksTabState} from '../../types/events';
import {charsOnly} from '../../utils/string-util';
import {getAppContext} from '../../services/app-context';
@@ -198,8 +198,8 @@
@property({attribute: false})
selected = false;
- @property({attribute: false})
- selectedAttempt?: number;
+ @state()
+ selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
@property({attribute: false})
deselected = false;
@@ -212,26 +212,22 @@
private readonly reporting = getAppContext().reportingService;
+ private getChecksModel = resolve(this, checksModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChecksModel().checksSelectedAttemptNumber$,
+ x => (this.selectedAttempt = x)
+ );
+ }
+
override firstUpdated() {
assertIsDefined(this.chipElement, 'chip element');
whenVisible(this.chipElement, () => (this.shouldRender = true), 200);
}
- protected override updated(changedProperties: PropertyValues) {
- super.updated(changedProperties);
-
- // For some reason the browser does not pick up the correct `checked` state
- // that is set in renderAttempt(). So we have to set it programmatically
- // here.
- const selectedAttempt = this.selectedAttempt ?? this.run.attempt;
- const inputToBeSelected = this.shadowRoot?.querySelector(
- `.attemptDetails input#attempt-${selectedAttempt}`
- ) as HTMLInputElement | undefined;
- if (inputToBeSelected) {
- inputToBeSelected.checked = true;
- }
- }
-
override render() {
if (!this.shouldRender) return html`<div class="chip">Loading ...</div>`;
@@ -280,31 +276,31 @@
class="attemptDetails"
?hidden=${this.run.isSingleAttempt || !this.selected}
>
+ ${this.renderAttempt({attempt: LATEST_ATTEMPT})}
+ ${this.renderAttempt({attempt: ALL_ATTEMPTS})}
${this.run.attemptDetails.map(a => this.renderAttempt(a))}
</div>
`;
}
- isSelected(detail: AttemptDetail) {
- // this.selectedAttempt may be undefined, then choose the latest attempt,
- // which is what this.run has.
- const selectedAttempt = this.selectedAttempt ?? this.run.attempt;
- return detail.attempt === selectedAttempt;
- }
-
renderAttempt(detail: AttemptDetail) {
+ const attempt = detail.attempt ?? 0;
const checkNameId = charsOnly(this.run.checkName).toLowerCase();
const id = `attempt-${detail.attempt}`;
- const icon = detail.icon;
- const wasNotRun = icon?.name === iconFor(RunStatus.RUNNABLE)?.name;
+ const icon = detail.icon ?? {name: ''};
+ const wasNotRun =
+ icon?.name === iconFor(RunStatus.RUNNABLE)?.name &&
+ attempt !== LATEST_ATTEMPT &&
+ attempt !== ALL_ATTEMPTS;
+ const selected = this.selectedAttempt === attempt;
return html`<div class="attemptDetail">
<input
type="radio"
id=${id}
name=${`${checkNameId}-attempt-choice`}
- ?checked=${this.isSelected(detail)}
- ?disabled=${!this.isSelected(detail) && wasNotRun}
- @change=${() => this.handleAttemptChange(detail)}
+ .checked=${selected}
+ ?disabled=${!selected && wasNotRun}
+ @change=${() => this.handleAttemptChange(attempt)}
/>
<gr-icon
icon=${icon.name}
@@ -312,15 +308,13 @@
?filled=${icon.filled}
></gr-icon>
<label for=${id}>
- Attempt ${detail.attempt}${wasNotRun ? ' (not run)' : ''}
+ ${attemptChoiceLabel(attempt)}${wasNotRun ? ' (not run)' : ''}
</label>
</div>`;
}
- handleAttemptChange(detail: AttemptDetail) {
- if (!this.isSelected(detail)) {
- fireAttemptSelected(this, this.run.checkName, detail.attempt);
- }
+ handleAttemptChange(attempt: AttemptChoice) {
+ this.getChecksModel().updateStateSetAttempt(attempt);
}
renderETA() {
@@ -400,12 +394,8 @@
@query('#filterInput')
filterInput?: HTMLInputElement;
- /**
- * We prefer `undefined` over a RegExp with '', because `.source` yields
- * a strange '(?:)' for ''.
- */
@state()
- filterRegExp?: RegExp;
+ filterRegExp = '';
@property({attribute: false})
runs: CheckRun[] = [];
@@ -416,12 +406,8 @@
@property({attribute: false})
selectedRuns: string[] = [];
- /** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @property({attribute: false})
- selectedAttempts: Map<string, number | undefined> = new Map<
- string,
- number | undefined
- >();
+ @state()
+ selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
@property({attribute: false})
tabState?: ChecksTabState;
@@ -457,6 +443,16 @@
() => this.getChecksModel().loginCallbackLatest$,
x => (this.loginCallback = x)
);
+ subscribe(
+ this,
+ () => this.getChecksModel().checksSelectedAttemptNumber$,
+ x => (this.selectedAttempt = x)
+ );
+ subscribe(
+ this,
+ () => this.getChecksModel().runFilterRegexp$,
+ x => (this.filterRegExp = x)
+ );
this.addEventListener('click', () => {
if (this.collapsed) this.toggleCollapsed();
});
@@ -583,20 +579,7 @@
protected override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
- // This update is done is response to setting this.filterRegExp below, but
- // this.filterInput not yet being available at that point.
- if (this.filterInput && !this.filterInput.value && this.filterRegExp) {
- this.filterInput.value = this.filterRegExp.source;
- }
if (changedProperties.has('tabState') && this.tabState) {
- // Note that tabState.select and tabState.attempt are processed by
- // <gr-checks-tab>.
- if (
- this.tabState.filter &&
- this.tabState.filter !== this.filterRegExp?.source
- ) {
- this.filterRegExp = new RegExp(this.tabState.filter, 'i');
- }
const {statusOrCategory} = this.tabState;
if (
statusOrCategory === RunStatus.RUNNING ||
@@ -624,6 +607,7 @@
type="text"
placeholder="Filter runs by regular expression"
?hidden=${!this.showFilter()}
+ .value=${this.filterRegExp}
@input=${this.onInput}
/>
${this.renderSection(RunStatus.RUNNING)}
@@ -767,11 +751,8 @@
{},
{deduping: Deduping.EVENT_ONCE_PER_CHANGE}
);
- if (this.filterInput.value) {
- this.filterRegExp = new RegExp(this.filterInput.value, 'i');
- } else {
- this.filterRegExp = undefined;
- }
+ const value = this.filterInput.value;
+ this.getChecksModel().updateStateSetRunFilter(value ?? '');
}
toggle(
@@ -793,6 +774,7 @@
}
renderSection(status: RunStatus) {
+ const regExp = new RegExp(this.filterRegExp, 'i');
const runs = this.runs
.filter(r => r.isLatestAttempt)
.filter(
@@ -800,7 +782,7 @@
r.status === status ||
(status === RunStatus.RUNNING && r.status === RunStatus.SCHEDULED)
)
- .filter(r => !this.filterRegExp || this.filterRegExp.test(r.checkName))
+ .filter(r => regExp.test(r.checkName))
.sort(compareByWorstCategory);
if (runs.length === 0) return;
const expanded = this.isSectionExpanded.get(status) ?? true;
@@ -839,13 +821,11 @@
renderRun(run: CheckRun) {
const selectedRun = this.selectedRuns.includes(run.checkName);
- const selectedAttempt = this.selectedAttempts.get(run.checkName);
const deselected = !selectedRun && this.selectedRuns.length > 0;
return html`<gr-checks-run
.run=${run}
?condensed=${this.collapsed}
.selected=${selectedRun}
- .selectedAttempt=${selectedAttempt}
.deselected=${deselected}
></gr-checks-run>`;
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
index 1244a2f..5bb2788 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
@@ -5,12 +5,13 @@
*/
import '../../test/common-test-setup-karma';
import './gr-checks-runs';
-import {GrChecksRuns} from './gr-checks-runs';
+import {GrChecksRun, GrChecksRuns} from './gr-checks-runs';
import {html} from 'lit';
import {fixture, assert} from '@open-wc/testing';
import {checksModelToken} from '../../models/checks/checks-model';
-import {setAllFakeRuns} from '../../models/checks/checks-fakes';
+import {fakeRun0, setAllFakeRuns} from '../../models/checks/checks-fakes';
import {resolve} from '../../models/dependency';
+import {queryAll} from '../../utils/common-util';
suite('gr-checks-runs test', () => {
let element: GrChecksRuns;
@@ -21,16 +22,20 @@
);
const getChecksModel = resolve(element, checksModelToken);
setAllFakeRuns(getChecksModel());
+ await element.updateComplete;
});
- test('tabState filter', async () => {
- element.tabState = {filter: 'fff'};
+ test('filterRegExp', async () => {
+ // Without a filter all 6 fake runs (0-5) will be rendered.
+ assert.equal(queryAll(element, 'gr-checks-run').length, 6);
+
+ // This filter will only match fakeRun2 (checkName: 'FAKE Mega Analysis').
+ element.filterRegExp = 'Mega';
await element.updateComplete;
- assert.equal(element.filterRegExp?.source, 'fff');
+ assert.equal(queryAll(element, 'gr-checks-run').length, 1);
});
test('renders', async () => {
- await element.updateComplete;
assert.equal(element.runs.length, 44);
assert.shadowDom.equal(
element,
@@ -92,7 +97,7 @@
);
});
- test('renders', async () => {
+ test('renders collapsed', async () => {
element.collapsed = true;
await element.updateComplete;
assert.equal(element.runs.length, 44);
@@ -154,3 +159,66 @@
);
});
});
+
+suite('gr-checks-run test', () => {
+ let element: GrChecksRun;
+
+ setup(async () => {
+ element = await fixture<GrChecksRun>(html`<gr-checks-run></gr-checks-run>`);
+ const getChecksModel = resolve(element, checksModelToken);
+ setAllFakeRuns(getChecksModel());
+ await element.updateComplete;
+ });
+
+ test('renders loading', async () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ ' <div class="chip">Loading ...</div> '
+ );
+ });
+
+ test('renders fakeRun0', async () => {
+ element.shouldRender = true;
+ element.run = fakeRun0;
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="chip error" tabindex="0">
+ <div class="left">
+ <gr-hovercard-run> </gr-hovercard-run>
+ <gr-icon class="error" filled="" icon="error"> </gr-icon>
+ <span class="name">
+ FAKE Error Finder Finder Finder Finder Finder Finder Finder
+ </span>
+ </div>
+ <div class="middle">
+ <gr-checks-attempt> </gr-checks-attempt>
+ </div>
+ <div class="right"></div>
+ </div>
+ <div class="attemptDetails" hidden="">
+ <div class="attemptDetail">
+ <input
+ id="attempt-latest"
+ name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+ type="radio"
+ />
+ <gr-icon icon=""> </gr-icon>
+ <label for="attempt-latest"> Latest Attempt </label>
+ </div>
+ <div class="attemptDetail">
+ <input
+ id="attempt-all"
+ name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+ type="radio"
+ />
+ <gr-icon icon=""> </gr-icon>
+ <label for="attempt-all"> All Attempts </label>
+ </div>
+ </div>
+ </div>
+ `
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index b36c66d..c72bccb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -14,7 +14,7 @@
import './gr-checks-runs';
import './gr-checks-results';
import {NumericChangeId, PatchSetNumber} from '../../types/common';
-import {AttemptSelectedEvent, RunSelectedEvent} from './gr-checks-util';
+import {RunSelectedEvent} from './gr-checks-util';
import {TabState} from '../../types/events';
import {getAppContext} from '../../services/app-context';
import {subscribe} from '../lit/subscription-controller';
@@ -22,6 +22,7 @@
import {Interaction} from '../../constants/reporting';
import {resolve} from '../../models/dependency';
import {GrChecksRuns} from './gr-checks-runs';
+import {LATEST_ATTEMPT} from '../../models/checks/checks-util';
/**
* The "Checks" tab on the Gerrit change page. Gets its data from plugins that
@@ -53,13 +54,6 @@
@state()
selectedRuns: string[] = [];
- /** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @state()
- selectedAttempts: Map<string, number | undefined> = new Map<
- string,
- number | undefined
- >();
-
private readonly getChangeModel = resolve(this, changeModelToken);
private readonly getChecksModel = resolve(this, checksModelToken);
@@ -140,18 +134,14 @@
?collapsed=${this.offsetWidth < 1000}
.runs=${this.runs}
.selectedRuns=${this.selectedRuns}
- .selectedAttempts=${this.selectedAttempts}
.tabState=${this.tabState?.checksTab}
@run-selected=${this.handleRunSelected}
- @attempt-selected=${this.handleAttemptSelected}
></gr-checks-runs>
<gr-checks-results
class="results"
.tabState=${this.tabState?.checksTab}
.runs=${this.runs}
.selectedRuns=${this.selectedRuns}
- .selectedAttempts=${this.selectedAttempts}
- @run-selected=${this.handleRunSelected}
></gr-checks-results>
</div>
`;
@@ -159,63 +149,23 @@
protected override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
- if (changedProperties.has('tabState')) this.applyTabState();
- if (changedProperties.has('runs')) this.applyTabState();
+ if (changedProperties.has('tabState')) this.tabStateUpdated();
}
- /**
- * Clearing the tabState means that from now on the user interaction counts,
- * not the content of the URL (which is where tabState is populated from).
- */
- private clearTabState() {
- this.tabState = {};
- }
-
- /**
- * We want to keep applying the tabState to newly incoming check runs until
- * the user explicitly interacts with the selection or the attempts, which
- * will result in clearTabState() being called.
- */
- private applyTabState() {
+ private tabStateUpdated() {
if (!this.tabState?.checksTab) return;
- // Note that .filter is processed by <gr-checks-runs>.
- const {select, filter, attempt} = this.tabState.checksTab;
- if (!select) {
- this.selectedRuns = [];
- this.selectedAttempts = new Map<string, number>();
- return;
- }
- const regexpSelect = new RegExp(select, 'i');
- // We do not allow selection of runs that are invisible because of the
- // filter.
- const regexpFilter = new RegExp(filter ?? '', 'i');
- const selectedRuns = this.runs.filter(
- run =>
- regexpSelect.test(run.checkName) && regexpFilter.test(run.checkName)
- );
- this.selectedRuns = selectedRuns.map(run => run.checkName);
- const selectedAttempts = new Map<string, number>();
- if (attempt) {
- for (const run of selectedRuns) {
- if (run.isSingleAttempt) continue;
- const hasAttempt = run.attemptDetails.some(
- detail => detail.attempt === attempt
- );
- if (hasAttempt) selectedAttempts.set(run.checkName, attempt);
- }
- }
- this.selectedAttempts = selectedAttempts;
+ const {attempt, filter} = this.tabState.checksTab;
+ this.getChecksModel().updateStateSetAttempt(attempt ?? LATEST_ATTEMPT);
+ this.getChecksModel().updateStateSetRunFilter(filter ?? '');
}
handleRunSelected(e: RunSelectedEvent) {
- this.clearTabState();
this.reporting.reportInteraction(Interaction.CHECKS_RUN_SELECTED, {
checkName: e.detail.checkName,
reset: e.detail.reset,
});
if (e.detail.reset) {
this.selectedRuns = [];
- this.selectedAttempts = new Map();
return;
}
if (e.detail.checkName) {
@@ -223,24 +173,9 @@
}
}
- handleAttemptSelected(e: AttemptSelectedEvent) {
- this.clearTabState();
- this.reporting.reportInteraction(Interaction.CHECKS_ATTEMPT_SELECTED, {
- checkName: e.detail.checkName,
- attempt: e.detail.attempt,
- });
- const {checkName, attempt} = e.detail;
- this.selectedAttempts.set(checkName, attempt);
- // Force property update.
- this.selectedAttempts = new Map(this.selectedAttempts);
- }
-
toggleSelected(checkName: string) {
- this.clearTabState();
if (this.selectedRuns.includes(checkName)) {
this.selectedRuns = this.selectedRuns.filter(r => r !== checkName);
- this.selectedAttempts.set(checkName, undefined);
- this.selectedAttempts = new Map(this.selectedAttempts);
} else {
this.selectedRuns = [...this.selectedRuns, checkName];
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
index e797265..e554e1e 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
@@ -9,9 +9,8 @@
import {GrChecksTab} from './gr-checks-tab';
import {fixture, assert} from '@open-wc/testing';
import {checksModelToken} from '../../models/checks/checks-model';
-import {fakeRun4_3, setAllFakeRuns} from '../../models/checks/checks-fakes';
+import {setAllFakeRuns} from '../../models/checks/checks-fakes';
import {resolve} from '../../models/dependency';
-import {Category} from '../../api/checks';
suite('gr-checks-tab test', () => {
let element: GrChecksTab;
@@ -35,18 +34,4 @@
`
);
});
-
- test('select from tab state', async () => {
- element.tabState = {
- checksTab: {
- statusOrCategory: Category.ERROR,
- filter: 'elim',
- select: 'fake',
- attempt: 3,
- },
- };
- await element.updateComplete;
- assert.equal(element.selectedRuns.length, 39);
- assert.equal(element.selectedAttempts.get(fakeRun4_3.checkName), 3);
- });
});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index cd09a45..939a87e 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -4,33 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {CheckRun, RunResult} from '../../models/checks/checks-model';
-
-export interface AttemptSelectedEventDetail {
- checkName: string;
- attempt: number | undefined;
-}
-
-export type AttemptSelectedEvent = CustomEvent<AttemptSelectedEventDetail>;
-
-declare global {
- interface HTMLElementEventMap {
- 'attempt-selected': AttemptSelectedEvent;
- }
-}
-
-export function fireAttemptSelected(
- target: EventTarget,
- checkName: string,
- attempt: number | undefined
-) {
- target.dispatchEvent(
- new CustomEvent('attempt-selected', {
- detail: {checkName, attempt},
- composed: true,
- bubbles: true,
- })
- );
-}
+import {
+ ALL_ATTEMPTS,
+ AttemptChoice,
+ LATEST_ATTEMPT,
+} from '../../models/checks/checks-util';
export interface RunSelectedEventDetail {
reset: boolean;
@@ -66,13 +44,12 @@
}
export function isAttemptSelected(
- selectedAttempts: Map<string, number | undefined>,
+ selectedAttempt: AttemptChoice,
run: CheckRun
) {
- const selected = selectedAttempts.get(run.checkName);
- return (
- (selected === undefined && run.isLatestAttempt) || selected === run.attempt
- );
+ if (selectedAttempt === LATEST_ATTEMPT) return run.isLatestAttempt;
+ if (selectedAttempt === ALL_ATTEMPTS) return true;
+ return selectedAttempt === (run.attempt ?? 0);
}
export function matches(result: RunResult, regExp: RegExp) {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 0e2194f..9aa837d 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -213,13 +213,18 @@
}
private renderAttempt(attempt: AttemptDetail) {
+ const attemptNumber = attempt.attempt;
+ const icon = attempt.icon ?? {name: ''};
+ if (attemptNumber !== undefined && typeof attemptNumber !== 'number') {
+ return;
+ }
return html`
<div>
<div class="attemptIcon">
- <gr-icon icon=${attempt.icon.name} ?filled=${attempt.icon.filled}>
+ <gr-icon class=${icon.name} icon=${icon.name} ?filled=${icon.filled}>
</gr-icon>
</div>
- <div class="attemptNumber">${ordinal(attempt.attempt)}</div>
+ <div class="attemptNumber">${ordinal(attemptNumber)}</div>
</div>
`;
}
@@ -355,8 +360,8 @@
}
computeAttempts(): AttemptDetail[] {
- const details = this.run?.attemptDetails ?? [];
- const more =
+ const details: AttemptDetail[] = this.run?.attemptDetails ?? [];
+ const more: AttemptDetail[] =
details.length > 7
? [{icon: {name: 'more_horiz'}, attempt: undefined}]
: [];
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index da57d5a..358ab18 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -7,14 +7,16 @@
import './gr-hovercard-run';
import {fixture, html, assert} from '@open-wc/testing';
import {GrHovercardRun} from './gr-hovercard-run';
-import {fakeRun0} from '../../models/checks/checks-fakes';
+import {fakeRun4Att, fakeRun4_4} from '../../models/checks/checks-fakes';
+import {createAttemptMap} from '../../models/checks/checks-util';
+import {CheckRun} from '../../models/checks/checks-model';
suite('gr-hovercard-run tests', () => {
let element: GrHovercardRun;
setup(async () => {
element = await fixture<GrHovercardRun>(html`
- <gr-hovercard-run class="hovered" .run=${fakeRun0}></gr-hovercard-run>
+ <gr-hovercard-run class="hovered"></gr-hovercard-run>
`);
});
@@ -22,7 +24,12 @@
element.mouseHide(new MouseEvent('click'));
});
- test('render', () => {
+ test('render fakeRun4', async () => {
+ const attemptMap = createAttemptMap(fakeRun4Att);
+ const attemptDetails = attemptMap.get(fakeRun4_4.checkName)!.attempts;
+ const run: CheckRun = {...fakeRun4_4, attemptDetails};
+ element.run = run;
+ await element.updateComplete;
assert.shadowDom.equal(
element,
/* HTML */ `
@@ -37,22 +44,149 @@
</div>
<div class="section">
<div class="sectionIcon">
- <gr-icon icon="error" filled class="error"></gr-icon>
+ <gr-icon class="info" icon="info"></gr-icon>
</div>
<div class="sectionContent">
<h3 class="heading-3 name">
- <span>
- FAKE Error Finder Finder Finder Finder Finder Finder Finder
- </span>
+ <span> FAKE Elimination Long Long Long Long Long </span>
</h3>
</div>
</div>
+ <div class="section">
+ <div class="sectionIcon">
+ <gr-icon class="small" icon="info"> </gr-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">Status</div>
+ <div>
+ <a href="https://www.google.com" target="_blank">
+ <gr-icon
+ aria-label="external link to check status"
+ class="link small"
+ icon="open_in_new"
+ >
+ </gr-icon>
+ www.google.com
+ </a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="title">Message</div>
+ <div>Everything was eliminated already.</div>
+ </div>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <gr-icon class="small" icon="arrow_forward"> </gr-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="attempts row">
+ <div class="title">Attempt</div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="more_horiz" icon="more_horiz"> </gr-icon>
+ </div>
+ <div class="attemptNumber"></div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="error" filled="" icon="error"> </gr-icon>
+ </div>
+ <div class="attemptNumber">34th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="check_circle" icon="check_circle">
+ </gr-icon>
+ </div>
+ <div class="attemptNumber">35th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="error" filled="" icon="error"> </gr-icon>
+ </div>
+ <div class="attemptNumber">36th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="check_circle" icon="check_circle">
+ </gr-icon>
+ </div>
+ <div class="attemptNumber">37th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="error" filled="" icon="error"> </gr-icon>
+ </div>
+ <div class="attemptNumber">38th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="check_circle" icon="check_circle">
+ </gr-icon>
+ </div>
+ <div class="attemptNumber">39th</div>
+ </div>
+ <div>
+ <div class="attemptIcon">
+ <gr-icon class="info" icon="info"> </gr-icon>
+ </div>
+ <div class="attemptNumber">40th</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <gr-icon class="small" icon="schedule"> </gr-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">Started</div>
+ <div>1 year 5 m ago</div>
+ </div>
+ <div class="row">
+ <div class="title">Ended</div>
+ <div>1 year 5 m ago</div>
+ </div>
+ <div class="row">
+ <div class="title">Completion</div>
+ <div>1 minute</div>
+ </div>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <gr-icon class="small" icon="link"> </gr-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">Description</div>
+ <div>Shows you the possible eliminations.</div>
+ </div>
+ <div class="row">
+ <div class="title">Documentation</div>
+ <div>
+ <a href="https://www.google.com" target="_blank">
+ <gr-icon
+ aria-label="external link to check documentation"
+ class="link small"
+ icon="open_in_new"
+ >
+ </gr-icon>
+ www.google.com
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="action">
+ <gr-checks-action context="hovercard"> </gr-checks-action>
+ </div>
</div>
`
);
});
-
- test('hovercard is shown with error icon', () => {
- assert.equal(element.computeIcon().name, 'error');
- });
});
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 360eaa3..0e88b06 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -57,6 +57,10 @@
GroupDetailView,
RepoDetailView,
} from '../../../utils/router-util';
+import {
+ LATEST_ATTEMPT,
+ stringToAttemptChoice,
+} from '../../../models/checks/checks-util';
const RoutePattern = {
ROOT: '/',
@@ -1466,15 +1470,8 @@
if (tab) params.tab = tab;
const filter = ctx.queryMap.get('filter');
if (filter) params.filter = filter;
- const select = ctx.queryMap.get('select');
- if (select) params.select = select;
- const attempt = ctx.queryMap.get('attempt');
- if (attempt) {
- const attemptInt = parseInt(attempt);
- if (!isNaN(attemptInt) && attemptInt > 0) {
- params.attempt = attemptInt;
- }
- }
+ const attempt = stringToAttemptChoice(ctx.queryMap.get('attempt'));
+ if (attempt && attempt !== LATEST_ATTEMPT) params.attempt = attempt;
this.reporting.setRepoName(params.project);
this.reporting.setChangeId(changeNum);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index c8581df..29b8e7a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -1264,7 +1264,6 @@
patchNum: 7 as RevisionPatchSetNum,
attempt: 1,
filter: 'fff',
- select: 'sss',
tab: 'checks',
});
});
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 9f0857e..6abcedf 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -19,6 +19,7 @@
GroupDetailView,
RepoDetailView,
} from '../utils/router-util';
+import {AttemptChoice} from '../models/checks/checks-util';
export interface AppElement extends HTMLElement {
params: AppElementParams | GenerateUrlParameters;
@@ -120,10 +121,8 @@
tab?: string;
/** regular expression for filtering check runs */
filter?: string;
- /** regular expression for selecting check runs */
- select?: string;
/** selected attempt for selected check runs */
- attempt?: number;
+ attempt?: AttemptChoice;
}
export interface AppElementJustRegisteredParams {
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 9f50b8d..28dec3c 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -3,7 +3,13 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {AttemptDetail, createAttemptMap} from './checks-util';
+import {
+ AttemptChoice,
+ AttemptDetail,
+ createAttemptMap,
+ LATEST_ATTEMPT,
+ sortAttemptDetails,
+} from './checks-util';
import {assertIsDefined} from '../../utils/common-util';
import {select} from '../../utils/observable-util';
import {Finalizable} from '../../services/registry';
@@ -136,6 +142,15 @@
* can be picked up from the change model.
*/
patchsetNumberSelected?: PatchSetNumber;
+ /**
+ * This is the attempt number selected by the user. If this is `undefined`
+ * (default), then for each run the latest attempt is displayed.
+ */
+ attemptNumberSelected: AttemptChoice;
+ /**
+ * Current filter set by the user in the runs panel or via URL.
+ */
+ runFilterRegexp: string;
/** Checks data for the latest patchset. */
pluginStateLatest: {
[name: string]: ChecksProviderState;
@@ -200,6 +215,13 @@
state => state.patchsetNumberSelected
);
+ public checksSelectedAttemptNumber$ = select(
+ this.state$,
+ state => state.attemptNumberSelected
+ );
+
+ public runFilterRegexp$ = select(this.state$, state => state.runFilterRegexp);
+
public checksLatest$ = select(this.state$, state => state.pluginStateLatest);
public checksSelected$ = select(this.state$, state =>
@@ -362,6 +384,9 @@
readonly pluginsModel: PluginsModel
) {
super({
+ patchsetNumberSelected: undefined,
+ attemptNumberSelected: LATEST_ATTEMPT,
+ runFilterRegexp: '',
pluginStateLatest: {},
pluginStateSelected: {},
});
@@ -552,11 +577,7 @@
) {
const attemptMap = createAttemptMap(runs);
for (const attemptInfo of attemptMap.values()) {
- // Per run only one attempt can be undefined, so the '?? -1' is not really
- // relevant for sorting.
- attemptInfo.attempts.sort(
- (a, b) => (a.attempt ?? -1) - (b.attempt ?? -1)
- );
+ attemptInfo.attempts.sort(sortAttemptDetails);
}
const nextState = {...this.subject$.getValue()};
const pluginState = this.getPluginState(nextState, patchset);
@@ -573,9 +594,10 @@
assertIsDefined(attemptInfo, 'attemptInfo');
return {
...run,
+ attempt: run.attempt ?? 0,
pluginName,
internalRunId: runId,
- isLatestAttempt: attemptInfo.latestAttempt === run.attempt,
+ isLatestAttempt: attemptInfo.latestAttempt === (run.attempt ?? 0),
isSingleAttempt: attemptInfo.isSingleAttempt,
attemptDetails: attemptInfo.attempts,
results: (run.results ?? []).map((result, i) => {
@@ -638,6 +660,18 @@
this.subject$.next(nextState);
}
+ updateStateSetAttempt(attemptNumber: AttemptChoice) {
+ const nextState = {...this.subject$.getValue()};
+ nextState.attemptNumberSelected = attemptNumber;
+ this.subject$.next(nextState);
+ }
+
+ updateStateSetRunFilter(runFilter: string) {
+ const nextState = {...this.subject$.getValue()};
+ nextState.runFilterRegexp = runFilter;
+ this.subject$.next(nextState);
+ }
+
setPatchset(num?: PatchSetNumber) {
this.updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
}
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index f6dac8a..09c9273 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -15,7 +15,7 @@
import {PatchSetNumber} from '../../api/rest-api';
import {OpenFixPreviewEventDetail} from '../../types/events';
import {PROVIDED_FIX_ID} from '../../utils/comment-util';
-import {assertNever} from '../../utils/common-util';
+import {assert, assertNever} from '../../utils/common-util';
import {fire} from '../../utils/event-util';
import {CheckResult, CheckRun, RunResult} from './checks-model';
@@ -328,27 +328,79 @@
}
export interface AttemptDetail {
- attempt: number | undefined;
- icon: ChecksIcon;
+ attempt?: AttemptChoice;
+ icon?: ChecksIcon;
}
export interface AttemptInfo {
- latestAttempt: number | undefined;
+ latestAttempt: AttemptChoice;
isSingleAttempt: boolean;
attempts: AttemptDetail[];
}
+export type AttemptChoice = number | 'latest' | 'all';
+export const ALL_ATTEMPTS = 'all' as AttemptChoice;
+export const LATEST_ATTEMPT = 'latest' as AttemptChoice;
+
+export function isAttemptChoice(x: number | string): x is AttemptChoice {
+ if (typeof x === 'string') {
+ return x === ALL_ATTEMPTS || x === LATEST_ATTEMPT;
+ }
+ if (typeof x === 'number') {
+ return x >= 0;
+ }
+ return false;
+}
+
+export function stringToAttemptChoice(
+ s?: string | null
+): AttemptChoice | undefined {
+ if (s === undefined) return undefined;
+ if (s === null) return undefined;
+ if (s === '') return undefined;
+ if (isAttemptChoice(s)) return s;
+ const n = Number(s);
+ if (isAttemptChoice(n)) return n;
+ return undefined;
+}
+
+export function attemptChoiceLabel(attempt: AttemptChoice): string {
+ if (attempt === LATEST_ATTEMPT) return 'Latest Attempt';
+ if (attempt === ALL_ATTEMPTS) return 'All Attempts';
+ return `Attempt ${attempt}`;
+}
+
+export function sortAttemptDetails(a: AttemptDetail, b: AttemptDetail): number {
+ return sortAttemptChoices(a.attempt, b.attempt);
+}
+
+export function sortAttemptChoices(
+ a?: AttemptChoice,
+ b?: AttemptChoice
+): number {
+ if (a === b) return 0;
+ if (a === undefined) return -1;
+ if (b === undefined) return 1;
+ if (a === LATEST_ATTEMPT) return -1;
+ if (b === LATEST_ATTEMPT) return 1;
+ if (a === ALL_ATTEMPTS) return -1;
+ if (b === ALL_ATTEMPTS) return 1;
+ assert(typeof a === 'number', `unexpected attempt ${a}`);
+ assert(typeof b === 'number', `unexpected attempt ${b}`);
+ return a - b;
+}
+
export function createAttemptMap(runs: CheckRunApi[]) {
const map = new Map<string, AttemptInfo>();
for (const run of runs) {
const value = map.get(run.checkName);
- const detail = {
- attempt: run.attempt,
+ const detail: AttemptDetail = {
+ attempt: run.attempt ?? 0,
icon: iconForRun(fromApiToInternalRun(run)),
};
if (value === undefined) {
map.set(run.checkName, {
- latestAttempt: run.attempt,
+ latestAttempt: run.attempt ?? 0,
isSingleAttempt: true,
attempts: [detail],
});
diff --git a/polygerrit-ui/app/models/checks/checks-util_test.ts b/polygerrit-ui/app/models/checks/checks-util_test.ts
new file mode 100644
index 0000000..e53c29f
--- /dev/null
+++ b/polygerrit-ui/app/models/checks/checks-util_test.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup-karma';
+import './checks-model';
+import {assert} from '@open-wc/testing';
+import {
+ ALL_ATTEMPTS,
+ AttemptChoice,
+ LATEST_ATTEMPT,
+ sortAttemptChoices,
+ stringToAttemptChoice,
+} from './checks-util';
+
+suite('checks-util tests', () => {
+ setup(() => {});
+
+ teardown(() => {});
+
+ test('stringToAttemptChoice', () => {
+ assert.equal(stringToAttemptChoice('0'), 0);
+ assert.equal(stringToAttemptChoice('1'), 1);
+ assert.equal(stringToAttemptChoice('999'), 999);
+ assert.equal(stringToAttemptChoice('latest'), 'latest');
+ assert.equal(stringToAttemptChoice('all'), 'all');
+
+ assert.equal(stringToAttemptChoice(undefined), undefined);
+ assert.equal(stringToAttemptChoice(''), undefined);
+ assert.equal(stringToAttemptChoice('asdf'), undefined);
+ assert.equal(stringToAttemptChoice('-1'), undefined);
+ assert.equal(stringToAttemptChoice('1x'), undefined);
+ });
+
+ test('sortAttemptChoices', () => {
+ const unsorted: (AttemptChoice | undefined)[] = [
+ 3,
+ 1,
+ LATEST_ATTEMPT,
+ ALL_ATTEMPTS,
+ undefined,
+ 0,
+ 999,
+ ];
+ const sortedExpected: (AttemptChoice | undefined)[] = [
+ LATEST_ATTEMPT,
+ ALL_ATTEMPTS,
+ 0,
+ 1,
+ 3,
+ 999,
+ undefined,
+ ];
+ assert.deepEqual(unsorted.sort(sortAttemptChoices), sortedExpected);
+ });
+});
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 508b188..23acff4 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -8,6 +8,7 @@
import {FetchRequest} from './types';
import {LineNumberEventDetail, MovedLinkClickedEventDetail} from '../api/diff';
import {Category, RunStatus} from '../api/checks';
+import {AttemptChoice} from '../models/checks/checks-util';
export enum EventType {
BIND_VALUE_CHANGED = 'bind-value-changed',
@@ -232,10 +233,8 @@
checkName?: string;
/** regular expression for filtering runs */
filter?: string;
- /** regular expression for selecting runs */
- select?: string;
/** selected attempt for selected runs */
- attempt?: number;
+ attempt?: AttemptChoice;
}
export type SwitchTabEvent = CustomEvent<SwitchTabEventDetail>;
diff --git a/polygerrit-ui/app/utils/router-util.ts b/polygerrit-ui/app/utils/router-util.ts
index ec7993c..97e9ae0 100644
--- a/polygerrit-ui/app/utils/router-util.ts
+++ b/polygerrit-ui/app/utils/router-util.ts
@@ -21,6 +21,7 @@
import {assertNever} from './common-util';
import {GerritView} from '../services/router/router-model';
import {addQuotesWhen} from './string-util';
+import {AttemptChoice} from '../models/checks/checks-util';
export interface DashboardSection {
name: string;
@@ -73,10 +74,8 @@
tab?: string;
/** regular expression for filtering check runs */
filter?: string;
- /** regular expression for selecting check runs */
- select?: string;
/** selected attempt for selected check runs */
- attempt?: number;
+ attempt?: AttemptChoice;
usp?: string;
}