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  );