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 "([^"]*)/;