go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/legacy/build_page/build_lit_env_provider.tsx (about)

     1  // Copyright 2023 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 as litCss, html } from 'lit';
    16  import { customElement } from 'lit/decorators.js';
    17  import { computed, makeObservable, observable, reaction } from 'mobx';
    18  import { ReactNode } from 'react';
    19  
    20  import { OPTIONAL_RESOURCE } from '@/common/common_tags';
    21  import { POTENTIALLY_EXPIRED } from '@/common/constants/legacy';
    22  import { LoadTestVariantsError } from '@/common/models/test_loader';
    23  import { consumeStore, StoreInstance } from '@/common/store';
    24  import { GetBuildError } from '@/common/store/build_page';
    25  import {
    26    provideInvocationState,
    27    QueryInvocationError,
    28  } from '@/common/store/invocation_state';
    29  import { getBuildURLPath } from '@/common/tools/url_utils';
    30  import { MobxExtLitElement } from '@/generic_libs/components/lit_mobx_ext';
    31  import {
    32    errorHandler,
    33    forwardWithoutMsg,
    34    renderErrorInPre,
    35  } from '@/generic_libs/tools/error_handler';
    36  import { consumer, provider } from '@/generic_libs/tools/lit_context';
    37  import { attachTags, hasTags } from '@/generic_libs/tools/tag';
    38  import {
    39    provideInvId,
    40    provideProject,
    41    provideTestTabUrl,
    42  } from '@/test_verdict/legacy/test_results_tab/test_variants_table/context';
    43  
    44  function retryWithoutComputedInvId(
    45    err: ErrorEvent,
    46    ele: BuildLitEnvProviderElement,
    47  ) {
    48    let recovered = false;
    49    if (err.error instanceof LoadTestVariantsError) {
    50      // Ignore request using the old invocation ID.
    51      if (
    52        !err.error.req.invocations.includes(
    53          `invocations/${ele.store.buildPage.invocationId}`,
    54        )
    55      ) {
    56        recovered = true;
    57      }
    58  
    59      // Old builds don't support computed invocation ID.
    60      // Disable it and try again.
    61      if (ele.store.buildPage.useComputedInvId && !err.error.req.pageToken) {
    62        ele.store.buildPage.setUseComputedInvId(false);
    63        recovered = true;
    64      }
    65    } else if (err.error instanceof QueryInvocationError) {
    66      // Ignore request using the old invocation ID.
    67      if (err.error.invId !== ele.store.buildPage.invocationId) {
    68        recovered = true;
    69      }
    70  
    71      // Old builds don't support computed invocation ID.
    72      // Disable it and try again.
    73      if (ele.store.buildPage.useComputedInvId) {
    74        ele.store.buildPage.setUseComputedInvId(false);
    75        recovered = true;
    76      }
    77    }
    78  
    79    if (recovered) {
    80      err.stopImmediatePropagation();
    81      err.preventDefault();
    82      return false;
    83    }
    84  
    85    if (!(err.error instanceof GetBuildError)) {
    86      attachTags(err.error, OPTIONAL_RESOURCE);
    87    }
    88  
    89    return forwardWithoutMsg(err, ele);
    90  }
    91  
    92  function renderError(err: ErrorEvent, ele: BuildLitEnvProviderElement) {
    93    if (
    94      err.error instanceof GetBuildError &&
    95      hasTags(err.error, POTENTIALLY_EXPIRED)
    96    ) {
    97      return html`
    98        <div id="build-not-found-error">
    99          Build Not Found: if you are trying to view an old build, it could have
   100          been wiped from the server already.
   101        </div>
   102        ${renderErrorInPre(err, ele)}
   103      `;
   104    }
   105  
   106    return renderErrorInPre(err, ele);
   107  }
   108  
   109  /**
   110   * Provides context and error handling to lit components in a build page.
   111   */
   112  @customElement('milo-build-lit-env-provider')
   113  @errorHandler(retryWithoutComputedInvId, renderError)
   114  @provider
   115  @consumer
   116  export class BuildLitEnvProviderElement extends MobxExtLitElement {
   117    @observable.ref
   118    @consumeStore()
   119    store!: StoreInstance;
   120  
   121    @provideInvocationState({ global: true })
   122    @computed
   123    get invState() {
   124      return this.store.buildPage.invocation;
   125    }
   126  
   127    @provideProject({ global: true })
   128    @computed
   129    get project() {
   130      return this.store.buildPage.build?.data.builder.project;
   131    }
   132  
   133    @provideInvId({ global: true })
   134    @computed
   135    get invId() {
   136      return this.store.buildPage.invocationId || undefined;
   137    }
   138  
   139    @provideTestTabUrl({ global: true })
   140    @computed
   141    get testTabUrl() {
   142      if (
   143        !this.store.buildPage.builderIdParam ||
   144        !this.store.buildPage.buildNumOrIdParam
   145      ) {
   146        return undefined;
   147      }
   148      return (
   149        getBuildURLPath(
   150          this.store.buildPage.builderIdParam,
   151          this.store.buildPage.buildNumOrIdParam,
   152        ) + '/test-results'
   153      );
   154    }
   155  
   156    constructor() {
   157      super();
   158      makeObservable(this);
   159    }
   160  
   161    connectedCallback() {
   162      super.connectedCallback();
   163  
   164      this.addDisposer(
   165        reaction(
   166          () => this.invState,
   167          (invState) => {
   168            // Emulate @property() update.
   169            this.updated(new Map([['invState', invState]]));
   170          },
   171          { fireImmediately: true },
   172        ),
   173      );
   174  
   175      this.addDisposer(
   176        reaction(
   177          () => this.project,
   178          (project) => {
   179            // Emulate @property() update.
   180            this.updated(new Map([['project', project]]));
   181          },
   182          { fireImmediately: true },
   183        ),
   184      );
   185  
   186      this.addDisposer(
   187        reaction(
   188          () => this.invId,
   189          (invId) => {
   190            // Emulate @property() update.
   191            this.updated(new Map([['invId', invId]]));
   192          },
   193          { fireImmediately: true },
   194        ),
   195      );
   196  
   197      this.addDisposer(
   198        reaction(
   199          () => this.testTabUrl,
   200          (testTabUrl) => {
   201            // Emulate @property() update.
   202            this.updated(new Map([['testTabUrl', testTabUrl]]));
   203          },
   204          { fireImmediately: true },
   205        ),
   206      );
   207    }
   208  
   209    protected render() {
   210      return html`<slot></slot>`;
   211    }
   212  
   213    static styles = [
   214      litCss`
   215        #build-not-found-error {
   216          background-color: var(--warning-color);
   217          font-weight: 500;
   218          padding: 5px;
   219          margin: 8px 16px;
   220        }
   221      `,
   222    ];
   223  }
   224  
   225  declare global {
   226    // eslint-disable-next-line @typescript-eslint/no-namespace
   227    namespace JSX {
   228      interface IntrinsicElements {
   229        'milo-build-lit-env-provider': {
   230          children: ReactNode;
   231        };
   232      }
   233    }
   234  }
   235  
   236  export interface BuildLitEnvProviderProps {
   237    readonly children: React.ReactNode;
   238  }
   239  
   240  export function BuildLitEnvProvider({ children }: BuildLitEnvProviderProps) {
   241    return <milo-build-lit-env-provider>{children}</milo-build-lit-env-provider>;
   242  }