go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/artifact/image_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 { css, html } from 'lit';
    17  import { customElement } from 'lit/decorators.js';
    18  import { computed, makeObservable, observable } from 'mobx';
    19  import { fromPromise } from 'mobx-utils';
    20  
    21  import '@/common/components/image_diff_viewer';
    22  import '@/common/components/status_bar';
    23  import '@/generic_libs/components/dot_spinner';
    24  import { RecoverableErrorBoundary } from '@/common/components/error_handling';
    25  import {
    26    ArtifactIdentifier,
    27    constructArtifactName,
    28  } from '@/common/services/resultdb';
    29  import { consumeStore, StoreInstance } from '@/common/store';
    30  import { commonStyles } from '@/common/styles/stylesheets';
    31  import { useSyncedSearchParams } from '@/generic_libs/hooks/synced_search_params';
    32  import { reportRenderError } from '@/generic_libs/tools/error_handler';
    33  import { consumer } from '@/generic_libs/tools/lit_context';
    34  import { unwrapObservable } from '@/generic_libs/tools/mobx_utils';
    35  
    36  import { consumeArtifactIdent } from './artifact_page_layout';
    37  
    38  /**
    39   * Renders an image diff artifact set, including expected image, actual image
    40   * and image diff.
    41   */
    42  // TODO(weiweilin): improve error handling.
    43  @customElement('milo-image-diff-artifact-page')
    44  @consumer
    45  export class ImageDiffArtifactPageElement extends MobxLitElement {
    46    static get properties() {
    47      return {
    48        expectedArtifactId: {
    49          type: String,
    50        },
    51        actualArtifactId: {
    52          type: String,
    53        },
    54      };
    55    }
    56  
    57    @observable.ref
    58    @consumeStore()
    59    store!: StoreInstance;
    60  
    61    @observable.ref
    62    @consumeArtifactIdent()
    63    artifactIdent!: ArtifactIdentifier;
    64  
    65    @observable.ref _expectedArtifactId!: string;
    66    @computed get expectedArtifactId() {
    67      return this._expectedArtifactId;
    68    }
    69    set expectedArtifactId(newVal: string) {
    70      this._expectedArtifactId = newVal;
    71    }
    72  
    73    @observable.ref _actualArtifactId!: string;
    74    @computed get actualArtifactId() {
    75      return this._actualArtifactId;
    76    }
    77    set actualArtifactId(newVal: string) {
    78      this._actualArtifactId = newVal;
    79    }
    80  
    81    @computed private get diffArtifactName() {
    82      return constructArtifactName({ ...this.artifactIdent });
    83    }
    84    @computed private get expectedArtifactName() {
    85      return constructArtifactName({
    86        ...this.artifactIdent,
    87        artifactId: this.expectedArtifactId,
    88      });
    89    }
    90    @computed private get actualArtifactName() {
    91      return constructArtifactName({
    92        ...this.artifactIdent,
    93        artifactId: this.actualArtifactId,
    94      });
    95    }
    96  
    97    @computed
    98    private get diffArtifact$() {
    99      if (!this.store.services.resultDb) {
   100        return fromPromise(Promise.race([]));
   101      }
   102      return fromPromise(
   103        this.store.services.resultDb.getArtifact({ name: this.diffArtifactName }),
   104      );
   105    }
   106    @computed private get diffArtifact() {
   107      return unwrapObservable(this.diffArtifact$, null);
   108    }
   109  
   110    @computed
   111    private get expectedArtifact$() {
   112      if (!this.store.services.resultDb) {
   113        return fromPromise(Promise.race([]));
   114      }
   115      return fromPromise(
   116        this.store.services.resultDb.getArtifact({
   117          name: this.expectedArtifactName,
   118        }),
   119      );
   120    }
   121    @computed private get expectedArtifact() {
   122      return unwrapObservable(this.expectedArtifact$, null);
   123    }
   124  
   125    @computed
   126    private get actualArtifact$() {
   127      if (!this.store.services.resultDb) {
   128        return fromPromise(Promise.race([]));
   129      }
   130      return fromPromise(
   131        this.store.services.resultDb.getArtifact({
   132          name: this.actualArtifactName,
   133        }),
   134      );
   135    }
   136    @computed private get actualArtifact() {
   137      return unwrapObservable(this.actualArtifact$, null);
   138    }
   139  
   140    @computed get isLoading() {
   141      return !this.expectedArtifact || !this.actualArtifact || !this.diffArtifact;
   142    }
   143  
   144    constructor() {
   145      super();
   146      makeObservable(this);
   147    }
   148  
   149    protected render = reportRenderError(this, () => {
   150      if (this.isLoading) {
   151        return html`<div id="loading-spinner" class="active-text">
   152          Loading <milo-dot-spinner></milo-dot-spinner>
   153        </div>`;
   154      }
   155  
   156      return html`
   157        <milo-image-diff-viewer
   158          .expected=${this.expectedArtifact}
   159          .actual=${this.actualArtifact}
   160          .diff=${this.diffArtifact}
   161        >
   162        </milo-image-diff-viewer>
   163      `;
   164    });
   165  
   166    static styles = [
   167      commonStyles,
   168      css`
   169        :host {
   170          display: block;
   171        }
   172  
   173        #loading-spinner {
   174          margin: 20px;
   175        }
   176      `,
   177    ];
   178  }
   179  
   180  declare global {
   181    // eslint-disable-next-line @typescript-eslint/no-namespace
   182    namespace JSX {
   183      interface IntrinsicElements {
   184        'milo-image-diff-artifact-page': {
   185          expectedArtifactId: string;
   186          actualArtifactId: string;
   187        };
   188      }
   189    }
   190  }
   191  
   192  export function ImageDiffArtifactPage() {
   193    const [search] = useSyncedSearchParams();
   194  
   195    const expectedArtifactId = search.get('expectedArtifactId');
   196    if (expectedArtifactId === null) {
   197      throw new Error(
   198        'expectedArtifactId must be provided via the search params',
   199      );
   200    }
   201  
   202    const actualArtifactId = search.get('actualArtifactId');
   203    if (actualArtifactId === null) {
   204      throw new Error('actualArtifactId must be provided via the search params');
   205    }
   206  
   207    return (
   208      <milo-image-diff-artifact-page
   209        expectedArtifactId={expectedArtifactId}
   210        actualArtifactId={actualArtifactId}
   211      ></milo-image-diff-artifact-page>
   212    );
   213  }
   214  
   215  export const element = (
   216    // See the documentation for `<LoginPage />` for why we handle error this way.
   217    <RecoverableErrorBoundary key="image-diff">
   218      <ImageDiffArtifactPage />
   219    </RecoverableErrorBoundary>
   220  );