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 }