go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/artifact/image_diff_artifact_page.tsx (about) 1 // Copyright 2020 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 { MobxLitElement } from '@adobe/lit-mobx'; 16 import { css, html } from 'lit'; 17 import { customElement } from 'lit/decorators.js'; 18 import { computed, makeObservable, observable } from 'mobx'; 19 import { fromPromise } from 'mobx-utils'; 20 21 import '@/common/components/image_diff_viewer'; 22 import '@/common/components/status_bar'; 23 import '@/generic_libs/components/dot_spinner'; 24 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 25 import { 26 ArtifactIdentifier, 27 constructArtifactName, 28 } from '@/common/services/resultdb'; 29 import { consumeStore, StoreInstance } from '@/common/store'; 30 import { commonStyles } from '@/common/styles/stylesheets'; 31 import { useSyncedSearchParams } from '@/generic_libs/hooks/synced_search_params'; 32 import { reportRenderError } from '@/generic_libs/tools/error_handler'; 33 import { consumer } from '@/generic_libs/tools/lit_context'; 34 import { unwrapObservable } from '@/generic_libs/tools/mobx_utils'; 35 36 import { consumeArtifactIdent } from './artifact_page_layout'; 37 38 /** 39 * Renders an image diff artifact set, including expected image, actual image 40 * and image diff. 41 */ 42 // TODO(weiweilin): improve error handling. 43 @customElement('milo-image-diff-artifact-page') 44 @consumer 45 export class ImageDiffArtifactPageElement extends MobxLitElement { 46 static get properties() { 47 return { 48 expectedArtifactId: { 49 type: String, 50 }, 51 actualArtifactId: { 52 type: String, 53 }, 54 }; 55 } 56 57 @observable.ref 58 @consumeStore() 59 store!: StoreInstance; 60 61 @observable.ref 62 @consumeArtifactIdent() 63 artifactIdent!: ArtifactIdentifier; 64 65 @observable.ref _expectedArtifactId!: string; 66 @computed get expectedArtifactId() { 67 return this._expectedArtifactId; 68 } 69 set expectedArtifactId(newVal: string) { 70 this._expectedArtifactId = newVal; 71 } 72 73 @observable.ref _actualArtifactId!: string; 74 @computed get actualArtifactId() { 75 return this._actualArtifactId; 76 } 77 set actualArtifactId(newVal: string) { 78 this._actualArtifactId = newVal; 79 } 80 81 @computed private get diffArtifactName() { 82 return constructArtifactName({ ...this.artifactIdent }); 83 } 84 @computed private get expectedArtifactName() { 85 return constructArtifactName({ 86 ...this.artifactIdent, 87 artifactId: this.expectedArtifactId, 88 }); 89 } 90 @computed private get actualArtifactName() { 91 return constructArtifactName({ 92 ...this.artifactIdent, 93 artifactId: this.actualArtifactId, 94 }); 95 } 96 97 @computed 98 private get diffArtifact$() { 99 if (!this.store.services.resultDb) { 100 return fromPromise(Promise.race([])); 101 } 102 return fromPromise( 103 this.store.services.resultDb.getArtifact({ name: this.diffArtifactName }), 104 ); 105 } 106 @computed private get diffArtifact() { 107 return unwrapObservable(this.diffArtifact$, null); 108 } 109 110 @computed 111 private get expectedArtifact$() { 112 if (!this.store.services.resultDb) { 113 return fromPromise(Promise.race([])); 114 } 115 return fromPromise( 116 this.store.services.resultDb.getArtifact({ 117 name: this.expectedArtifactName, 118 }), 119 ); 120 } 121 @computed private get expectedArtifact() { 122 return unwrapObservable(this.expectedArtifact$, null); 123 } 124 125 @computed 126 private get actualArtifact$() { 127 if (!this.store.services.resultDb) { 128 return fromPromise(Promise.race([])); 129 } 130 return fromPromise( 131 this.store.services.resultDb.getArtifact({ 132 name: this.actualArtifactName, 133 }), 134 ); 135 } 136 @computed private get actualArtifact() { 137 return unwrapObservable(this.actualArtifact$, null); 138 } 139 140 @computed get isLoading() { 141 return !this.expectedArtifact || !this.actualArtifact || !this.diffArtifact; 142 } 143 144 constructor() { 145 super(); 146 makeObservable(this); 147 } 148 149 protected render = reportRenderError(this, () => { 150 if (this.isLoading) { 151 return html`<div id="loading-spinner" class="active-text"> 152 Loading <milo-dot-spinner></milo-dot-spinner> 153 </div>`; 154 } 155 156 return html` 157 <milo-image-diff-viewer 158 .expected=${this.expectedArtifact} 159 .actual=${this.actualArtifact} 160 .diff=${this.diffArtifact} 161 > 162 </milo-image-diff-viewer> 163 `; 164 }); 165 166 static styles = [ 167 commonStyles, 168 css` 169 :host { 170 display: block; 171 } 172 173 #loading-spinner { 174 margin: 20px; 175 } 176 `, 177 ]; 178 } 179 180 declare global { 181 // eslint-disable-next-line @typescript-eslint/no-namespace 182 namespace JSX { 183 interface IntrinsicElements { 184 'milo-image-diff-artifact-page': { 185 expectedArtifactId: string; 186 actualArtifactId: string; 187 }; 188 } 189 } 190 } 191 192 export function ImageDiffArtifactPage() { 193 const [search] = useSyncedSearchParams(); 194 195 const expectedArtifactId = search.get('expectedArtifactId'); 196 if (expectedArtifactId === null) { 197 throw new Error( 198 'expectedArtifactId must be provided via the search params', 199 ); 200 } 201 202 const actualArtifactId = search.get('actualArtifactId'); 203 if (actualArtifactId === null) { 204 throw new Error('actualArtifactId must be provided via the search params'); 205 } 206 207 return ( 208 <milo-image-diff-artifact-page 209 expectedArtifactId={expectedArtifactId} 210 actualArtifactId={actualArtifactId} 211 ></milo-image-diff-artifact-page> 212 ); 213 } 214 215 export const element = ( 216 // See the documentation for `<LoginPage />` for why we handle error this way. 217 <RecoverableErrorBoundary key="image-diff"> 218 <ImageDiffArtifactPage /> 219 </RecoverableErrorBoundary> 220 );