go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/monitoring/components/alert_table/bug_menu.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 { Alert, Divider, Menu, MenuItem, Snackbar } from '@mui/material';
    16  import { useMutation, useQueryClient } from '@tanstack/react-query';
    17  import { useState } from 'react';
    18  
    19  import { FileBugDialog } from '@/monitoring/components/file_bug_dialog/file_bug_dialog';
    20  import { linkBug, unlinkBug } from '@/monitoring/util/bug_annotations';
    21  import { AlertJson, Bug, BugId, TreeJson } from '@/monitoring/util/server_json';
    22  
    23  interface BugMenuProps {
    24    anchorEl: HTMLElement | null;
    25    onClose: () => void;
    26    tree: TreeJson;
    27    alerts: AlertJson[];
    28    bugs: Bug[];
    29    alertBugs: { [alertKey: string]: BugId[] };
    30  }
    31  // TODO(b/319315200): Dialog to confirm multiple alert bug linking
    32  // TODO(b/319315200): Unlink before linking to another bug + dialog to confirm it
    33  export const BugMenu = ({
    34    anchorEl,
    35    onClose,
    36    alerts,
    37    tree,
    38    bugs,
    39    alertBugs,
    40  }: BugMenuProps) => {
    41    const [linkBugOpen, setLinkBugOpen] = useState(false);
    42    const open = Boolean(anchorEl) && !linkBugOpen;
    43    const queryClient = useQueryClient();
    44  
    45    const isLinkedToBugs =
    46      alerts.filter((a) => alertBugs[a.key]?.length > 0).length > 0;
    47  
    48    const linkBugMutation = useMutation({
    49      mutationFn: (bug: string) => {
    50        return linkBug(tree, alerts, bug);
    51      },
    52      onSuccess: () => queryClient.invalidateQueries(['annotations']),
    53    });
    54    const unlinkBugMutation = useMutation({
    55      mutationFn: () => {
    56        const promises: Promise<unknown>[] = [];
    57        for (const alert of alerts) {
    58          promises.push(unlinkBug(tree, alert, alertBugs[alert.key]));
    59        }
    60        return Promise.all(promises);
    61      },
    62      onSuccess: () => queryClient.invalidateQueries(['annotations']),
    63    });
    64    return (
    65      <>
    66        <Menu id="basic-menu" anchorEl={anchorEl} open={open} onClose={onClose}>
    67          {isLinkedToBugs ? (
    68            <>
    69              <MenuItem
    70                onClick={(e) => {
    71                  e.stopPropagation();
    72                  unlinkBugMutation.mutateAsync().finally(() => onClose());
    73                }}
    74              >
    75                Unlink bug {alerts.length !== 1 && 'from all alerts'}
    76              </MenuItem>
    77              <Divider />
    78            </>
    79          ) : null}
    80          {bugs.map((bug) => (
    81            <MenuItem
    82              key={bug.link}
    83              onClick={(e) => {
    84                e.stopPropagation();
    85                linkBugMutation
    86                  .mutateAsync(`${bug.number}`)
    87                  .finally(() => onClose());
    88              }}
    89            >
    90              {bug.summary}
    91            </MenuItem>
    92          ))}
    93          {bugs.length > 0 ? <Divider /> : null}
    94          <MenuItem
    95            onClick={(e) => {
    96              e.stopPropagation();
    97              setLinkBugOpen(true);
    98            }}
    99          >
   100            Create bug...
   101          </MenuItem>
   102        </Menu>
   103        <FileBugDialog
   104          alerts={alerts}
   105          tree={tree}
   106          open={linkBugOpen}
   107          onClose={() => {
   108            setLinkBugOpen(false);
   109            onClose();
   110          }}
   111        />
   112        <Snackbar open={linkBugMutation.isError}>
   113          <Alert severity="error">
   114            Error saving bug links: {linkBugMutation.error as string}
   115          </Alert>
   116        </Snackbar>
   117      </>
   118    );
   119  };