go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/monitoring/components/alert_table/summary_row.tsx (about) 1 // Copyright 2024 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 BugReportIcon from '@mui/icons-material/BugReport'; 16 import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 17 import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 18 import NotificationsIcon from '@mui/icons-material/Notifications'; 19 import NotificationsPausedIcon from '@mui/icons-material/NotificationsPaused'; 20 import { IconButton, TableCell, TableRow, Tooltip } from '@mui/material'; 21 import { Link } from '@mui/material'; 22 import { useState } from 'react'; 23 24 import { 25 AlertBuilderJson, 26 AlertJson, 27 TreeJson, 28 BugId, 29 Bug, 30 } from '@/monitoring/util/server_json'; 31 32 import { BugMenu } from './bug_menu'; 33 34 interface AlertSummaryRowProps { 35 alert: AlertJson; 36 builder: AlertBuilderJson; 37 expanded: boolean; 38 onExpand: () => void; 39 tree: TreeJson; 40 bugs: Bug[]; 41 alertBugs: { [alertKey: string]: BugId[] }; 42 } 43 // An expandable row in the AlertTable containing a summary of a single alert. 44 export const AlertSummaryRow = ({ 45 alert, 46 builder, 47 expanded, 48 onExpand, 49 tree, 50 bugs, 51 alertBugs, 52 }: AlertSummaryRowProps) => { 53 const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); 54 55 const step = stepRe.exec(alert.title)?.[1]; 56 // TODO: snoozing. 57 const snoozed = false; 58 const numTestFailures = alert.extension?.reason?.num_failing_tests || 0; 59 const firstTestFailureName = shortTestName( 60 alert.extension?.reason?.tests?.[0].test_name, 61 ); 62 const failureCount = 63 builder.first_failure_build_number === 0 64 ? undefined 65 : builder.latest_failure_build_number - 66 builder.first_failure_build_number + 67 1; 68 69 return ( 70 <TableRow hover onClick={() => onExpand()} sx={{ cursor: 'pointer' }}> 71 <TableCell> 72 <IconButton> 73 {expanded ? <ExpandMoreIcon /> : <ChevronRightIcon />} 74 </IconButton> 75 </TableCell> 76 <TableCell> 77 <Link 78 href={builder.url} 79 target="_blank" 80 rel="noreferrer" 81 onClick={(e) => e.stopPropagation()} 82 > 83 {builder.name} 84 </Link> 85 </TableCell> 86 <TableCell> 87 {step} 88 {numTestFailures > 0 && ( 89 <span style={{ opacity: '0.7', marginLeft: '5px' }}> 90 {firstTestFailureName} 91 {numTestFailures > 1 && <> + {numTestFailures - 1} more</>} 92 </span> 93 )} 94 </TableCell> 95 <TableCell> 96 {failureCount && ( 97 <> 98 {failureCount} build{failureCount > 1 && 's'}:{' '} 99 </> 100 )} 101 {builder.first_failure_url !== '' ? ( 102 <Link 103 href={builder.first_failure_url} 104 target="_blank" 105 rel="noreferrer" 106 onClick={(e) => e.stopPropagation()} 107 > 108 {builder.first_failure_build_number} 109 </Link> 110 ) : ( 111 'Unknown' 112 )} 113 {builder.latest_failure_build_number !== 114 builder.first_failure_build_number ? ( 115 <> 116 {' - '} 117 <Link 118 href={builder.latest_failure_url} 119 target="_blank" 120 rel="noreferrer" 121 onClick={(e) => e.stopPropagation()} 122 > 123 {builder.latest_failure_build_number} 124 </Link> 125 </> 126 ) : null} 127 </TableCell> 128 <TableCell> 129 {builder.first_failure_url === '' ? ( 130 'Unknown' 131 ) : ( 132 <Link 133 href={builder.first_failure_url + '/blamelist'} 134 target="_blank" 135 rel="noreferrer" 136 onClick={(e) => e.stopPropagation()} 137 > 138 {builder.first_failing_rev?.commit_position && 139 builder.last_passing_rev?.commit_position ? ( 140 <> 141 {builder.first_failing_rev?.commit_position - 142 builder.last_passing_rev?.commit_position}{' '} 143 CL 144 {builder.first_failing_rev?.commit_position - 145 builder.last_passing_rev?.commit_position > 146 1 && 's'} 147 </> 148 ) : ( 149 'Blamelist' 150 )} 151 </Link> 152 )} 153 </TableCell> 154 <TableCell> 155 <div style={{ display: 'flex' }}> 156 <Tooltip title="Link bug"> 157 <IconButton 158 onClick={(e) => { 159 e.stopPropagation(); 160 setMenuAnchorEl(e.currentTarget); 161 }} 162 > 163 <BugReportIcon /> 164 </IconButton> 165 </Tooltip> 166 <BugMenu 167 anchorEl={menuAnchorEl} 168 onClose={() => setMenuAnchorEl(null)} 169 alerts={[alert]} 170 tree={tree} 171 bugs={bugs} 172 alertBugs={alertBugs} 173 /> 174 175 <Tooltip title="Snooze alert for 60 minutes"> 176 <IconButton onClick={(e) => e.stopPropagation()}> 177 {snoozed ? <NotificationsIcon /> : <NotificationsPausedIcon />} 178 </IconButton> 179 </Tooltip> 180 </div> 181 </TableCell> 182 </TableRow> 183 ); 184 }; 185 186 // shortTestName applies various heuristics to try to get the best test name in less than 80 characters. 187 const shortTestName = (name: string | null | undefined): string | undefined => { 188 if (!name) { 189 return undefined; 190 } 191 const parts = name.split('/'); 192 let short = parts.pop(); 193 while (parts.length && short && short.length < 5) { 194 short = parts.pop() + '/' + short; 195 } 196 if (short && short?.length <= 80) { 197 return short; 198 } 199 return short?.slice(0, 77) + '...'; 200 }; 201 202 // stepRE extracts the step name from an alert title. 203 const stepRe = /Step "([^"]*)/;