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 };