go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/changelists_badge/changelists_badge.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 { MobxLitElement } from '@adobe/lit-mobx';
    16  import createCache from '@emotion/cache';
    17  import { CacheProvider, EmotionCache } from '@emotion/react';
    18  import { Chip } from '@mui/material';
    19  import { html, render } from 'lit';
    20  import { customElement } from 'lit/decorators.js';
    21  import { makeObservable, observable } from 'mobx';
    22  import { createRef } from 'react';
    23  import { createRoot, Root } from 'react-dom/client';
    24  
    25  import './changelists_tooltip';
    26  import {
    27    HideTooltipEventDetail,
    28    ShowTooltipEventDetail,
    29  } from '@/common/components/tooltip';
    30  import { Changelist } from '@/common/services/luci_analysis';
    31  import { commonStyles } from '@/common/styles/stylesheets';
    32  
    33  import { getClLabel, getClLink } from './changelists_tooltip';
    34  
    35  export interface ChangelistBadgeProps {
    36    readonly changelists: readonly Changelist[];
    37  }
    38  
    39  export function ChangelistsBadge({ changelists }: ChangelistBadgeProps) {
    40    const badgeRef = createRef<HTMLAnchorElement>();
    41    const firstCl = changelists[0];
    42    if (!firstCl) {
    43      return <></>;
    44    }
    45  
    46    const hasMultipleCls = changelists.length > 1;
    47  
    48    return (
    49      <Chip
    50        label={`${getClLabel(firstCl)}${hasMultipleCls ? ', ...' : ''}`}
    51        size="small"
    52        component="a"
    53        target="_blank"
    54        href={getClLink(firstCl)}
    55        clickable
    56        ref={badgeRef}
    57        onMouseOver={() => {
    58          if (!hasMultipleCls) {
    59            return;
    60          }
    61          const tooltip = document.createElement('div');
    62          render(
    63            html`
    64              <milo-changelists-tooltip
    65                .changelists=${changelists}
    66              ></milo-changelists-tooltip>
    67            `,
    68            tooltip,
    69          );
    70          window.dispatchEvent(
    71            new CustomEvent<ShowTooltipEventDetail>('show-tooltip', {
    72              detail: {
    73                tooltip,
    74                targetRect: badgeRef.current!.getBoundingClientRect(),
    75                gapSize: 2,
    76              },
    77            }),
    78          );
    79        }}
    80        onMouseOut={() => {
    81          if (!hasMultipleCls) {
    82            return;
    83          }
    84          window.dispatchEvent(
    85            new CustomEvent<HideTooltipEventDetail>('hide-tooltip', {
    86              detail: { delay: 50 },
    87            }),
    88          );
    89        }}
    90        onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) =>
    91          e.stopPropagation()
    92        }
    93      />
    94    );
    95  }
    96  
    97  @customElement('milo-changelists-badge')
    98  export class ChangelistsBadgeElement extends MobxLitElement {
    99    @observable.ref changelists!: readonly Changelist[];
   100  
   101    private readonly cache: EmotionCache;
   102    private readonly parent: HTMLSpanElement;
   103    private readonly root: Root;
   104  
   105    constructor() {
   106      super();
   107      makeObservable(this);
   108      this.parent = document.createElement('span');
   109      const child = document.createElement('span');
   110      this.root = createRoot(child);
   111      this.parent.appendChild(child);
   112      this.cache = createCache({
   113        key: 'milo-changelists-badge',
   114        container: this.parent,
   115      });
   116    }
   117  
   118    protected render() {
   119      this.root.render(
   120        <CacheProvider value={this.cache}>
   121          <ChangelistsBadge changelists={this.changelists} />
   122        </CacheProvider>,
   123      );
   124      return this.parent;
   125    }
   126  
   127    static styles = [commonStyles];
   128  }