go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/artifact/text_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 * as Diff2Html from 'diff2html'; 17 import { css, html } from 'lit'; 18 import { customElement } from 'lit/decorators.js'; 19 import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 20 import { computed, makeObservable, observable } from 'mobx'; 21 import { fromPromise } from 'mobx-utils'; 22 23 import '@/generic_libs/components/dot_spinner'; 24 import '@/common/components/status_bar'; 25 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 26 import { ARTIFACT_LENGTH_LIMIT } from '@/common/constants/test'; 27 import { 28 ArtifactIdentifier, 29 constructArtifactName, 30 } from '@/common/services/resultdb'; 31 import { consumeStore, StoreInstance } from '@/common/store'; 32 import { commonStyles } from '@/common/styles/stylesheets'; 33 import { getRawArtifactURLPath } from '@/common/tools/url_utils'; 34 import { reportRenderError } from '@/generic_libs/tools/error_handler'; 35 import { consumer } from '@/generic_libs/tools/lit_context'; 36 import { unwrapObservable } from '@/generic_libs/tools/mobx_utils'; 37 import { urlSetSearchQueryParam } from '@/generic_libs/tools/utils'; 38 39 import { consumeArtifactIdent } from './artifact_page_layout'; 40 41 /** 42 * Renders a text diff artifact. 43 */ 44 @customElement('milo-text-diff-artifact-page') 45 @consumer 46 export class TextDiffArtifactPageElement extends MobxLitElement { 47 @observable.ref 48 @consumeStore() 49 store!: StoreInstance; 50 51 @observable.ref 52 @consumeArtifactIdent() 53 artifactIdent!: ArtifactIdentifier; 54 55 @computed 56 private get artifact$() { 57 if (!this.store.services.resultDb) { 58 return fromPromise(Promise.race([])); 59 } 60 return fromPromise( 61 this.store.services.resultDb.getArtifact({ 62 name: constructArtifactName(this.artifactIdent), 63 }), 64 ); 65 } 66 @computed private get artifact() { 67 return unwrapObservable(this.artifact$, null); 68 } 69 70 @computed 71 private get content$() { 72 if (!this.store.services.resultDb || !this.artifact) { 73 return fromPromise(Promise.race([])); 74 } 75 return fromPromise( 76 // TODO(crbug/1206109): use permanent raw artifact URL. 77 fetch( 78 urlSetSearchQueryParam( 79 this.artifact.fetchUrl, 80 'n', 81 ARTIFACT_LENGTH_LIMIT, 82 ), 83 ).then((res) => res.text()), 84 ); 85 } 86 @computed private get content() { 87 return unwrapObservable(this.content$, null); 88 } 89 90 constructor() { 91 super(); 92 makeObservable(this); 93 } 94 95 protected render = reportRenderError(this, () => { 96 if (!this.artifact || !this.content) { 97 return html`<div id="content" class="active-text"> 98 Loading <milo-dot-spinner></milo-dot-spinner> 99 </div>`; 100 } 101 102 return html` 103 <div id="details"> 104 <a href=${getRawArtifactURLPath(this.artifact.name)} 105 >View Raw Content</a 106 > 107 </div> 108 <div id="content"> 109 <link 110 rel="stylesheet" 111 type="text/css" 112 href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" 113 /> 114 ${unsafeHTML( 115 Diff2Html.html(this.content || '', { 116 drawFileList: false, 117 outputFormat: 'side-by-side', 118 }), 119 )} 120 </div> 121 `; 122 }); 123 124 static styles = [ 125 commonStyles, 126 css` 127 #details { 128 margin: 20px; 129 } 130 #content { 131 position: relative; 132 margin: 20px; 133 } 134 135 .d2h-code-linenumber { 136 cursor: default; 137 } 138 .d2h-moved-tag { 139 display: none; 140 } 141 `, 142 ]; 143 } 144 145 declare global { 146 // eslint-disable-next-line @typescript-eslint/no-namespace 147 namespace JSX { 148 interface IntrinsicElements { 149 'milo-text-diff-artifact-page': Record<string, never>; 150 } 151 } 152 } 153 154 export function TextDiffArtifactPage() { 155 return <milo-text-diff-artifact-page></milo-text-diff-artifact-page>; 156 } 157 158 export const element = ( 159 // See the documentation for `<LoginPage />` for why we handle error this way. 160 <RecoverableErrorBoundary key="text-diff"> 161 <TextDiffArtifactPage /> 162 </RecoverableErrorBoundary> 163 );