go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/frontend/ui/src/components/bug_picker/bug_picker.tsx (about) 1 // Copyright 2022 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 { ChangeEvent } from 'react'; 16 import { useParams } from 'react-router-dom'; 17 18 import CircularProgress from '@mui/material/CircularProgress'; 19 import FormControl from '@mui/material/FormControl'; 20 import Grid from '@mui/material/Grid'; 21 import InputLabel from '@mui/material/InputLabel'; 22 import MenuItem from '@mui/material/MenuItem'; 23 import Select, { SelectChangeEvent } from '@mui/material/Select'; 24 import TextField from '@mui/material/TextField'; 25 26 import ErrorAlert from '@/components/error_alert/error_alert'; 27 import LoadErrorAlert from '@/components/load_error_alert/load_error_alert'; 28 import { useFetchProjectConfig } from '@/hooks/use_fetch_project_config'; 29 30 interface Props { 31 bugSystem: string; 32 bugId: string; 33 handleBugSystemChanged: (bugSystem: string) => void; 34 handleBugIdChanged: (bugId: string) => void; 35 } 36 37 const getMonorailSystem = (bugId: string): string | null => { 38 if (bugId.indexOf('/') >= 0) { 39 const parts = bugId.split('/'); 40 return parts[0]; 41 } else { 42 return null; 43 } 44 }; 45 46 const getBugNumber = (bugId: string): string => { 47 if (bugId.indexOf('/') >= 0) { 48 const parts = bugId.split('/'); 49 return parts[1]; 50 } else { 51 return bugId; 52 } 53 }; 54 55 /** 56 * An enum representing the supported bug systems. 57 * 58 * This is needed because mui's <Select> doesn't compare string correctly in typescript. 59 */ 60 enum BugSystems { 61 EMPTY = '', 62 MONORAIL = 'monorail', 63 BUGANIZER = 'buganizer', 64 } 65 66 /** 67 * This method works around the fact that Select 68 * components compare strings for reference. 69 * 70 * @param {string} bugSystem The bug system to find in the enum. 71 * @return {string} A static enum value equal to the string 72 * provided and used in the Select component. 73 */ 74 const getStaticBugSystem = (bugSystem: string): string => { 75 switch (bugSystem) { 76 case '': { 77 return BugSystems.EMPTY; 78 } 79 case 'monorail': { 80 return BugSystems.MONORAIL; 81 } 82 case 'buganizer': { 83 return BugSystems.BUGANIZER; 84 } 85 default: { 86 throw new Error('Unknown bug system.'); 87 } 88 } 89 }; 90 91 const BugPicker = ({ 92 bugSystem, 93 bugId, 94 handleBugSystemChanged, 95 handleBugIdChanged, 96 }: Props) => { 97 const { project } = useParams(); 98 99 const { 100 isLoading, 101 data: projectConfig, 102 error, 103 } = useFetchProjectConfig(project || ''); 104 105 if (!project) { 106 return ( 107 <ErrorAlert 108 showError 109 errorTitle="Project not defined" 110 errorText={'No project param detected.}'}/> 111 ); 112 } 113 114 const selectedBugSystem = getStaticBugSystem(bugSystem); 115 116 if (isLoading || !projectConfig) { 117 return ( 118 <Grid container justifyContent="center"> 119 <CircularProgress data-testid="circle-loading" /> 120 </Grid> 121 ); 122 } 123 124 if (error) { 125 return <LoadErrorAlert 126 entityName="project config" 127 error={error} 128 />; 129 } 130 131 const monorailSystem = getMonorailSystem(bugId); 132 133 const onBugSystemChange = (e: SelectChangeEvent<typeof bugSystem>) => { 134 handleBugSystemChanged(e.target.value); 135 136 // When the bug system changes, we also need to update the Bug ID. 137 if (e.target.value == 'monorail') { 138 // Switching to monorail is disabled if the project config does not have 139 // monorail details, so we can assume it exists. 140 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 141 const monorailConfig = projectConfig.bugManagement!.monorail!; 142 handleBugIdChanged(`${monorailConfig.project}/${getBugNumber(bugId)}`); 143 } else if (e.target.value == 'buganizer') { 144 handleBugIdChanged(getBugNumber(bugId)); 145 } 146 }; 147 148 const onBugNumberChange = (e: ChangeEvent<HTMLInputElement>) => { 149 const enteredBugId = e.target.value; 150 151 if (monorailSystem != null) { 152 handleBugIdChanged(`${monorailSystem}/${enteredBugId}`); 153 } else { 154 handleBugIdChanged(enteredBugId); 155 } 156 }; 157 158 return ( 159 <Grid container item columnSpacing={1}> 160 <Grid item xs={6}> 161 <FormControl variant="standard" fullWidth> 162 <InputLabel id="bug-picker_select-bug-tracker-label">Bug tracker</InputLabel> 163 <Select 164 labelId="bug-picker_select-bug-tracker-label" 165 id="bug-picker_select-bug-tracker" 166 value={selectedBugSystem} 167 onChange={onBugSystemChange} 168 variant="standard" 169 inputProps={{ 'data-testid': 'bug-system' }}> 170 <MenuItem value={getStaticBugSystem('monorail')} disabled={!projectConfig.bugManagement?.monorail}> 171 {projectConfig.bugManagement?.monorail?.displayPrefix || 'monorail'} 172 </MenuItem> 173 <MenuItem value={getStaticBugSystem('buganizer')}> 174 Buganizer 175 </MenuItem> 176 </Select> 177 </FormControl> 178 </Grid> 179 <Grid item xs={6}> 180 <TextField 181 label="Bug number" 182 variant="standard" 183 inputProps={{ 'data-testid': 'bug-number' }} 184 value={getBugNumber(bugId)} 185 onChange={onBugNumberChange}/> 186 </Grid> 187 </Grid> 188 ); 189 }; 190 191 export default BugPicker;