go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/legacy/build_page/build_lit_env_provider.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { css as litCss, html } from 'lit'; 16 import { customElement } from 'lit/decorators.js'; 17 import { computed, makeObservable, observable, reaction } from 'mobx'; 18 import { ReactNode } from 'react'; 19 20 import { OPTIONAL_RESOURCE } from '@/common/common_tags'; 21 import { POTENTIALLY_EXPIRED } from '@/common/constants/legacy'; 22 import { LoadTestVariantsError } from '@/common/models/test_loader'; 23 import { consumeStore, StoreInstance } from '@/common/store'; 24 import { GetBuildError } from '@/common/store/build_page'; 25 import { 26 provideInvocationState, 27 QueryInvocationError, 28 } from '@/common/store/invocation_state'; 29 import { getBuildURLPath } from '@/common/tools/url_utils'; 30 import { MobxExtLitElement } from '@/generic_libs/components/lit_mobx_ext'; 31 import { 32 errorHandler, 33 forwardWithoutMsg, 34 renderErrorInPre, 35 } from '@/generic_libs/tools/error_handler'; 36 import { consumer, provider } from '@/generic_libs/tools/lit_context'; 37 import { attachTags, hasTags } from '@/generic_libs/tools/tag'; 38 import { 39 provideInvId, 40 provideProject, 41 provideTestTabUrl, 42 } from '@/test_verdict/legacy/test_results_tab/test_variants_table/context'; 43 44 function retryWithoutComputedInvId( 45 err: ErrorEvent, 46 ele: BuildLitEnvProviderElement, 47 ) { 48 let recovered = false; 49 if (err.error instanceof LoadTestVariantsError) { 50 // Ignore request using the old invocation ID. 51 if ( 52 !err.error.req.invocations.includes( 53 `invocations/${ele.store.buildPage.invocationId}`, 54 ) 55 ) { 56 recovered = true; 57 } 58 59 // Old builds don't support computed invocation ID. 60 // Disable it and try again. 61 if (ele.store.buildPage.useComputedInvId && !err.error.req.pageToken) { 62 ele.store.buildPage.setUseComputedInvId(false); 63 recovered = true; 64 } 65 } else if (err.error instanceof QueryInvocationError) { 66 // Ignore request using the old invocation ID. 67 if (err.error.invId !== ele.store.buildPage.invocationId) { 68 recovered = true; 69 } 70 71 // Old builds don't support computed invocation ID. 72 // Disable it and try again. 73 if (ele.store.buildPage.useComputedInvId) { 74 ele.store.buildPage.setUseComputedInvId(false); 75 recovered = true; 76 } 77 } 78 79 if (recovered) { 80 err.stopImmediatePropagation(); 81 err.preventDefault(); 82 return false; 83 } 84 85 if (!(err.error instanceof GetBuildError)) { 86 attachTags(err.error, OPTIONAL_RESOURCE); 87 } 88 89 return forwardWithoutMsg(err, ele); 90 } 91 92 function renderError(err: ErrorEvent, ele: BuildLitEnvProviderElement) { 93 if ( 94 err.error instanceof GetBuildError && 95 hasTags(err.error, POTENTIALLY_EXPIRED) 96 ) { 97 return html` 98 <div id="build-not-found-error"> 99 Build Not Found: if you are trying to view an old build, it could have 100 been wiped from the server already. 101 </div> 102 ${renderErrorInPre(err, ele)} 103 `; 104 } 105 106 return renderErrorInPre(err, ele); 107 } 108 109 /** 110 * Provides context and error handling to lit components in a build page. 111 */ 112 @customElement('milo-build-lit-env-provider') 113 @errorHandler(retryWithoutComputedInvId, renderError) 114 @provider 115 @consumer 116 export class BuildLitEnvProviderElement extends MobxExtLitElement { 117 @observable.ref 118 @consumeStore() 119 store!: StoreInstance; 120 121 @provideInvocationState({ global: true }) 122 @computed 123 get invState() { 124 return this.store.buildPage.invocation; 125 } 126 127 @provideProject({ global: true }) 128 @computed 129 get project() { 130 return this.store.buildPage.build?.data.builder.project; 131 } 132 133 @provideInvId({ global: true }) 134 @computed 135 get invId() { 136 return this.store.buildPage.invocationId || undefined; 137 } 138 139 @provideTestTabUrl({ global: true }) 140 @computed 141 get testTabUrl() { 142 if ( 143 !this.store.buildPage.builderIdParam || 144 !this.store.buildPage.buildNumOrIdParam 145 ) { 146 return undefined; 147 } 148 return ( 149 getBuildURLPath( 150 this.store.buildPage.builderIdParam, 151 this.store.buildPage.buildNumOrIdParam, 152 ) + '/test-results' 153 ); 154 } 155 156 constructor() { 157 super(); 158 makeObservable(this); 159 } 160 161 connectedCallback() { 162 super.connectedCallback(); 163 164 this.addDisposer( 165 reaction( 166 () => this.invState, 167 (invState) => { 168 // Emulate @property() update. 169 this.updated(new Map([['invState', invState]])); 170 }, 171 { fireImmediately: true }, 172 ), 173 ); 174 175 this.addDisposer( 176 reaction( 177 () => this.project, 178 (project) => { 179 // Emulate @property() update. 180 this.updated(new Map([['project', project]])); 181 }, 182 { fireImmediately: true }, 183 ), 184 ); 185 186 this.addDisposer( 187 reaction( 188 () => this.invId, 189 (invId) => { 190 // Emulate @property() update. 191 this.updated(new Map([['invId', invId]])); 192 }, 193 { fireImmediately: true }, 194 ), 195 ); 196 197 this.addDisposer( 198 reaction( 199 () => this.testTabUrl, 200 (testTabUrl) => { 201 // Emulate @property() update. 202 this.updated(new Map([['testTabUrl', testTabUrl]])); 203 }, 204 { fireImmediately: true }, 205 ), 206 ); 207 } 208 209 protected render() { 210 return html`<slot></slot>`; 211 } 212 213 static styles = [ 214 litCss` 215 #build-not-found-error { 216 background-color: var(--warning-color); 217 font-weight: 500; 218 padding: 5px; 219 margin: 8px 16px; 220 } 221 `, 222 ]; 223 } 224 225 declare global { 226 // eslint-disable-next-line @typescript-eslint/no-namespace 227 namespace JSX { 228 interface IntrinsicElements { 229 'milo-build-lit-env-provider': { 230 children: ReactNode; 231 }; 232 } 233 } 234 } 235 236 export interface BuildLitEnvProviderProps { 237 readonly children: React.ReactNode; 238 } 239 240 export function BuildLitEnvProvider({ children }: BuildLitEnvProviderProps) { 241 return <milo-build-lit-env-provider>{children}</milo-build-lit-env-provider>; 242 }