go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/text_artifact.ts (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 '@material/mwc-icon';
    16  import { MobxLitElement } from '@adobe/lit-mobx';
    17  import { css, html } from 'lit';
    18  import { customElement } from 'lit/decorators.js';
    19  import { computed, makeObservable, observable } from 'mobx';
    20  import { fromPromise, IPromiseBasedObservable } from 'mobx-utils';
    21  
    22  import '@/generic_libs/components/dot_spinner';
    23  import { ARTIFACT_LENGTH_LIMIT } from '@/common/constants/test';
    24  import { commonStyles } from '@/common/styles/stylesheets';
    25  import { unwrapObservable } from '@/generic_libs/tools/mobx_utils';
    26  import { toError, urlSetSearchQueryParam } from '@/generic_libs/tools/utils';
    27  
    28  import { ON_TEST_RESULT_DATA_READY } from '../constants/event';
    29  import { getRawArtifactURLPath } from '../tools/url_utils';
    30  
    31  export interface TextArtifactEvent {
    32    setData: (invName: string, resultName: string) => void;
    33  }
    34  
    35  /**
    36   * Renders a text artifact.
    37   */
    38  @customElement('text-artifact')
    39  export class TextArtifactElement extends MobxLitElement {
    40    static get properties() {
    41      return {
    42        artifactId: {
    43          attribute: 'artifact-id',
    44          type: String,
    45        },
    46        invLevel: {
    47          attribute: 'inv-level',
    48          type: Boolean,
    49        },
    50      };
    51    }
    52  
    53    @observable.ref
    54    resultName: string | undefined;
    55  
    56    @observable.ref
    57    invName: string | undefined;
    58  
    59    @observable.ref _artifactId!: string;
    60    @computed get artifactId() {
    61      return this._artifactId;
    62    }
    63    set artifactId(newVal: string) {
    64      this._artifactId = newVal;
    65    }
    66  
    67    @observable.ref _invLevel = false;
    68    @computed get invLevel() {
    69      return this._invLevel;
    70    }
    71    set invLevel(newVal: boolean) {
    72      this._invLevel = newVal;
    73    }
    74  
    75    @computed
    76    private get fetchUrl(): string {
    77      if (!this.resultName || !this.invName) {
    78        return '';
    79      }
    80  
    81      if (this.invLevel) {
    82        return getRawArtifactURLPath(
    83          `${this.invName}/artifacts/${this.artifactId}`,
    84        );
    85      }
    86  
    87      return getRawArtifactURLPath(
    88        `${this.resultName}/artifacts/${this.artifactId}`,
    89      );
    90    }
    91  
    92    @computed
    93    private get content$(): IPromiseBasedObservable<string> {
    94      if (!this.fetchUrl) {
    95        return fromPromise(Promise.race([]));
    96      }
    97      return fromPromise(
    98        fetch(
    99          urlSetSearchQueryParam(this.fetchUrl, 'n', ARTIFACT_LENGTH_LIMIT),
   100        ).then((res) => res.text()),
   101      );
   102    }
   103  
   104    @computed
   105    private get content() {
   106      return unwrapObservable(this.content$, null);
   107    }
   108  
   109    eventTimeout: number | undefined;
   110  
   111    constructor() {
   112      super();
   113      makeObservable(this);
   114    }
   115  
   116    protected createRenderRoot() {
   117      return this;
   118    }
   119  
   120    connectedCallback(): void {
   121      super.connectedCallback();
   122      // This must be done in the connected callback as it needs
   123      // to occur when the element is in the dom in order for the
   124      // event to bubble up to the parent.
   125      // The timeout is needed to ensure that the event is fired after
   126      // parent React components are mounted and were able to
   127      // register the event handlers
   128      this.eventTimeout = window.setTimeout(
   129        () =>
   130          this.dispatchEvent(
   131            new CustomEvent<TextArtifactEvent>(ON_TEST_RESULT_DATA_READY, {
   132              detail: {
   133                setData: (invName, resultName) => {
   134                  this.resultName = resultName;
   135                  this.invName = invName;
   136                  this.requestUpdate();
   137                },
   138              },
   139              // Allows events to bubble up the tree,
   140              // which can then be captured by React components.
   141              bubbles: true,
   142              // Allows the event to traverse shadowroot boundries.
   143              composed: true,
   144            }),
   145          ),
   146        0,
   147      );
   148    }
   149  
   150    disconnectedCallback(): void {
   151      super.disconnectedCallback();
   152  
   153      clearTimeout(this.eventTimeout);
   154    }
   155  
   156    protected render() {
   157      try {
   158        const label = this._invLevel ? 'Inv-level artifact' : 'Artifact';
   159  
   160        if (this.content === null) {
   161          return html`<div id="load">
   162            Loading <milo-dot-spinner></milo-dot-spinner>
   163          </div>`;
   164        }
   165  
   166        if (this.content === '') {
   167          return html`<div>${label}: <i>${this.artifactId}</i> is empty.</div>`;
   168        }
   169  
   170        return html`<pre>${this.content}</pre>`;
   171      } catch (e: unknown) {
   172        const error = toError(e);
   173        return html`<span>An error occured: ${error.message}</span>`;
   174      }
   175    }
   176  
   177    static styles = [
   178      commonStyles,
   179      css`
   180        #load {
   181          color: var(--active-text-color);
   182        }
   183        pre {
   184          margin: 0;
   185          font-size: 12px;
   186          white-space: pre-wrap;
   187          overflow-wrap: anywhere;
   188        }
   189      `,
   190    ];
   191  }