Migrate checks-model to new pattern - Rename service to model on appContext. - Merge ...-model.ts and ...-service.ts. - Rename ...Service to ...Model. - Move all observables onto ...Model. - Inject ...Model in the models and components that were directly accessing the observables. Google-Bug-Id: b/206459178 Change-Id: I65d722a58f264444f0dd1b1a5380187b91c06528
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts index f59fdec..e8c5916 100644 --- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts +++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -20,15 +20,9 @@ import {sharedStyles} from '../../../styles/shared-styles'; import {getAppContext} from '../../../services/app-context'; import { - allRunsLatestPatchsetLatestAttempt$, - aPluginHasRegistered$, CheckResult, CheckRun, ErrorMessages, - errorMessagesLatest$, - loginCallbackLatest$, - someProvidersAreLoadingFirstTime$, - topLevelActionsLatest$, } from '../../../services/checks/checks-model'; import {Action, Category, Link, RunStatus} from '../../../api/checks'; import {fireShowPrimaryTab} from '../../../utils/event-util'; @@ -413,20 +407,40 @@ private userModel = getAppContext().userModel; - private checksService = getAppContext().checksService; + private checksModel = getAppContext().checksModel; constructor() { super(); - subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x)); - subscribe(this, aPluginHasRegistered$, x => (this.showChecksSummary = x)); subscribe( this, - someProvidersAreLoadingFirstTime$, + this.checksModel.allRunsLatestPatchsetLatestAttempt$, + x => (this.runs = x) + ); + subscribe( + this, + this.checksModel.aPluginHasRegistered$, + x => (this.showChecksSummary = x) + ); + subscribe( + this, + this.checksModel.someProvidersAreLoadingFirstTime$, x => (this.someProvidersAreLoading = x) ); - subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x)); - subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x)); - subscribe(this, topLevelActionsLatest$, x => (this.actions = x)); + subscribe( + this, + this.checksModel.errorMessagesLatest$, + x => (this.errorMessages = x) + ); + subscribe( + this, + this.checksModel.loginCallbackLatest$, + x => (this.loginCallback = x) + ); + subscribe( + this, + this.checksModel.topLevelActionsLatest$, + x => (this.actions = x) + ); subscribe(this, changeComments$, x => (this.changeComments = x)); subscribe(this, threads$, x => (this.commentThreads = x)); subscribe(this, this.userModel.account$, x => (this.selfAccount = x)); @@ -560,7 +574,7 @@ } private handleAction(e: CustomEvent<Action>) { - this.checksService.triggerAction(e.detail); + this.checksModel.triggerAction(e.detail); } private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
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 8b530f6..d8ce665 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
@@ -181,7 +181,6 @@ fireTitleChange, } from '../../../utils/event-util'; import {GerritView, routerView$} from '../../../services/router/router-model'; -import {aPluginHasRegistered$} from '../../../services/checks/checks-model'; import { debounce, DelayedTask, @@ -286,6 +285,8 @@ private readonly changeService = getAppContext().changeService; + private readonly checksModel = getAppContext().checksModel; + /** * URL params passed from the router. */ @@ -633,7 +634,7 @@ override ready() { super.ready(); this.subscriptions.push( - aPluginHasRegistered$.subscribe(b => { + this.checksModel.aPluginHasRegistered$.subscribe(b => { this._showChecksTab = b; }) );
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts index 7ef4487..7a50bc1 100644 --- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts +++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -44,10 +44,7 @@ import {fontStyles} from '../../../styles/gr-font-styles'; import {charsOnly} from '../../../utils/string-util'; import {subscribe} from '../../lit/subscription-controller'; -import { - allRunsLatestPatchsetLatestAttempt$, - CheckRun, -} from '../../../services/checks/checks-model'; +import {CheckRun} from '../../../services/checks/checks-model'; import { firstPrimaryLink, getResultsOf, @@ -57,6 +54,7 @@ import '../../shared/gr-vote-chip/gr-vote-chip'; import {fireShowPrimaryTab} from '../../../utils/event-util'; import {PrimaryTab} from '../../../constants/constants'; +import {getAppContext} from '../../../services/app-context'; /** * @attr {Boolean} suppress-title - hide titles, currently for hovercard view @@ -149,9 +147,15 @@ ]; } + private readonly checksModel = getAppContext().checksModel; + constructor() { super(); - subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x)); + subscribe( + this, + this.checksModel.allRunsLatestPatchsetLatestAttempt$, + x => (this.runs = x) + ); } override render() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts index b213fa6..74d0e30 100644 --- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts +++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -28,7 +28,7 @@ @property({type: Object}) eventTarget: HTMLElement | null = null; - private checksService = getAppContext().checksService; + private checksModel = getAppContext().checksModel; override connectedCallback() { super.connectedCallback(); @@ -80,7 +80,7 @@ handleClick(e: Event) { e.stopPropagation(); - this.checksService.triggerAction(this.action); + this.checksModel.triggerAction(this.action); } }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts index ca53d28..24c06e0 100644 --- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts +++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -32,14 +32,7 @@ Tag, } from '../../api/checks'; import {sharedStyles} from '../../styles/shared-styles'; -import { - CheckRun, - checksSelectedPatchsetNumber$, - RunResult, - someProvidersAreLoadingSelected$, - topLevelActionsSelected$, - topLevelLinksSelected$, -} from '../../services/checks/checks-model'; +import {CheckRun, RunResult} from '../../services/checks/checks-model'; import { allResults, firstPrimaryLink, @@ -95,7 +88,7 @@ @state() labels?: LabelNameToInfoMap; - private checksService = getAppContext().checksService; + private checksModel = getAppContext().checksModel; constructor() { super(); @@ -493,7 +486,7 @@ } private handleAction(e: CustomEvent<Action>) { - this.checksService.triggerAction(e.detail); + this.checksModel.triggerAction(e.detail); } private renderAction(action?: Action) { @@ -733,21 +726,29 @@ */ private isSectionExpandedByUser = new Map<Category, boolean>(); - private readonly checksService = getAppContext().checksService; + private readonly checksModel = getAppContext().checksModel; constructor() { super(); - subscribe(this, topLevelActionsSelected$, x => (this.actions = x)); - subscribe(this, topLevelLinksSelected$, x => (this.links = x)); subscribe( this, - checksSelectedPatchsetNumber$, + this.checksModel.topLevelActionsSelected$, + x => (this.actions = x) + ); + subscribe( + this, + this.checksModel.topLevelLinksSelected$, + x => (this.links = x) + ); + subscribe( + this, + this.checksModel.checksSelectedPatchsetNumber$, x => (this.checksPatchsetNumber = x) ); subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x)); subscribe( this, - someProvidersAreLoadingSelected$, + this.checksModel.someProvidersAreLoadingSelected$, x => (this.someProvidersAreLoading = x) ); } @@ -1101,7 +1102,7 @@ } private handleAction(e: CustomEvent<Action>) { - this.checksService.triggerAction(e.detail); + this.checksModel.triggerAction(e.detail); } private renderAction(action?: Action) { @@ -1112,11 +1113,11 @@ private onPatchsetSelected(e: CustomEvent<{value: string}>) { const patchset = Number(e.detail.value); check(!isNaN(patchset), 'selected patchset must be a number'); - this.checksService.setPatchset(patchset as PatchSetNumber); + this.checksModel.setPatchset(patchset as PatchSetNumber); } private goToLatestPatchset() { - this.checksService.setPatchset(undefined); + this.checksModel.setPatchset(undefined); } private createPatchsetDropdownItems() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts index 474d2f2..20041de 100644 --- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts +++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -33,11 +33,11 @@ worstCategory, } from '../../services/checks/checks-util'; import { - allRunsSelectedPatchset$, CheckRun, ChecksPatchset, ErrorMessages, - errorMessagesLatest$, +} from '../../services/checks/checks-model'; +import { fakeActions, fakeLinks, fakeRun0, @@ -45,9 +45,7 @@ fakeRun2, fakeRun3, fakeRun4Att, - loginCallbackLatest$, - updateStateSetResults, -} from '../../services/checks/checks-model'; +} from '../../services/checks/checks-fakes'; import {assertIsDefined} from '../../utils/common-util'; import {modifierPressed, whenVisible} from '../../utils/dom-util'; import { @@ -391,13 +389,25 @@ private flagService = getAppContext().flagsService; - private checksService = getAppContext().checksService; + private checksModel = getAppContext().checksModel; constructor() { super(); - subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x)); - subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x)); - subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x)); + subscribe( + this, + this.checksModel.allRunsSelectedPatchset$, + x => (this.runs = x) + ); + subscribe( + this, + this.checksModel.errorMessagesLatest$, + x => (this.errorMessages = x) + ); + subscribe( + this, + this.checksModel.loginCallbackLatest$, + x => (this.loginCallback = x) + ); } static override get styles() { @@ -619,7 +629,7 @@ link ?disabled=${runButtonDisabled} @click="${() => { - actions.forEach(action => this.checksService.triggerAction(action)); + actions.forEach(action => this.checksModel.triggerAction(action)); }}" >Run Selected</gr-button > @@ -659,25 +669,79 @@ } none() { - updateStateSetResults('f0', [], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f1', [], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f2', [], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f3', [], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f4', [], [], [], ChecksPatchset.LATEST); + this.checksModel.updateStateSetResults( + 'f0', + [], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f1', + [], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f2', + [], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f3', + [], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f4', + [], + [], + [], + ChecksPatchset.LATEST + ); } all() { - updateStateSetResults( + this.checksModel.updateStateSetResults( 'f0', [fakeRun0], fakeActions, fakeLinks, ChecksPatchset.LATEST ); - updateStateSetResults('f1', [fakeRun1], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f2', [fakeRun2], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f3', [fakeRun3], [], [], ChecksPatchset.LATEST); - updateStateSetResults('f4', fakeRun4Att, [], [], ChecksPatchset.LATEST); + this.checksModel.updateStateSetResults( + 'f1', + [fakeRun1], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f2', + [fakeRun2], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f3', + [fakeRun3], + [], + [], + ChecksPatchset.LATEST + ); + this.checksModel.updateStateSetResults( + 'f4', + fakeRun4Att, + [], + [], + ChecksPatchset.LATEST + ); } toggle( @@ -687,7 +751,7 @@ links: Link[] = [] ) { const newRuns = this.runs.includes(runs[0]) ? [] : runs; - updateStateSetResults( + this.checksModel.updateStateSetResults( plugin, newRuns, actions,
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts index d1ccd11..16c9bfe 100644 --- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts +++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -17,13 +17,7 @@ import {LitElement, css, html, PropertyValues} from 'lit'; import {customElement, property, state} from 'lit/decorators'; import {Action} from '../../api/checks'; -import { - CheckResult, - CheckRun, - allResultsSelected$, - checksSelectedPatchsetNumber$, - allRunsSelectedPatchset$, -} from '../../services/checks/checks-model'; +import {CheckResult, CheckRun} from '../../services/checks/checks-model'; import './gr-checks-runs'; import './gr-checks-results'; import {changeNum$, latestPatchNum$} from '../../services/change/change-model'; @@ -68,15 +62,23 @@ number | undefined >(); - private readonly checksService = getAppContext().checksService; + private readonly checksModel = getAppContext().checksModel; constructor() { super(); - subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x)); - subscribe(this, allResultsSelected$, x => (this.results = x)); subscribe( this, - checksSelectedPatchsetNumber$, + this.checksModel.allRunsSelectedPatchset$, + x => (this.runs = x) + ); + subscribe( + this, + this.checksModel.allResultsSelected$, + x => (this.results = x) + ); + subscribe( + this, + this.checksModel.checksSelectedPatchsetNumber$, x => (this.checksPatchsetNumber = x) ); subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x)); @@ -140,7 +142,7 @@ } handleActionTriggered(action: Action, run?: CheckRun) { - this.checksService.triggerAction(action, run); + this.checksModel.triggerAction(action, run); } handleRunSelected(e: RunSelectedEvent) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts index 6484f92..e1f3d3c 100644 --- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts +++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -43,7 +43,7 @@ export class GrChecksApi implements ChecksPluginApi { private state = State.NOT_REGISTERED; - private readonly checksService = getAppContext().checksService; + private readonly checksModel = getAppContext().checksModel; private readonly reporting = getAppContext().reportingService; @@ -53,14 +53,14 @@ announceUpdate() { this.reporting.trackApi(this.plugin, 'checks', 'announceUpdate'); - this.checksService.reload(this.plugin.getPluginName()); + this.checksModel.reload(this.plugin.getPluginName()); } updateResult(run: CheckRun, result: CheckResult) { if (result.externalId === undefined) { throw new Error('ChecksApi.updateResult() was called without externalId'); } - this.checksService.updateResult(this.plugin.getPluginName(), run, result); + this.checksModel.updateResult(this.plugin.getPluginName(), run, result); } register(provider: ChecksProvider, config?: ChecksApiConfig): void { @@ -68,7 +68,7 @@ if (this.state === State.REGISTERED) throw new Error('Only one provider can be registered per plugin.'); this.state = State.REGISTERED; - this.checksService.register( + this.checksModel.register( this.plugin.getPluginName(), provider, config ?? DEFAULT_CONFIG
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts index 17e0994..16308c0 100644 --- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts +++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -83,8 +83,8 @@ commentsService: (_ctx: Partial<AppContext>) => { throw new Error('commentsService is not implemented'); }, - checksService: (_ctx: Partial<AppContext>) => { - throw new Error('checksService is not implemented'); + checksModel: (_ctx: Partial<AppContext>) => { + throw new Error('checksModel is not implemented'); }, jsApiService: (_ctx: Partial<AppContext>) => { throw new Error('jsApiService is not implemented');
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts index bfc56b4..6333934 100644 --- a/polygerrit-ui/app/services/app-context-init.ts +++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -22,7 +22,7 @@ import {Auth} from './gr-auth/gr-auth_impl'; import {GrRestApiInterface} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface'; import {ChangeService} from './change/change-service'; -import {ChecksService} from './checks/checks-service'; +import {ChecksModel} from './checks/checks-model'; import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element'; import {GrStorageService} from './storage/gr-storage_impl'; import {UserModel} from './user/user-model'; @@ -61,9 +61,9 @@ assertIsDefined(ctx.restApiService, 'restApiService'); return new CommentsService(ctx.restApiService!); }, - checksService: (ctx: Partial<AppContext>) => { + checksModel: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.reportingService, 'reportingService'); - return new ChecksService(ctx.reportingService!); + return new ChecksModel(ctx.reportingService!); }, jsApiService: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.reportingService, 'reportingService');
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts index 53064fa..4488b12 100644 --- a/polygerrit-ui/app/services/app-context.ts +++ b/polygerrit-ui/app/services/app-context.ts
@@ -21,7 +21,7 @@ import {AuthService} from './gr-auth/gr-auth'; import {RestApiService} from './gr-rest-api/gr-rest-api'; import {ChangeService} from './change/change-service'; -import {ChecksService} from './checks/checks-service'; +import {ChecksModel} from './checks/checks-model'; import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types'; import {StorageService} from './storage/gr-storage'; import {UserModel} from './user/user-model'; @@ -38,7 +38,7 @@ restApiService: RestApiService; changeService: ChangeService; commentsService: CommentsService; - checksService: ChecksService; + checksModel: ChecksModel; jsApiService: JsApiService; storageService: StorageService; configModel: ConfigModel;
diff --git a/polygerrit-ui/app/services/checks/checks-fakes.ts b/polygerrit-ui/app/services/checks/checks-fakes.ts new file mode 100644 index 0000000..09cd2e7 --- /dev/null +++ b/polygerrit-ui/app/services/checks/checks-fakes.ts
@@ -0,0 +1,418 @@ +/** + * @license + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Action, + Category, + Link, + LinkIcon, + RunStatus, + TagColor, +} from '../../api/checks'; +import {CheckRun} from './checks-model'; + +// TODO(brohlfs): Eventually these fakes should be removed. But they have proven +// to be super convenient for testing, debugging and demoing, so I would like to +// keep them around for a few quarters. Maybe remove by EOY 2022? + +export const fakeRun0: CheckRun = { + pluginName: 'f0', + internalRunId: 'f0', + checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder', + labelName: 'Presubmit', + isSingleAttempt: true, + isLatestAttempt: true, + attemptDetails: [], + results: [ + { + internalResultId: 'f0r0', + category: Category.ERROR, + summary: 'I would like to point out this error: 1 is not equal to 2!', + links: [ + {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL}, + ], + tags: [{name: 'OBSOLETE'}, {name: 'E2E'}], + }, + { + internalResultId: 'f0r1', + category: Category.ERROR, + summary: 'Running the mighty test has failed by crashing.', + message: 'Btw, 1 is also not equal to 3. Did you know?', + actions: [ + { + name: 'Ignore', + tooltip: 'Ignore this result', + primary: true, + callback: () => Promise.resolve({message: 'fake "ignore" triggered'}), + }, + { + name: 'Flag', + tooltip: 'Flag this result as totally absolutely really not useful', + primary: true, + disabled: true, + callback: () => Promise.resolve({message: 'flag "flag" triggered'}), + }, + { + name: 'Upload', + tooltip: 'Upload the result to the super cloud.', + primary: false, + callback: () => Promise.resolve({message: 'fake "upload" triggered'}), + }, + ], + tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}], + links: [ + {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL}, + {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD}, + { + primary: true, + url: 'https://google.com', + icon: LinkIcon.DOWNLOAD_MOBILE, + }, + {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, + {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, + {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE}, + {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG}, + {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE}, + {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY}, + ], + }, + ], + status: RunStatus.COMPLETED, +}; + +export const fakeRun1: CheckRun = { + pluginName: 'f1', + internalRunId: 'f1', + checkName: 'FAKE Super Check', + statusLink: 'https://www.google.com/', + patchset: 1, + labelName: 'Verified', + isSingleAttempt: true, + isLatestAttempt: true, + attemptDetails: [], + results: [ + { + internalResultId: 'f1r0', + category: Category.WARNING, + summary: 'We think that you could improve this.', + message: `There is a lot to be said. A lot. I say, a lot.\n + So please keep reading.`, + tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}], + codePointers: [ + { + path: '/COMMIT_MSG', + range: { + start_line: 10, + start_character: 0, + end_line: 10, + end_character: 0, + }, + }, + { + path: 'polygerrit-ui/app/api/checks.ts', + range: { + start_line: 5, + start_character: 0, + end_line: 7, + end_character: 0, + }, + }, + ], + links: [ + {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL}, + {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD}, + { + primary: true, + url: 'https://google.com', + icon: LinkIcon.DOWNLOAD_MOBILE, + }, + {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, + { + primary: false, + url: 'https://google.com', + tooltip: 'look at this', + icon: LinkIcon.IMAGE, + }, + { + primary: false, + url: 'https://google.com', + tooltip: 'not at this', + icon: LinkIcon.IMAGE, + }, + ], + }, + ], + status: RunStatus.RUNNING, +}; + +export const fakeRun2: CheckRun = { + pluginName: 'f2', + internalRunId: 'f2', + checkName: 'FAKE Mega Analysis', + statusDescription: 'This run is nearly completed, but not quite.', + statusLink: 'https://www.google.com/', + checkDescription: + 'From what the title says you can tell that this check analyses.', + checkLink: 'https://www.google.com/', + scheduledTimestamp: new Date('2021-04-01T03:14:15'), + startedTimestamp: new Date('2021-04-01T04:24:25'), + finishedTimestamp: new Date('2021-04-01T04:44:44'), + isSingleAttempt: true, + isLatestAttempt: true, + attemptDetails: [], + actions: [ + { + name: 'Re-Run', + tooltip: 'More powerful run than before', + primary: true, + callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), + }, + { + name: 'Monetize', + primary: true, + disabled: true, + callback: () => Promise.resolve({message: 'fake "monetize" triggered'}), + }, + { + name: 'Delete', + primary: true, + callback: () => Promise.resolve({message: 'fake "delete" triggered'}), + }, + ], + results: [ + { + internalResultId: 'f2r0', + category: Category.INFO, + summary: 'This is looking a bit too large.', + message: `We are still looking into how large exactly. Stay tuned. +And have a look at https://www.google.com! + +Or have a look at change 30000. +Example code: + const constable = ''; + var variable = '';`, + tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}], + }, + ], + status: RunStatus.COMPLETED, +}; + +export const fakeRun3: CheckRun = { + pluginName: 'f3', + internalRunId: 'f3', + checkName: 'FAKE Critical Observations', + status: RunStatus.RUNNABLE, + isSingleAttempt: true, + isLatestAttempt: true, + attemptDetails: [], +}; + +export const fakeRun4_1: CheckRun = { + pluginName: 'f4', + internalRunId: 'f4', + checkName: 'FAKE Elimination Long Long Long Long Long', + status: RunStatus.RUNNABLE, + attempt: 1, + isSingleAttempt: false, + isLatestAttempt: false, + attemptDetails: [], +}; + +export const fakeRun4_2: CheckRun = { + pluginName: 'f4', + internalRunId: 'f4', + checkName: 'FAKE Elimination Long Long Long Long Long', + status: RunStatus.COMPLETED, + attempt: 2, + isSingleAttempt: false, + isLatestAttempt: false, + attemptDetails: [], + results: [ + { + internalResultId: 'f42r0', + category: Category.INFO, + summary: 'Please eliminate all the TODOs!', + }, + ], +}; + +export const fakeRun4_3: CheckRun = { + pluginName: 'f4', + internalRunId: 'f4', + checkName: 'FAKE Elimination Long Long Long Long Long', + status: RunStatus.COMPLETED, + attempt: 3, + isSingleAttempt: false, + isLatestAttempt: false, + attemptDetails: [], + results: [ + { + internalResultId: 'f43r0', + category: Category.ERROR, + summary: 'Without eliminating all the TODOs your change will break!', + }, + ], +}; + +export const fakeRun4_4: CheckRun = { + pluginName: 'f4', + internalRunId: 'f4', + checkName: 'FAKE Elimination Long Long Long Long Long', + checkDescription: 'Shows you the possible eliminations.', + checkLink: 'https://www.google.com', + status: RunStatus.COMPLETED, + statusDescription: 'Everything was eliminated already.', + statusLink: 'https://www.google.com', + attempt: 40, + scheduledTimestamp: new Date('2021-04-02T03:14:15'), + startedTimestamp: new Date('2021-04-02T04:24:25'), + finishedTimestamp: new Date('2021-04-02T04:25:44'), + isSingleAttempt: false, + isLatestAttempt: true, + attemptDetails: [], + results: [ + { + internalResultId: 'f44r0', + category: Category.INFO, + summary: 'Dont be afraid. All TODOs will be eliminated.', + actions: [ + { + name: 'Re-Run', + tooltip: 'More powerful run than before with a long tooltip, really.', + primary: true, + callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), + }, + ], + }, + ], + actions: [ + { + name: 'Re-Run', + tooltip: 'small', + primary: true, + callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), + }, + ], +}; + +export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] { + const runs: CheckRun[] = []; + for (let i = from; i < to; i++) { + runs.push(fakeRun4CreateAttempt(i)); + } + return runs; +} + +export function fakeRun4CreateAttempt(attempt: number): CheckRun { + return { + pluginName: 'f4', + internalRunId: 'f4', + checkName: 'FAKE Elimination Long Long Long Long Long', + status: RunStatus.COMPLETED, + attempt, + isSingleAttempt: false, + isLatestAttempt: false, + attemptDetails: [], + results: + attempt % 2 === 0 + ? [ + { + internalResultId: 'f43r0', + category: Category.ERROR, + summary: + 'Without eliminating all the TODOs your change will break!', + }, + ] + : [], + }; +} + +export const fakeRun4Att = [ + fakeRun4_1, + fakeRun4_2, + fakeRun4_3, + ...fakeRun4CreateAttempts(5, 40), + fakeRun4_4, +]; + +export const fakeActions: Action[] = [ + { + name: 'Fake Action 1', + primary: true, + disabled: true, + tooltip: 'Tooltip for Fake Action 1', + callback: () => Promise.resolve({message: 'fake action 1 triggered'}), + }, + { + name: 'Fake Action 2', + primary: false, + disabled: true, + tooltip: 'Tooltip for Fake Action 2', + callback: () => Promise.resolve({message: 'fake action 2 triggered'}), + }, + { + name: 'Fake Action 3', + summary: true, + primary: false, + tooltip: 'Tooltip for Fake Action 3', + callback: () => Promise.resolve({message: 'fake action 3 triggered'}), + }, +]; + +export const fakeLinks: Link[] = [ + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Bug Report 1', + icon: LinkIcon.REPORT_BUG, + }, + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Bug Report 2', + icon: LinkIcon.REPORT_BUG, + }, + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Link 1', + icon: LinkIcon.EXTERNAL, + }, + { + url: 'https://www.google.com', + primary: false, + tooltip: 'Fake Link 2', + icon: LinkIcon.EXTERNAL, + }, + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Code Link', + icon: LinkIcon.CODE, + }, + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Image Link', + icon: LinkIcon.IMAGE, + }, + { + url: 'https://www.google.com', + primary: true, + tooltip: 'Fake Help Link', + icon: LinkIcon.HELP_PAGE, + }, +];
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts index bb6809f..6036d07 100644 --- a/polygerrit-ui/app/services/checks/checks-model.ts +++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -14,23 +14,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import {BehaviorSubject, Observable} from 'rxjs'; +import {AttemptDetail, createAttemptMap} from './checks-util'; +import {assertIsDefined} from '../../utils/common-util'; +import {select} from '../../utils/observable-util'; +import {Finalizable} from '../registry'; +import { + BehaviorSubject, + combineLatest, + from, + Observable, + of, + Subject, + Subscription, + timer, +} from 'rxjs'; +import { + catchError, + filter, + switchMap, + takeUntil, + takeWhile, + throttleTime, + withLatestFrom, +} from 'rxjs/operators'; import { Action, - Category, CheckResult as CheckResultApi, CheckRun as CheckRunApi, Link, - LinkIcon, - RunStatus, - TagColor, + ChangeData, + ChecksApiConfig, + ChecksProvider, + FetchResponse, + ResponseCode, } from '../../api/checks'; -import {distinctUntilChanged, map} from 'rxjs/operators'; -import {PatchSetNumber} from '../../types/common'; -import {AttemptDetail, createAttemptMap} from './checks-util'; -import {assertIsDefined} from '../../utils/common-util'; -import {deepEqual} from '../../utils/deep-util'; +import {change$, changeNum$, latestPatchNum$} from '../change/change-model'; +import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common'; +import {getCurrentRevision} from '../../utils/change-util'; +import {getShaByPatchNum} from '../../utils/patch-set-util'; +import {ReportingService} from '../gr-reporting/gr-reporting'; +import {routerPatchNum$} from '../router/router-model'; +import {Execution} from '../../constants/reporting'; +import {fireAlert, fireEvent} from '../../utils/event-util'; /** * The checks model maintains the state of checks for two patchsets: the latest @@ -83,7 +108,7 @@ // properties. So you can just combine them with {...run, ...result}. export type RunResult = CheckRun & CheckResult; -interface ChecksProviderState { +export interface ChecksProviderState { pluginName: string; loading: boolean; /** @@ -121,117 +146,101 @@ }; } -const initialState: ChecksState = { - pluginStateLatest: {}, - pluginStateSelected: {}, -}; - -const privateState$ = new BehaviorSubject(initialState); - -export function _testOnly_resetState() { - // We cannot assign a new subject to privateState$, because all the selectors - // have already subscribed to the original subject. So we have to emit the - // initial state on the existing subject. - privateState$.next({...initialState}); -} - -export function _testOnly_setState(state: ChecksState) { - privateState$.next(state); -} - -export function _testOnly_getState() { - return privateState$.getValue(); -} - -// Re-exporting as Observable so that you can only subscribe, but not emit. -export const checksState$: Observable<ChecksState> = privateState$; - -export const checksSelectedPatchsetNumber$ = checksState$.pipe( - map(state => state.patchsetNumberSelected), - distinctUntilChanged() -); - -export const checksLatest$ = checksState$.pipe( - map(state => state.pluginStateLatest), - distinctUntilChanged() -); - -export const checksSelected$ = checksState$.pipe( - map(state => - state.patchsetNumberSelected - ? state.pluginStateSelected - : state.pluginStateLatest - ), - distinctUntilChanged() -); - -export const aPluginHasRegistered$ = checksLatest$.pipe( - map(state => Object.keys(state).length > 0), - distinctUntilChanged() -); - -export const someProvidersAreLoadingFirstTime$ = checksLatest$.pipe( - map(state => - Object.values(state).some( - provider => provider.loading && provider.firstTimeLoad - ) - ), - distinctUntilChanged() -); - -export const someProvidersAreLoadingLatest$ = checksLatest$.pipe( - map(state => - Object.values(state).some(providerState => providerState.loading) - ), - distinctUntilChanged() -); - -export const someProvidersAreLoadingSelected$ = checksSelected$.pipe( - map(state => - Object.values(state).some(providerState => providerState.loading) - ), - distinctUntilChanged() -); - -export const errorMessageLatest$ = checksLatest$.pipe( - map( - state => - Object.values(state).find( - providerState => providerState.errorMessage !== undefined - )?.errorMessage - ), - distinctUntilChanged() -); - export interface ErrorMessages { /* Maps plugin name to error message. */ [name: string]: string; } -export const errorMessagesLatest$ = checksLatest$.pipe( - map(state => { +export class ChecksModel implements Finalizable { + private readonly providers: {[name: string]: ChecksProvider} = {}; + + private readonly reloadSubjects: {[name: string]: Subject<void>} = {}; + + private checkToPluginMap = new Map<string, string>(); + + private changeNum?: NumericChangeId; + + private latestPatchNum?: PatchSetNumber; + + private readonly documentVisibilityChange$ = new BehaviorSubject(undefined); + + private readonly reloadListener: () => void; + + private readonly visibilityChangeListener: () => void; + + private subscriptions: Subscription[] = []; + + private readonly privateState$ = new BehaviorSubject<ChecksState>({ + pluginStateLatest: {}, + pluginStateSelected: {}, + }); + + public checksState$: Observable<ChecksState> = + this.privateState$.asObservable(); + + public checksSelectedPatchsetNumber$ = select( + this.checksState$, + state => state.patchsetNumberSelected + ); + + public checksLatest$ = select( + this.checksState$, + state => state.pluginStateLatest + ); + + public checksSelected$ = select(this.checksState$, state => + state.patchsetNumberSelected + ? state.pluginStateSelected + : state.pluginStateLatest + ); + + public aPluginHasRegistered$ = select( + this.checksLatest$, + state => Object.keys(state).length > 0 + ); + + public someProvidersAreLoadingFirstTime$ = select(this.checksLatest$, state => + Object.values(state).some( + provider => provider.loading && provider.firstTimeLoad + ) + ); + + public someProvidersAreLoadingLatest$ = select(this.checksLatest$, state => + Object.values(state).some(providerState => providerState.loading) + ); + + public someProvidersAreLoadingSelected$ = select( + this.checksSelected$, + state => Object.values(state).some(providerState => providerState.loading) + ); + + public errorMessageLatest$ = select( + this.checksLatest$, + + state => + Object.values(state).find( + providerState => providerState.errorMessage !== undefined + )?.errorMessage + ); + + public errorMessagesLatest$ = select(this.checksLatest$, state => { const errorMessages: ErrorMessages = {}; for (const providerState of Object.values(state)) { if (providerState.errorMessage === undefined) continue; errorMessages[providerState.pluginName] = providerState.errorMessage; } return errorMessages; - }), - distinctUntilChanged(deepEqual) -); + }); -export const loginCallbackLatest$ = checksLatest$.pipe( - map( + public loginCallbackLatest$ = select( + this.checksLatest$, state => Object.values(state).find( providerState => providerState.loginCallback !== undefined )?.loginCallback - ), - distinctUntilChanged() -); + ); -export const topLevelActionsLatest$ = checksLatest$.pipe( - map(state => + public topLevelActionsLatest$ = select(this.checksLatest$, state => Object.values(state).reduce( (allActions: Action[], providerState: ChecksProviderState) => [ ...allActions, @@ -239,12 +248,9 @@ ], [] ) - ), - distinctUntilChanged<Action[]>(deepEqual) -); + ); -export const topLevelActionsSelected$ = checksSelected$.pipe( - map(state => + public topLevelActionsSelected$ = select(this.checksSelected$, state => Object.values(state).reduce( (allActions: Action[], providerState: ChecksProviderState) => [ ...allActions, @@ -252,12 +258,9 @@ ], [] ) - ), - distinctUntilChanged<Action[]>(deepEqual) -); + ); -export const topLevelLinksSelected$ = checksSelected$.pipe( - map(state => + public topLevelLinksSelected$ = select(this.checksSelected$, state => Object.values(state).reduce( (allLinks: Link[], providerState: ChecksProviderState) => [ ...allLinks, @@ -265,12 +268,9 @@ ], [] ) - ), - distinctUntilChanged<Link[]>(deepEqual) -); + ); -export const allRunsLatestPatchset$ = checksLatest$.pipe( - map(state => + public allRunsLatestPatchset$ = select(this.checksLatest$, state => Object.values(state).reduce( (allRuns: CheckRun[], providerState: ChecksProviderState) => [ ...allRuns, @@ -278,12 +278,9 @@ ], [] ) - ), - distinctUntilChanged<CheckRun[]>(deepEqual) -); + ); -export const allRunsSelectedPatchset$ = checksSelected$.pipe( - map(state => + public allRunsSelectedPatchset$ = select(this.checksSelected$, state => Object.values(state).reduce( (allRuns: CheckRun[], providerState: ChecksProviderState) => [ ...allRuns, @@ -291,16 +288,14 @@ ], [] ) - ), - distinctUntilChanged<CheckRun[]>(deepEqual) -); + ); -export const allRunsLatestPatchsetLatestAttempt$ = allRunsLatestPatchset$.pipe( - map(runs => runs.filter(run => run.isLatestAttempt)) -); + public allRunsLatestPatchsetLatestAttempt$ = select( + this.allRunsLatestPatchset$, + runs => runs.filter(run => run.isLatestAttempt) + ); -export const checkToPluginMap$ = checksLatest$.pipe( - map(state => { + public checkToPluginMap$ = select(this.checksLatest$, state => { const map = new Map<string, string>(); for (const [pluginName, providerState] of Object.entries(state)) { for (const run of providerState.runs) { @@ -308,11 +303,9 @@ } } return map; - }) -); + }); -export const allResultsSelected$ = checksSelected$.pipe( - map(state => + public allResultsSelected$ = select(this.checksSelected$, state => Object.values(state) .reduce( (allResults: CheckResult[], providerState: ChecksProviderState) => [ @@ -326,569 +319,436 @@ [] ) .filter(r => r !== undefined) - ) -); + ); -// Must only be used by the checks service or whatever is in control of this -// model. -export function updateStateSetProvider( - pluginName: string, - patchset: ChecksPatchset -) { - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - pluginState[pluginName] = { - pluginName, - loading: false, - firstTimeLoad: true, - runs: [], - actions: [], - links: [], - }; - privateState$.next(nextState); -} - -// TODO(brohlfs): Remove all fake runs once the Checks UI is fully launched. -// They are just making it easier to develop the UI and always see all the -// different types/states of runs and results. - -export const fakeRun0: CheckRun = { - pluginName: 'f0', - internalRunId: 'f0', - checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder', - labelName: 'Presubmit', - isSingleAttempt: true, - isLatestAttempt: true, - attemptDetails: [], - results: [ - { - internalResultId: 'f0r0', - category: Category.ERROR, - summary: 'I would like to point out this error: 1 is not equal to 2!', - links: [ - {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL}, - ], - tags: [{name: 'OBSOLETE'}, {name: 'E2E'}], - }, - { - internalResultId: 'f0r1', - category: Category.ERROR, - summary: 'Running the mighty test has failed by crashing.', - message: 'Btw, 1 is also not equal to 3. Did you know?', - actions: [ - { - name: 'Ignore', - tooltip: 'Ignore this result', - primary: true, - callback: () => Promise.resolve({message: 'fake "ignore" triggered'}), - }, - { - name: 'Flag', - tooltip: 'Flag this result as totally absolutely really not useful', - primary: true, - disabled: true, - callback: () => Promise.resolve({message: 'flag "flag" triggered'}), - }, - { - name: 'Upload', - tooltip: 'Upload the result to the super cloud.', - primary: false, - callback: () => Promise.resolve({message: 'fake "upload" triggered'}), - }, - ], - tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}], - links: [ - {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL}, - {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD}, - { - primary: true, - url: 'https://google.com', - icon: LinkIcon.DOWNLOAD_MOBILE, - }, - {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, - {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, - {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE}, - {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG}, - {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE}, - {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY}, - ], - }, - ], - status: RunStatus.COMPLETED, -}; - -export const fakeRun1: CheckRun = { - pluginName: 'f1', - internalRunId: 'f1', - checkName: 'FAKE Super Check', - statusLink: 'https://www.google.com/', - patchset: 1, - labelName: 'Verified', - isSingleAttempt: true, - isLatestAttempt: true, - attemptDetails: [], - results: [ - { - internalResultId: 'f1r0', - category: Category.WARNING, - summary: 'We think that you could improve this.', - message: `There is a lot to be said. A lot. I say, a lot.\n - So please keep reading.`, - tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}], - codePointers: [ - { - path: '/COMMIT_MSG', - range: { - start_line: 10, - start_character: 0, - end_line: 10, - end_character: 0, - }, - }, - { - path: 'polygerrit-ui/app/api/checks.ts', - range: { - start_line: 5, - start_character: 0, - end_line: 7, - end_character: 0, - }, - }, - ], - links: [ - {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL}, - {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD}, - { - primary: true, - url: 'https://google.com', - icon: LinkIcon.DOWNLOAD_MOBILE, - }, - {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE}, - { - primary: false, - url: 'https://google.com', - tooltip: 'look at this', - icon: LinkIcon.IMAGE, - }, - { - primary: false, - url: 'https://google.com', - tooltip: 'not at this', - icon: LinkIcon.IMAGE, - }, - ], - }, - ], - status: RunStatus.RUNNING, -}; - -export const fakeRun2: CheckRun = { - pluginName: 'f2', - internalRunId: 'f2', - checkName: 'FAKE Mega Analysis', - statusDescription: 'This run is nearly completed, but not quite.', - statusLink: 'https://www.google.com/', - checkDescription: - 'From what the title says you can tell that this check analyses.', - checkLink: 'https://www.google.com/', - scheduledTimestamp: new Date('2021-04-01T03:14:15'), - startedTimestamp: new Date('2021-04-01T04:24:25'), - finishedTimestamp: new Date('2021-04-01T04:44:44'), - isSingleAttempt: true, - isLatestAttempt: true, - attemptDetails: [], - actions: [ - { - name: 'Re-Run', - tooltip: 'More powerful run than before', - primary: true, - callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), - }, - { - name: 'Monetize', - primary: true, - disabled: true, - callback: () => Promise.resolve({message: 'fake "monetize" triggered'}), - }, - { - name: 'Delete', - primary: true, - callback: () => Promise.resolve({message: 'fake "delete" triggered'}), - }, - ], - results: [ - { - internalResultId: 'f2r0', - category: Category.INFO, - summary: 'This is looking a bit too large.', - message: `We are still looking into how large exactly. Stay tuned. -And have a look at https://www.google.com! - -Or have a look at change 30000. -Example code: - const constable = ''; - var variable = '';`, - tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}], - }, - ], - status: RunStatus.COMPLETED, -}; - -export const fakeRun3: CheckRun = { - pluginName: 'f3', - internalRunId: 'f3', - checkName: 'FAKE Critical Observations', - status: RunStatus.RUNNABLE, - isSingleAttempt: true, - isLatestAttempt: true, - attemptDetails: [], -}; - -export const fakeRun4_1: CheckRun = { - pluginName: 'f4', - internalRunId: 'f4', - checkName: 'FAKE Elimination Long Long Long Long Long', - status: RunStatus.RUNNABLE, - attempt: 1, - isSingleAttempt: false, - isLatestAttempt: false, - attemptDetails: [], -}; - -export const fakeRun4_2: CheckRun = { - pluginName: 'f4', - internalRunId: 'f4', - checkName: 'FAKE Elimination Long Long Long Long Long', - status: RunStatus.COMPLETED, - attempt: 2, - isSingleAttempt: false, - isLatestAttempt: false, - attemptDetails: [], - results: [ - { - internalResultId: 'f42r0', - category: Category.INFO, - summary: 'Please eliminate all the TODOs!', - }, - ], -}; - -export const fakeRun4_3: CheckRun = { - pluginName: 'f4', - internalRunId: 'f4', - checkName: 'FAKE Elimination Long Long Long Long Long', - status: RunStatus.COMPLETED, - attempt: 3, - isSingleAttempt: false, - isLatestAttempt: false, - attemptDetails: [], - results: [ - { - internalResultId: 'f43r0', - category: Category.ERROR, - summary: 'Without eliminating all the TODOs your change will break!', - }, - ], -}; - -export const fakeRun4_4: CheckRun = { - pluginName: 'f4', - internalRunId: 'f4', - checkName: 'FAKE Elimination Long Long Long Long Long', - checkDescription: 'Shows you the possible eliminations.', - checkLink: 'https://www.google.com', - status: RunStatus.COMPLETED, - statusDescription: 'Everything was eliminated already.', - statusLink: 'https://www.google.com', - attempt: 40, - scheduledTimestamp: new Date('2021-04-02T03:14:15'), - startedTimestamp: new Date('2021-04-02T04:24:25'), - finishedTimestamp: new Date('2021-04-02T04:25:44'), - isSingleAttempt: false, - isLatestAttempt: true, - attemptDetails: [], - results: [ - { - internalResultId: 'f44r0', - category: Category.INFO, - summary: 'Dont be afraid. All TODOs will be eliminated.', - actions: [ - { - name: 'Re-Run', - tooltip: 'More powerful run than before with a long tooltip, really.', - primary: true, - callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), - }, - ], - }, - ], - actions: [ - { - name: 'Re-Run', - tooltip: 'small', - primary: true, - callback: () => Promise.resolve({message: 'fake "re-run" triggered'}), - }, - ], -}; - -export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] { - const runs: CheckRun[] = []; - for (let i = from; i < to; i++) { - runs.push(fakeRun4CreateAttempt(i)); + constructor(readonly reporting: ReportingService) { + this.subscriptions = [ + changeNum$.subscribe(x => (this.changeNum = x)), + this.checkToPluginMap$.subscribe(map => { + this.checkToPluginMap = map; + }), + combineLatest([routerPatchNum$, latestPatchNum$]).subscribe( + ([routerPs, latestPs]) => { + this.latestPatchNum = latestPs; + if (latestPs === undefined) { + this.setPatchset(undefined); + } else if (typeof routerPs === 'number') { + this.setPatchset(routerPs); + } else { + this.setPatchset(latestPs); + } + } + ), + ]; + this.visibilityChangeListener = () => { + this.documentVisibilityChange$.next(undefined); + }; + document.addEventListener( + 'visibilitychange', + this.visibilityChangeListener + ); + this.reloadListener = () => this.reloadAll(); + document.addEventListener('reload', this.reloadListener); } - return runs; -} -export function fakeRun4CreateAttempt(attempt: number): CheckRun { - return { - pluginName: 'f4', - internalRunId: 'f4', - checkName: 'FAKE Elimination Long Long Long Long Long', - status: RunStatus.COMPLETED, - attempt, - isSingleAttempt: false, - isLatestAttempt: false, - attemptDetails: [], - results: - attempt % 2 === 0 - ? [ - { - internalResultId: 'f43r0', - category: Category.ERROR, - summary: - 'Without eliminating all the TODOs your change will break!', - }, - ] - : [], - }; -} - -export const fakeRun4Att = [ - fakeRun4_1, - fakeRun4_2, - fakeRun4_3, - ...fakeRun4CreateAttempts(5, 40), - fakeRun4_4, -]; - -export const fakeActions: Action[] = [ - { - name: 'Fake Action 1', - primary: true, - disabled: true, - tooltip: 'Tooltip for Fake Action 1', - callback: () => Promise.resolve({message: 'fake action 1 triggered'}), - }, - { - name: 'Fake Action 2', - primary: false, - disabled: true, - tooltip: 'Tooltip for Fake Action 2', - callback: () => Promise.resolve({message: 'fake action 2 triggered'}), - }, - { - name: 'Fake Action 3', - summary: true, - primary: false, - tooltip: 'Tooltip for Fake Action 3', - callback: () => Promise.resolve({message: 'fake action 3 triggered'}), - }, -]; - -export const fakeLinks: Link[] = [ - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Bug Report 1', - icon: LinkIcon.REPORT_BUG, - }, - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Bug Report 2', - icon: LinkIcon.REPORT_BUG, - }, - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Link 1', - icon: LinkIcon.EXTERNAL, - }, - { - url: 'https://www.google.com', - primary: false, - tooltip: 'Fake Link 2', - icon: LinkIcon.EXTERNAL, - }, - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Code Link', - icon: LinkIcon.CODE, - }, - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Image Link', - icon: LinkIcon.IMAGE, - }, - { - url: 'https://www.google.com', - primary: true, - tooltip: 'Fake Help Link', - icon: LinkIcon.HELP_PAGE, - }, -]; - -export function getPluginState( - state: ChecksState, - patchset: ChecksPatchset = ChecksPatchset.LATEST -) { - if (patchset === ChecksPatchset.LATEST) { - state.pluginStateLatest = {...state.pluginStateLatest}; - return state.pluginStateLatest; - } else { - state.pluginStateSelected = {...state.pluginStateSelected}; - return state.pluginStateSelected; + finalize() { + document.removeEventListener('reload', this.reloadListener); + document.removeEventListener( + 'visibilitychange', + this.visibilityChangeListener + ); + for (const s of this.subscriptions) { + s.unsubscribe(); + } + this.subscriptions = []; + this.privateState$.complete(); } -} -export function updateStateSetLoading( - pluginName: string, - patchset: ChecksPatchset -) { - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - pluginState[pluginName] = { - ...pluginState[pluginName], - loading: true, - }; - privateState$.next(nextState); -} - -export function updateStateSetError( - pluginName: string, - errorMessage: string, - patchset: ChecksPatchset -) { - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - pluginState[pluginName] = { - ...pluginState[pluginName], - loading: false, - firstTimeLoad: false, - errorMessage, - loginCallback: undefined, - runs: [], - actions: [], - }; - privateState$.next(nextState); -} - -export function updateStateSetNotLoggedIn( - pluginName: string, - loginCallback: () => void, - patchset: ChecksPatchset -) { - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - pluginState[pluginName] = { - ...pluginState[pluginName], - loading: false, - firstTimeLoad: false, - errorMessage: undefined, - loginCallback, - runs: [], - actions: [], - }; - privateState$.next(nextState); -} - -export function updateStateSetResults( - pluginName: string, - runs: CheckRunApi[], - actions: Action[] = [], - links: Link[] = [], - patchset: ChecksPatchset -) { - 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)); + // Must only be used by the checks service or whatever is in control of this + // model. + updateStateSetProvider(pluginName: string, patchset: ChecksPatchset) { + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + pluginState[pluginName] = { + pluginName, + loading: false, + firstTimeLoad: true, + runs: [], + actions: [], + links: [], + }; + this.privateState$.next(nextState); } - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - pluginState[pluginName] = { - ...pluginState[pluginName], - loading: false, - firstTimeLoad: false, - errorMessage: undefined, - loginCallback: undefined, - runs: runs.map(run => { - const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`; - const attemptInfo = attemptMap.get(run.checkName); - assertIsDefined(attemptInfo, 'attemptInfo'); - return { - ...run, - pluginName, - internalRunId: runId, - isLatestAttempt: attemptInfo.latestAttempt === run.attempt, - isSingleAttempt: attemptInfo.isSingleAttempt, - attemptDetails: attemptInfo.attempts, - results: (run.results ?? []).map((result, i) => { - return { - ...result, - internalResultId: `${runId}-${i}`, - }; - }), - }; - }), - actions: [...actions], - links: [...links], - }; - privateState$.next(nextState); -} -export function updateStateUpdateResult( - pluginName: string, - updatedRun: CheckRunApi, - updatedResult: CheckResultApi, - patchset: ChecksPatchset -) { - const nextState = {...privateState$.getValue()}; - const pluginState = getPluginState(nextState, patchset); - let runUpdated = false; - const runs: CheckRun[] = pluginState[pluginName].runs.map(run => { - if (run.change !== updatedRun.change) return run; - if (run.patchset !== updatedRun.patchset) return run; - if (run.attempt !== updatedRun.attempt) return run; - if (run.checkName !== updatedRun.checkName) return run; - let resultUpdated = false; - const results: CheckResult[] = (run.results ?? []).map(result => { - if (result.externalId && result.externalId === updatedResult.externalId) { - runUpdated = true; - resultUpdated = true; + getPluginState( + state: ChecksState, + patchset: ChecksPatchset = ChecksPatchset.LATEST + ) { + if (patchset === ChecksPatchset.LATEST) { + state.pluginStateLatest = {...state.pluginStateLatest}; + return state.pluginStateLatest; + } else { + state.pluginStateSelected = {...state.pluginStateSelected}; + return state.pluginStateSelected; + } + } + + updateStateSetLoading(pluginName: string, patchset: ChecksPatchset) { + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + pluginState[pluginName] = { + ...pluginState[pluginName], + loading: true, + }; + this.privateState$.next(nextState); + } + + updateStateSetError( + pluginName: string, + errorMessage: string, + patchset: ChecksPatchset + ) { + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + pluginState[pluginName] = { + ...pluginState[pluginName], + loading: false, + firstTimeLoad: false, + errorMessage, + loginCallback: undefined, + runs: [], + actions: [], + }; + this.privateState$.next(nextState); + } + + updateStateSetNotLoggedIn( + pluginName: string, + loginCallback: () => void, + patchset: ChecksPatchset + ) { + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + pluginState[pluginName] = { + ...pluginState[pluginName], + loading: false, + firstTimeLoad: false, + errorMessage: undefined, + loginCallback, + runs: [], + actions: [], + }; + this.privateState$.next(nextState); + } + + updateStateSetResults( + pluginName: string, + runs: CheckRunApi[], + actions: Action[] = [], + links: Link[] = [], + patchset: ChecksPatchset + ) { + 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) + ); + } + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + pluginState[pluginName] = { + ...pluginState[pluginName], + loading: false, + firstTimeLoad: false, + errorMessage: undefined, + loginCallback: undefined, + runs: runs.map(run => { + const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`; + const attemptInfo = attemptMap.get(run.checkName); + assertIsDefined(attemptInfo, 'attemptInfo'); return { - ...updatedResult, - internalResultId: result.internalResultId, + ...run, + pluginName, + internalRunId: runId, + isLatestAttempt: attemptInfo.latestAttempt === run.attempt, + isSingleAttempt: attemptInfo.isSingleAttempt, + attemptDetails: attemptInfo.attempts, + results: (run.results ?? []).map((result, i) => { + return { + ...result, + internalResultId: `${runId}-${i}`, + }; + }), }; - } - return result; - }); - return resultUpdated ? {...run, results} : run; - }); - if (!runUpdated) return; - pluginState[pluginName] = { - ...pluginState[pluginName], - runs, - }; - privateState$.next(nextState); -} + }), + actions: [...actions], + links: [...links], + }; + this.privateState$.next(nextState); + } -export function updateStateSetPatchset(patchsetNumber?: PatchSetNumber) { - const nextState = {...privateState$.getValue()}; - nextState.patchsetNumberSelected = patchsetNumber; - privateState$.next(nextState); + updateStateUpdateResult( + pluginName: string, + updatedRun: CheckRunApi, + updatedResult: CheckResultApi, + patchset: ChecksPatchset + ) { + const nextState = {...this.privateState$.getValue()}; + const pluginState = this.getPluginState(nextState, patchset); + let runUpdated = false; + const runs: CheckRun[] = pluginState[pluginName].runs.map(run => { + if (run.change !== updatedRun.change) return run; + if (run.patchset !== updatedRun.patchset) return run; + if (run.attempt !== updatedRun.attempt) return run; + if (run.checkName !== updatedRun.checkName) return run; + let resultUpdated = false; + const results: CheckResult[] = (run.results ?? []).map(result => { + if ( + result.externalId && + result.externalId === updatedResult.externalId + ) { + runUpdated = true; + resultUpdated = true; + return { + ...updatedResult, + internalResultId: result.internalResultId, + }; + } + return result; + }); + return resultUpdated ? {...run, results} : run; + }); + if (!runUpdated) return; + pluginState[pluginName] = { + ...pluginState[pluginName], + runs, + }; + this.privateState$.next(nextState); + } + + updateStateSetPatchset(patchsetNumber?: PatchSetNumber) { + const nextState = {...this.privateState$.getValue()}; + nextState.patchsetNumberSelected = patchsetNumber; + this.privateState$.next(nextState); + } + + setPatchset(num?: PatchSetNumber) { + this.updateStateSetPatchset(num === this.latestPatchNum ? undefined : num); + } + + reload(pluginName: string) { + this.reloadSubjects[pluginName].next(); + } + + reloadAll() { + for (const key of Object.keys(this.providers)) { + this.reload(key); + } + } + + reloadForCheck(checkName?: string) { + if (!checkName) return; + const plugin = this.checkToPluginMap.get(checkName); + if (plugin) this.reload(plugin); + } + + updateResult(pluginName: string, run: CheckRunApi, result: CheckResultApi) { + this.updateStateUpdateResult( + pluginName, + run, + result, + ChecksPatchset.LATEST + ); + this.updateStateUpdateResult( + pluginName, + run, + result, + ChecksPatchset.SELECTED + ); + } + + triggerAction(action?: Action, run?: CheckRun) { + if (!action?.callback) return; + if (!this.changeNum) return; + const patchSet = run?.patchset ?? this.latestPatchNum; + if (!patchSet) return; + const promise = action.callback( + this.changeNum, + patchSet, + run?.attempt, + run?.externalId, + run?.checkName, + action.name + ); + // If plugins return undefined or not a promise, then show no toast. + if (!promise?.then) return; + + fireAlert(document, `Triggering action '${action.name}' ...`); + from(promise) + // If the action takes longer than 5 seconds, then most likely the + // user is either not interested or the result not relevant anymore. + .pipe(takeUntil(timer(5000))) + .subscribe(result => { + if (result.errorMessage || result.message) { + fireAlert(document, `${result.message ?? result.errorMessage}`); + } else { + fireEvent(document, 'hide-alert'); + } + if (result.shouldReload) { + this.reloadForCheck(run?.checkName); + } + }); + } + + register( + pluginName: string, + provider: ChecksProvider, + config: ChecksApiConfig + ) { + if (this.providers[pluginName]) { + console.warn( + `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.` + ); + return; + } + this.providers[pluginName] = provider; + this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined); + this.updateStateSetProvider(pluginName, ChecksPatchset.LATEST); + this.updateStateSetProvider(pluginName, ChecksPatchset.SELECTED); + this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST); + this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED); + } + + initFetchingOfData( + pluginName: string, + config: ChecksApiConfig, + patchset: ChecksPatchset + ) { + const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000; + // Various events should trigger fetching checks from the provider: + // 1. Change number and patchset number changes. + // 2. Specific reload requests. + // 3. Regular polling starting with an initial fetch right now. + // 4. A hidden Gerrit tab becoming visible. + this.subscriptions.push( + combineLatest([ + changeNum$, + patchset === ChecksPatchset.LATEST + ? latestPatchNum$ + : this.checksSelectedPatchsetNumber$, + this.reloadSubjects[pluginName].pipe(throttleTime(1000)), + timer(0, pollIntervalMs), + this.documentVisibilityChange$, + ]) + .pipe( + takeWhile(_ => !!this.providers[pluginName]), + filter(_ => document.visibilityState !== 'hidden'), + withLatestFrom(change$), + switchMap( + ([[changeNum, patchNum], change]): Observable<FetchResponse> => { + if (!change || !changeNum || !patchNum) return of(this.empty()); + if (typeof patchNum !== 'number') return of(this.empty()); + assertIsDefined(change.revisions, 'change.revisions'); + const patchsetSha = getShaByPatchNum(change.revisions, patchNum); + // Sometimes patchNum is updated earlier than change, so change + // revisions don't have patchNum yet + if (!patchsetSha) return of(this.empty()); + const data: ChangeData = { + changeNumber: changeNum, + patchsetNumber: patchNum, + patchsetSha, + repo: change.project, + commitMessage: getCurrentRevision(change)?.commit?.message, + changeInfo: change as ChangeInfo, + }; + return this.fetchResults(pluginName, data, patchset); + } + ), + catchError(e => { + // This should not happen and is really severe, because it means that + // the Observable has terminated and we won't recover from that. No + // further attempts to fetch results for this plugin will be made. + this.reporting.error(e, `checks-model crash for ${pluginName}`); + return of(this.createErrorResponse(pluginName, e)); + }) + ) + .subscribe(response => { + switch (response.responseCode) { + case ResponseCode.ERROR: { + const message = response.errorMessage ?? '-'; + this.reporting.reportExecution(Execution.CHECKS_API_ERROR, { + plugin: pluginName, + message, + }); + this.updateStateSetError(pluginName, message, patchset); + break; + } + case ResponseCode.NOT_LOGGED_IN: { + assertIsDefined(response.loginCallback, 'loginCallback'); + this.reporting.reportExecution( + Execution.CHECKS_API_NOT_LOGGED_IN, + { + plugin: pluginName, + } + ); + this.updateStateSetNotLoggedIn( + pluginName, + response.loginCallback, + patchset + ); + break; + } + case ResponseCode.OK: { + this.updateStateSetResults( + pluginName, + response.runs ?? [], + response.actions ?? [], + response.links ?? [], + patchset + ); + break; + } + } + }) + ); + } + + private empty(): FetchResponse { + return { + responseCode: ResponseCode.OK, + runs: [], + }; + } + + private createErrorResponse( + pluginName: string, + message: object + ): FetchResponse { + return { + responseCode: ResponseCode.ERROR, + errorMessage: + `Error message from plugin '${pluginName}':` + + ` ${JSON.stringify(message)}`, + }; + } + + private fetchResults( + pluginName: string, + data: ChangeData, + patchset: ChecksPatchset + ): Observable<FetchResponse> { + this.updateStateSetLoading(pluginName, patchset); + const timer = this.reporting.getTimer('ChecksPluginFetch'); + const fetchPromise = this.providers[pluginName] + .fetch(data) + .then(response => { + timer.end({pluginName}); + return response; + }); + return from(fetchPromise).pipe( + catchError(e => of(this.createErrorResponse(pluginName, e))) + ); + } }
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts index 0be0451..390f6ad 100644 --- a/polygerrit-ui/app/services/checks/checks-model_test.ts +++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -16,15 +16,9 @@ */ import '../../test/common-test-setup-karma'; import './checks-model'; -import { - _testOnly_getState, - ChecksPatchset, - updateStateSetLoading, - updateStateSetProvider, - updateStateSetResults, - updateStateUpdateResult, -} from './checks-model'; +import {ChecksModel, ChecksPatchset, ChecksProviderState} from './checks-model'; import {Category, CheckRun, RunStatus} from '../../api/checks'; +import {grReportingMock} from '../gr-reporting/gr-reporting_mock'; const PLUGIN_NAME = 'test-plugin'; @@ -45,14 +39,23 @@ }, ]; -function current() { - return _testOnly_getState().pluginStateLatest[PLUGIN_NAME]; -} - suite('checks-model tests', () => { - test('updateStateSetProvider', () => { - updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST); - assert.deepEqual(current(), { + let model: ChecksModel; + + let current: ChecksProviderState; + + setup(() => { + model = new ChecksModel(grReportingMock); + model.checksLatest$.subscribe(c => (current = c[PLUGIN_NAME])); + }); + + teardown(() => { + model.finalize(); + }); + + test('model.updateStateSetProvider', () => { + model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST); + assert.deepEqual(current, { pluginName: PLUGIN_NAME, loading: false, firstTimeLoad: true, @@ -63,45 +66,69 @@ }); test('loading and first time load', () => { - updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST); - assert.isFalse(current().loading); - assert.isTrue(current().firstTimeLoad); - updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST); - assert.isTrue(current().loading); - assert.isTrue(current().firstTimeLoad); - updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST); - assert.isFalse(current().loading); - assert.isFalse(current().firstTimeLoad); - updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST); - assert.isTrue(current().loading); - assert.isFalse(current().firstTimeLoad); - updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST); - assert.isFalse(current().loading); - assert.isFalse(current().firstTimeLoad); + model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST); + assert.isFalse(current.loading); + assert.isTrue(current.firstTimeLoad); + model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST); + assert.isTrue(current.loading); + assert.isTrue(current.firstTimeLoad); + model.updateStateSetResults( + PLUGIN_NAME, + RUNS, + [], + [], + ChecksPatchset.LATEST + ); + assert.isFalse(current.loading); + assert.isFalse(current.firstTimeLoad); + model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST); + assert.isTrue(current.loading); + assert.isFalse(current.firstTimeLoad); + model.updateStateSetResults( + PLUGIN_NAME, + RUNS, + [], + [], + ChecksPatchset.LATEST + ); + assert.isFalse(current.loading); + assert.isFalse(current.firstTimeLoad); }); - test('updateStateSetResults', () => { - updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST); - assert.lengthOf(current().runs, 1); - assert.lengthOf(current().runs[0].results!, 1); + test('model.updateStateSetResults', () => { + model.updateStateSetResults( + PLUGIN_NAME, + RUNS, + [], + [], + ChecksPatchset.LATEST + ); + assert.lengthOf(current.runs, 1); + assert.lengthOf(current.runs[0].results!, 1); }); - test('updateStateUpdateResult', () => { - updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST); + test('model.updateStateUpdateResult', () => { + model.updateStateSetResults( + PLUGIN_NAME, + RUNS, + [], + [], + ChecksPatchset.LATEST + ); assert.equal( - current().runs[0].results![0].summary, + current.runs[0].results![0].summary, RUNS[0]!.results![0].summary ); const result = RUNS[0].results![0]; const updatedResult = {...result, summary: 'new'}; - updateStateUpdateResult( + model.updateStateUpdateResult( PLUGIN_NAME, RUNS[0], updatedResult, ChecksPatchset.LATEST ); - assert.lengthOf(current().runs, 1); - assert.lengthOf(current().runs[0].results!, 1); - assert.equal(current().runs[0].results![0].summary, 'new'); + assert.lengthOf(current.runs, 1); + assert.lengthOf(current.runs[0].results!, 1); + assert.equal(current.runs[0].results![0].summary, 'new'); }); });
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts deleted file mode 100644 index 111036c..0000000 --- a/polygerrit-ui/app/services/checks/checks-service.ts +++ /dev/null
@@ -1,337 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - BehaviorSubject, - combineLatest, - from, - Observable, - of, - Subject, - Subscription, - timer, -} from 'rxjs'; -import { - catchError, - filter, - switchMap, - takeUntil, - takeWhile, - throttleTime, - withLatestFrom, -} from 'rxjs/operators'; -import { - Action, - ChangeData, - CheckResult, - CheckRun, - ChecksApiConfig, - ChecksProvider, - FetchResponse, - ResponseCode, -} from '../../api/checks'; -import {change$, changeNum$, latestPatchNum$} from '../change/change-model'; -import { - ChecksPatchset, - checksSelectedPatchsetNumber$, - checkToPluginMap$, - updateStateSetError, - updateStateSetLoading, - updateStateSetNotLoggedIn, - updateStateSetPatchset, - updateStateSetProvider, - updateStateSetResults, - updateStateUpdateResult, -} from './checks-model'; -import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common'; -import {Finalizable} from '../registry'; -import {getCurrentRevision} from '../../utils/change-util'; -import {getShaByPatchNum} from '../../utils/patch-set-util'; -import {assertIsDefined} from '../../utils/common-util'; -import {ReportingService} from '../gr-reporting/gr-reporting'; -import {routerPatchNum$} from '../router/router-model'; -import {Execution} from '../../constants/reporting'; -import {fireAlert, fireEvent} from '../../utils/event-util'; - -export class ChecksService implements Finalizable { - private readonly providers: {[name: string]: ChecksProvider} = {}; - - private readonly reloadSubjects: {[name: string]: Subject<void>} = {}; - - private checkToPluginMap = new Map<string, string>(); - - private changeNum?: NumericChangeId; - - private latestPatchNum?: PatchSetNumber; - - private readonly documentVisibilityChange$ = new BehaviorSubject(undefined); - - private readonly reloadListener: () => void; - - private readonly subscriptions: Subscription[] = []; - - private readonly visibilityChangeListener: () => void; - - constructor(readonly reporting: ReportingService) { - this.subscriptions.push(changeNum$.subscribe(x => (this.changeNum = x))); - this.subscriptions.push( - checkToPluginMap$.subscribe(map => { - this.checkToPluginMap = map; - }) - ); - this.subscriptions.push( - combineLatest([routerPatchNum$, latestPatchNum$]).subscribe( - ([routerPs, latestPs]) => { - this.latestPatchNum = latestPs; - if (latestPs === undefined) { - this.setPatchset(undefined); - } else if (typeof routerPs === 'number') { - this.setPatchset(routerPs); - } else { - this.setPatchset(latestPs); - } - } - ) - ); - this.visibilityChangeListener = () => { - this.documentVisibilityChange$.next(undefined); - }; - document.addEventListener( - 'visibilitychange', - this.visibilityChangeListener - ); - this.reloadListener = () => this.reloadAll(); - document.addEventListener('reload', this.reloadListener); - } - - finalize() { - document.removeEventListener('reload', this.reloadListener); - document.removeEventListener( - 'visibilitychange', - this.visibilityChangeListener - ); - for (const s of this.subscriptions) { - s.unsubscribe(); - } - this.subscriptions.splice(0, this.subscriptions.length); - } - - setPatchset(num?: PatchSetNumber) { - updateStateSetPatchset(num === this.latestPatchNum ? undefined : num); - } - - reload(pluginName: string) { - this.reloadSubjects[pluginName].next(); - } - - reloadAll() { - Object.keys(this.providers).forEach(key => this.reload(key)); - } - - reloadForCheck(checkName?: string) { - if (!checkName) return; - const plugin = this.checkToPluginMap.get(checkName); - if (plugin) this.reload(plugin); - } - - updateResult(pluginName: string, run: CheckRun, result: CheckResult) { - updateStateUpdateResult(pluginName, run, result, ChecksPatchset.LATEST); - updateStateUpdateResult(pluginName, run, result, ChecksPatchset.SELECTED); - } - - triggerAction(action?: Action, run?: CheckRun) { - if (!action?.callback) return; - if (!this.changeNum) return; - const patchSet = run?.patchset ?? this.latestPatchNum; - if (!patchSet) return; - const promise = action.callback( - this.changeNum, - patchSet, - run?.attempt, - run?.externalId, - run?.checkName, - action.name - ); - // If plugins return undefined or not a promise, then show no toast. - if (!promise?.then) return; - - fireAlert(document, `Triggering action '${action.name}' ...`); - from(promise) - // If the action takes longer than 5 seconds, then most likely the - // user is either not interested or the result not relevant anymore. - .pipe(takeUntil(timer(5000))) - .subscribe(result => { - if (result.errorMessage || result.message) { - fireAlert(document, `${result.message ?? result.errorMessage}`); - } else { - fireEvent(document, 'hide-alert'); - } - if (result.shouldReload) { - this.reloadForCheck(run?.checkName); - } - }); - } - - register( - pluginName: string, - provider: ChecksProvider, - config: ChecksApiConfig - ) { - if (this.providers[pluginName]) { - console.warn( - `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.` - ); - return; - } - this.providers[pluginName] = provider; - this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined); - updateStateSetProvider(pluginName, ChecksPatchset.LATEST); - updateStateSetProvider(pluginName, ChecksPatchset.SELECTED); - this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST); - this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED); - } - - initFetchingOfData( - pluginName: string, - config: ChecksApiConfig, - patchset: ChecksPatchset - ) { - const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000; - // Various events should trigger fetching checks from the provider: - // 1. Change number and patchset number changes. - // 2. Specific reload requests. - // 3. Regular polling starting with an initial fetch right now. - // 4. A hidden Gerrit tab becoming visible. - this.subscriptions.push( - combineLatest([ - changeNum$, - patchset === ChecksPatchset.LATEST - ? latestPatchNum$ - : checksSelectedPatchsetNumber$, - this.reloadSubjects[pluginName].pipe(throttleTime(1000)), - timer(0, pollIntervalMs), - this.documentVisibilityChange$, - ]) - .pipe( - takeWhile(_ => !!this.providers[pluginName]), - filter(_ => document.visibilityState !== 'hidden'), - withLatestFrom(change$), - switchMap( - ([[changeNum, patchNum], change]): Observable<FetchResponse> => { - if (!change || !changeNum || !patchNum) return of(this.empty()); - if (typeof patchNum !== 'number') return of(this.empty()); - assertIsDefined(change.revisions, 'change.revisions'); - const patchsetSha = getShaByPatchNum(change.revisions, patchNum); - // Sometimes patchNum is updated earlier than change, so change - // revisions don't have patchNum yet - if (!patchsetSha) return of(this.empty()); - const data: ChangeData = { - changeNumber: changeNum, - patchsetNumber: patchNum, - patchsetSha, - repo: change.project, - commitMessage: getCurrentRevision(change)?.commit?.message, - changeInfo: change as ChangeInfo, - }; - return this.fetchResults(pluginName, data, patchset); - } - ), - catchError(e => { - // This should not happen and is really severe, because it means that - // the Observable has terminated and we won't recover from that. No - // further attempts to fetch results for this plugin will be made. - this.reporting.error(e, `checks-service crash for ${pluginName}`); - return of(this.createErrorResponse(pluginName, e)); - }) - ) - .subscribe(response => { - switch (response.responseCode) { - case ResponseCode.ERROR: { - const message = response.errorMessage ?? '-'; - this.reporting.reportExecution(Execution.CHECKS_API_ERROR, { - plugin: pluginName, - message, - }); - updateStateSetError(pluginName, message, patchset); - break; - } - case ResponseCode.NOT_LOGGED_IN: { - assertIsDefined(response.loginCallback, 'loginCallback'); - this.reporting.reportExecution( - Execution.CHECKS_API_NOT_LOGGED_IN, - { - plugin: pluginName, - } - ); - updateStateSetNotLoggedIn( - pluginName, - response.loginCallback, - patchset - ); - break; - } - case ResponseCode.OK: { - updateStateSetResults( - pluginName, - response.runs ?? [], - response.actions ?? [], - response.links ?? [], - patchset - ); - break; - } - } - }) - ); - } - - private empty(): FetchResponse { - return { - responseCode: ResponseCode.OK, - runs: [], - }; - } - - private createErrorResponse( - pluginName: string, - message: object - ): FetchResponse { - return { - responseCode: ResponseCode.ERROR, - errorMessage: - `Error message from plugin '${pluginName}':` + - ` ${JSON.stringify(message)}`, - }; - } - - private fetchResults( - pluginName: string, - data: ChangeData, - patchset: ChecksPatchset - ): Observable<FetchResponse> { - updateStateSetLoading(pluginName, patchset); - const timer = this.reporting.getTimer('ChecksPluginFetch'); - const fetchPromise = this.providers[pluginName] - .fetch(data) - .then(response => { - timer.end({pluginName}); - return response; - }); - return from(fetchPromise).pipe( - catchError(e => of(this.createErrorResponse(pluginName, e))) - ); - } -}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts index f041354..46067c1 100644 --- a/polygerrit-ui/app/test/common-test-setup.ts +++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -49,7 +49,6 @@ import {getAppContext} from '../services/app-context'; import {_testOnly_resetState as resetChangeState} from '../services/change/change-model'; -import {_testOnly_resetState as resetChecksState} from '../services/checks/checks-model'; import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model'; import {_testOnly_resetState as resetRouterState} from '../services/router/router-model'; @@ -117,7 +116,6 @@ _testOnly_initGerritPluginApi(); resetChangeState(); - resetChecksState(); resetCommentsState(); resetRouterState();
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts index 8bcd395b..160b543 100644 --- a/polygerrit-ui/app/test/test-app-context-init.ts +++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -26,7 +26,7 @@ import {FlagsServiceImplementation} from '../services/flags/flags_impl'; import {EventEmitter} from '../services/gr-event-interface/gr-event-interface_impl'; import {ChangeService} from '../services/change/change-service'; -import {ChecksService} from '../services/checks/checks-service'; +import {ChecksModel} from '../services/checks/checks-model'; import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element'; import {UserModel} from '../services/user/user-model'; import {CommentsService} from '../services/comments/comments-service'; @@ -55,9 +55,9 @@ assertIsDefined(ctx.restApiService, 'restApiService'); return new CommentsService(ctx.restApiService!); }, - checksService: (ctx: Partial<AppContext>) => { + checksModel: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.reportingService, 'reportingService'); - return new ChecksService(ctx.reportingService!); + return new ChecksModel(ctx.reportingService!); }, jsApiService: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.reportingService, 'reportingService');