go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/artifact/artifact_page_layout.tsx (about)

     1  // Copyright 2021 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, html, render } from 'lit';
    16  import { customElement } from 'lit/decorators.js';
    17  import { computed, makeObservable, observable, reaction } from 'mobx';
    18  import { useEffect, useRef } from 'react';
    19  import { Outlet, useParams } from 'react-router-dom';
    20  
    21  import '@/common/components/image_diff_viewer';
    22  import '@/common/components/status_bar';
    23  import { RecoverableErrorBoundary } from '@/common/components/error_handling';
    24  import { ArtifactIdentifier } from '@/common/services/resultdb';
    25  import { commonStyles } from '@/common/styles/stylesheets';
    26  import { getInvURLPath } from '@/common/tools/url_utils';
    27  import { MobxExtLitElement } from '@/generic_libs/components/lit_mobx_ext';
    28  import { createContextLink, provider } from '@/generic_libs/tools/lit_context';
    29  
    30  export const [provideArtifactIdent, consumeArtifactIdent] =
    31    createContextLink<ArtifactIdentifier>();
    32  
    33  /**
    34   * Renders the header of an artifact page.
    35   */
    36  @customElement('milo-artifact-page-layout')
    37  @provider
    38  export class ArtifactPageLayoutElement extends MobxExtLitElement {
    39    @observable.ref private invId!: string;
    40    @observable.ref private testId?: string;
    41    @observable.ref private resultId?: string;
    42    @observable.ref private artifactId!: string;
    43  
    44    @computed
    45    @provideArtifactIdent()
    46    get artifactIdent() {
    47      return {
    48        invocationId: this.invId,
    49        testId: this.testId,
    50        resultId: this.resultId,
    51        artifactId: this.artifactId,
    52      };
    53    }
    54  
    55    constructor() {
    56      super();
    57      makeObservable(this);
    58    }
    59  
    60    connectedCallback() {
    61      super.connectedCallback();
    62  
    63      this.addDisposer(
    64        reaction(
    65          () => this.artifactIdent,
    66          (artifactIdent) => {
    67            // Emulate @property() update.
    68            this.updated(new Map([['artifactIdent', artifactIdent]]));
    69          },
    70          { fireImmediately: true },
    71        ),
    72      );
    73    }
    74  
    75    protected render() {
    76      return html`
    77        <div id="artifact-header">
    78          <table>
    79            <tr>
    80              <td class="id-component-label">Invocation</td>
    81              <td>
    82                <a href=${getInvURLPath(this.invId)}> ${this.invId} </a>
    83              </td>
    84            </tr>
    85            ${this.testId &&
    86            html`
    87              <!-- TODO(weiweilin): add view test link -->
    88              <tr>
    89                <td class="id-component-label">Test</td>
    90                <td>${this.testId}</td>
    91              </tr>
    92            `}
    93            ${this.resultId &&
    94            html`
    95              <!-- TODO(weiweilin): add view result link -->
    96              <tr>
    97                <td class="id-component-label">Result</td>
    98                <td>${this.resultId}</td>
    99              </tr>
   100            `}
   101            <tr>
   102              <td class="id-component-label">Artifact</td>
   103              <td>${this.artifactId}</td>
   104            </tr>
   105          </table>
   106        </div>
   107        <milo-status-bar
   108          .components=${[{ color: 'var(--active-color)', weight: 1 }]}
   109        ></milo-status-bar>
   110        <slot></slot>
   111      `;
   112    }
   113  
   114    static styles = [
   115      commonStyles,
   116      css`
   117        :host {
   118          display: block;
   119        }
   120  
   121        #artifact-header {
   122          background-color: var(--block-background-color);
   123          padding: 6px 16px;
   124          font-family: 'Google Sans', 'Helvetica Neue', sans-serif;
   125          font-size: 14px;
   126        }
   127        .id-component-label {
   128          color: var(--light-text-color);
   129        }
   130      `,
   131    ];
   132  }
   133  
   134  export function ArtifactPageLayout() {
   135    const { invId, testId, resultId, artifactId } = useParams();
   136  
   137    const container = useRef<HTMLDivElement | null>(null);
   138  
   139    useEffect(() => {
   140      // This never happens, but useful for type narrowing.
   141      if (!container.current) {
   142        throw new Error('unreachable');
   143      }
   144  
   145      render(
   146        html` <milo-artifact-page-layout
   147          .invId=${invId}
   148          .testId=${testId}
   149          .resultId=${resultId}
   150          .artifactId=${artifactId}
   151        >
   152          ${container.current.children}
   153        </milo-artifact-page-layout>`,
   154        container.current,
   155      );
   156    }, [invId, testId, resultId, artifactId]);
   157  
   158    return (
   159      <div ref={container}>
   160        <div>
   161          <Outlet />
   162        </div>
   163      </div>
   164    );
   165  }
   166  
   167  export const element = (
   168    // See the documentation for `<LoginPage />` for why we handle error this way.
   169    <RecoverableErrorBoundary key="artifact">
   170      <ArtifactPageLayout />
   171    </RecoverableErrorBoundary>
   172  );