go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/artifact/artifact_page_layout.tsx (about) 1 // Copyright 2021 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, html, render } from 'lit'; 16 import { customElement } from 'lit/decorators.js'; 17 import { computed, makeObservable, observable, reaction } from 'mobx'; 18 import { useEffect, useRef } from 'react'; 19 import { Outlet, useParams } from 'react-router-dom'; 20 21 import '@/common/components/image_diff_viewer'; 22 import '@/common/components/status_bar'; 23 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 24 import { ArtifactIdentifier } from '@/common/services/resultdb'; 25 import { commonStyles } from '@/common/styles/stylesheets'; 26 import { getInvURLPath } from '@/common/tools/url_utils'; 27 import { MobxExtLitElement } from '@/generic_libs/components/lit_mobx_ext'; 28 import { createContextLink, provider } from '@/generic_libs/tools/lit_context'; 29 30 export const [provideArtifactIdent, consumeArtifactIdent] = 31 createContextLink<ArtifactIdentifier>(); 32 33 /** 34 * Renders the header of an artifact page. 35 */ 36 @customElement('milo-artifact-page-layout') 37 @provider 38 export class ArtifactPageLayoutElement extends MobxExtLitElement { 39 @observable.ref private invId!: string; 40 @observable.ref private testId?: string; 41 @observable.ref private resultId?: string; 42 @observable.ref private artifactId!: string; 43 44 @computed 45 @provideArtifactIdent() 46 get artifactIdent() { 47 return { 48 invocationId: this.invId, 49 testId: this.testId, 50 resultId: this.resultId, 51 artifactId: this.artifactId, 52 }; 53 } 54 55 constructor() { 56 super(); 57 makeObservable(this); 58 } 59 60 connectedCallback() { 61 super.connectedCallback(); 62 63 this.addDisposer( 64 reaction( 65 () => this.artifactIdent, 66 (artifactIdent) => { 67 // Emulate @property() update. 68 this.updated(new Map([['artifactIdent', artifactIdent]])); 69 }, 70 { fireImmediately: true }, 71 ), 72 ); 73 } 74 75 protected render() { 76 return html` 77 <div id="artifact-header"> 78 <table> 79 <tr> 80 <td class="id-component-label">Invocation</td> 81 <td> 82 <a href=${getInvURLPath(this.invId)}> ${this.invId} </a> 83 </td> 84 </tr> 85 ${this.testId && 86 html` 87 <!-- TODO(weiweilin): add view test link --> 88 <tr> 89 <td class="id-component-label">Test</td> 90 <td>${this.testId}</td> 91 </tr> 92 `} 93 ${this.resultId && 94 html` 95 <!-- TODO(weiweilin): add view result link --> 96 <tr> 97 <td class="id-component-label">Result</td> 98 <td>${this.resultId}</td> 99 </tr> 100 `} 101 <tr> 102 <td class="id-component-label">Artifact</td> 103 <td>${this.artifactId}</td> 104 </tr> 105 </table> 106 </div> 107 <milo-status-bar 108 .components=${[{ color: 'var(--active-color)', weight: 1 }]} 109 ></milo-status-bar> 110 <slot></slot> 111 `; 112 } 113 114 static styles = [ 115 commonStyles, 116 css` 117 :host { 118 display: block; 119 } 120 121 #artifact-header { 122 background-color: var(--block-background-color); 123 padding: 6px 16px; 124 font-family: 'Google Sans', 'Helvetica Neue', sans-serif; 125 font-size: 14px; 126 } 127 .id-component-label { 128 color: var(--light-text-color); 129 } 130 `, 131 ]; 132 } 133 134 export function ArtifactPageLayout() { 135 const { invId, testId, resultId, artifactId } = useParams(); 136 137 const container = useRef<HTMLDivElement | null>(null); 138 139 useEffect(() => { 140 // This never happens, but useful for type narrowing. 141 if (!container.current) { 142 throw new Error('unreachable'); 143 } 144 145 render( 146 html` <milo-artifact-page-layout 147 .invId=${invId} 148 .testId=${testId} 149 .resultId=${resultId} 150 .artifactId=${artifactId} 151 > 152 ${container.current.children} 153 </milo-artifact-page-layout>`, 154 container.current, 155 ); 156 }, [invId, testId, resultId, artifactId]); 157 158 return ( 159 <div ref={container}> 160 <div> 161 <Outlet /> 162 </div> 163 </div> 164 ); 165 } 166 167 export const element = ( 168 // See the documentation for `<LoginPage />` for why we handle error this way. 169 <RecoverableErrorBoundary key="artifact"> 170 <ArtifactPageLayout /> 171 </RecoverableErrorBoundary> 172 );