go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/duration_badge/duration_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 { Interpolation, Theme, css } from '@emotion/react';
    16  import { scaleThreshold } from 'd3';
    17  import { html, render } from 'lit';
    18  import { DateTime, Duration } from 'luxon';
    19  
    20  import {
    21    HideTooltipEventDetail,
    22    ShowTooltipEventDetail,
    23  } from '@/common/components/tooltip';
    24  import {
    25    LONG_TIME_FORMAT,
    26    displayCompactDuration,
    27    displayDuration,
    28  } from '@/common/tools/time_utils';
    29  
    30  const defaultColorScaleMs = scaleThreshold(
    31    [
    32      Duration.fromObject({ seconds: 20 }).toMillis(),
    33      Duration.fromObject({ minutes: 1 }).toMillis(),
    34      Duration.fromObject({ minutes: 5 }).toMillis(),
    35      Duration.fromObject({ minutes: 15 }).toMillis(),
    36      Duration.fromObject({ hours: 1 }).toMillis(),
    37      Duration.fromObject({ hours: 3 }).toMillis(),
    38      Duration.fromObject({ hours: 12 }).toMillis(),
    39    ],
    40    [
    41      {
    42        backgroundColor: 'hsl(206, 85%, 95%)',
    43        color: 'var(--light-text-color)',
    44      },
    45      {
    46        backgroundColor: 'hsl(206, 85%, 85%)',
    47        color: 'var(--light-text-color)',
    48      },
    49      { backgroundColor: 'hsl(206, 85%, 75%)', color: 'white' },
    50      { backgroundColor: 'hsl(206, 85%, 65%)', color: 'white' },
    51      { backgroundColor: 'hsl(206, 85%, 55%)', color: 'white' },
    52      { backgroundColor: 'hsl(206, 85%, 45%)', color: 'white' },
    53      { backgroundColor: 'hsl(206, 85%, 35%)', color: 'white' },
    54      { backgroundColor: 'hsl(206, 85%, 25%)', color: 'white' },
    55    ],
    56  );
    57  
    58  const defaultColorScale = (d: Duration) => defaultColorScaleMs(d.toMillis());
    59  
    60  const durationBadge = css`
    61    color: var(--light-text-color);
    62    background-color: var(--light-active-color);
    63    display: inline-block;
    64    padding: 0.25em 0.4em;
    65    font-size: 75%;
    66    font-weight: 500;
    67    line-height: 13px;
    68    text-align: center;
    69    white-space: nowrap;
    70    vertical-align: bottom;
    71    border-radius: 0.25rem;
    72    margin-bottom: 3px;
    73    width: 35px;
    74  `;
    75  
    76  function renderTooltip(
    77    duration: Duration,
    78    from?: DateTime | null,
    79    to?: DateTime | null,
    80  ) {
    81    return html`
    82      <table>
    83        <tr>
    84          <td>Duration:</td>
    85          <td>${displayDuration(duration)}</td>
    86        </tr>
    87        <tr>
    88          <td>From:</td>
    89          <td>${from ? from.toFormat(LONG_TIME_FORMAT) : 'N/A'}</td>
    90        </tr>
    91        <tr>
    92          <td>To:</td>
    93          <td>${to ? to.toFormat(LONG_TIME_FORMAT) : 'N/A'}</td>
    94        </tr>
    95      </table>
    96    `;
    97  }
    98  
    99  interface DurationBadgeProps {
   100    readonly duration: Duration;
   101    /**
   102     * When specified, renders start time in the tooltip.
   103     */
   104    readonly from?: DateTime | null;
   105    /**
   106     * When specified, renders end time in the tooltip.
   107     */
   108    readonly to?: DateTime | null;
   109    /**
   110     * Controls the text and background color base on the duration.
   111     */
   112    readonly colorScale?: (duration: Duration) => {
   113      backgroundColor: string;
   114      color: string;
   115    };
   116  
   117    readonly css?: Interpolation<Theme>;
   118    readonly className?: string;
   119  }
   120  
   121  /**
   122   * Renders a duration badge.
   123   */
   124  export function DurationBadge({
   125    duration,
   126    from,
   127    to,
   128    colorScale = defaultColorScale,
   129    css,
   130    className,
   131  }: DurationBadgeProps) {
   132    function onShowTooltip(target: HTMLElement) {
   133      const tooltip = document.createElement('div');
   134      render(renderTooltip(duration, from, to), tooltip);
   135  
   136      window.dispatchEvent(
   137        new CustomEvent<ShowTooltipEventDetail>('show-tooltip', {
   138          detail: {
   139            tooltip,
   140            targetRect: target.getBoundingClientRect(),
   141            gapSize: 2,
   142          },
   143        }),
   144      );
   145    }
   146  
   147    function onHideTooltip() {
   148      window.dispatchEvent(
   149        new CustomEvent<HideTooltipEventDetail>('hide-tooltip', {
   150          detail: { delay: 50 },
   151        }),
   152      );
   153    }
   154  
   155    const [compactDuration] = displayCompactDuration(duration);
   156  
   157    return (
   158      <span
   159        css={[durationBadge, css, colorScale(duration)]}
   160        className={className}
   161        onMouseOver={(e) => onShowTooltip(e.target as HTMLElement)}
   162        onFocus={(e) => onShowTooltip(e.target as HTMLElement)}
   163        onMouseOut={onHideTooltip}
   164        onBlur={onHideTooltip}
   165        data-testid="duration"
   166      >
   167        {compactDuration}
   168      </span>
   169    );
   170  }