github.com/minio/console@v1.4.1/web-app/src/screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 import React, { 17 ChangeEvent, 18 createRef, 19 Fragment, 20 useEffect, 21 useLayoutEffect, 22 useRef, 23 useState, 24 } from "react"; 25 import get from "lodash/get"; 26 import debounce from "lodash/debounce"; 27 import { 28 AddIcon, 29 Box, 30 Grid, 31 HelpIcon, 32 InputBox, 33 InputLabel, 34 Tooltip, 35 } from "mds"; 36 37 interface IQueryMultiSelector { 38 elements: string; 39 name: string; 40 label: string; 41 tooltip?: string; 42 keyPlaceholder?: string; 43 valuePlaceholder?: string; 44 withBorder?: boolean; 45 onChange: (elements: string) => void; 46 } 47 48 const QueryMultiSelector = ({ 49 elements, 50 name, 51 label, 52 tooltip = "", 53 keyPlaceholder = "", 54 valuePlaceholder = "", 55 onChange, 56 withBorder = false, 57 }: IQueryMultiSelector) => { 58 const [currentKeys, setCurrentKeys] = useState<string[]>([""]); 59 const [currentValues, setCurrentValues] = useState<string[]>([""]); 60 const bottomList = createRef<HTMLDivElement>(); 61 62 // Use effect to get the initial values from props 63 useEffect(() => { 64 if ( 65 currentKeys.length === 1 && 66 currentKeys[0] === "" && 67 currentValues.length === 1 && 68 currentValues[0] === "" && 69 elements && 70 elements !== "" 71 ) { 72 const elementsSplit = elements.split("&"); 73 let keys = []; 74 let values = []; 75 76 elementsSplit.forEach((element: string) => { 77 const splittedVals = element.split("="); 78 if (splittedVals.length === 2) { 79 keys.push(splittedVals[0]); 80 values.push(splittedVals[1]); 81 } 82 }); 83 84 keys.push(""); 85 values.push(""); 86 87 setCurrentKeys(keys); 88 setCurrentValues(values); 89 } 90 }, [currentKeys, currentValues, elements]); 91 92 // Use effect to send new values to onChange 93 useEffect(() => { 94 const refScroll = bottomList.current; 95 if (refScroll && currentKeys.length > 1) { 96 refScroll.scrollIntoView(false); 97 } 98 // eslint-disable-next-line react-hooks/exhaustive-deps 99 }, [currentKeys]); 100 101 // We avoid multiple re-renders / hang issue typing too fast 102 const firstUpdate = useRef(true); 103 useLayoutEffect(() => { 104 if (firstUpdate.current) { 105 firstUpdate.current = false; 106 return; 107 } 108 debouncedOnChange(); 109 // eslint-disable-next-line react-hooks/exhaustive-deps 110 }, [currentKeys, currentValues]); 111 112 // If the last input is not empty, we add a new one 113 const addEmptyLine = () => { 114 if ( 115 currentKeys[currentKeys.length - 1].trim() !== "" && 116 currentValues[currentValues.length - 1].trim() !== "" 117 ) { 118 const keysList = [...currentKeys]; 119 const valuesList = [...currentValues]; 120 121 keysList.push(""); 122 valuesList.push(""); 123 124 setCurrentKeys(keysList); 125 setCurrentValues(valuesList); 126 } 127 }; 128 129 // Onchange function for input box, we get the dataset-index & only update that value in the array 130 const onChangeKey = (e: ChangeEvent<HTMLInputElement>) => { 131 e.persist(); 132 133 let updatedElement = [...currentKeys]; 134 const index = get(e.target, "dataset.index", "0"); 135 const indexNum = parseInt(index); 136 updatedElement[indexNum] = e.target.value; 137 138 setCurrentKeys(updatedElement); 139 }; 140 141 const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => { 142 e.persist(); 143 144 let updatedElement = [...currentValues]; 145 const index = get(e.target, "dataset.index", "0"); 146 const indexNum = parseInt(index); 147 updatedElement[indexNum] = e.target.value; 148 149 setCurrentValues(updatedElement); 150 }; 151 152 // Debounce for On Change 153 const debouncedOnChange = debounce(() => { 154 let queryString = ""; 155 156 currentKeys.forEach((keyVal, index) => { 157 if (currentKeys[index] && currentValues[index]) { 158 let insertString = `${keyVal}=${currentValues[index]}`; 159 if (index !== 0) { 160 insertString = `&${insertString}`; 161 } 162 queryString = `${queryString}${insertString}`; 163 } 164 }); 165 166 onChange(queryString); 167 }, 500); 168 169 const inputs = currentValues.map((element, index) => { 170 return ( 171 <Grid 172 item 173 xs={12} 174 className={"lineInputBoxes inputItem"} 175 key={`query-pair-${name}-${index.toString()}`} 176 > 177 <InputBox 178 id={`${name}-key-${index.toString()}`} 179 label={""} 180 name={`${name}-${index.toString()}`} 181 value={currentKeys[index]} 182 onChange={onChangeKey} 183 index={index} 184 placeholder={keyPlaceholder} 185 /> 186 <span className={"queryDiv"}>:</span> 187 <InputBox 188 id={`${name}-value-${index.toString()}`} 189 label={""} 190 name={`${name}-${index.toString()}`} 191 value={currentValues[index]} 192 onChange={onChangeValue} 193 index={index} 194 placeholder={valuePlaceholder} 195 overlayIcon={index === currentValues.length - 1 ? <AddIcon /> : null} 196 overlayAction={() => { 197 addEmptyLine(); 198 }} 199 /> 200 </Grid> 201 ); 202 }); 203 204 return ( 205 <Fragment> 206 <Grid 207 item 208 xs={12} 209 sx={{ 210 "& .lineInputBoxes": { 211 display: "flex", 212 }, 213 "& .queryDiv": { 214 alignSelf: "center", 215 margin: "-15px 4px 0", 216 fontWeight: 600, 217 }, 218 }} 219 className={"inputItem"} 220 > 221 <InputLabel> 222 {label} 223 {tooltip !== "" && ( 224 <Box 225 sx={{ 226 marginLeft: 5, 227 display: "flex", 228 alignItems: "center", 229 "& .min-icon": { 230 width: 13, 231 }, 232 }} 233 > 234 <Tooltip tooltip={tooltip} placement="top"> 235 <HelpIcon style={{ width: 13, height: 13 }} /> 236 </Tooltip> 237 </Box> 238 )} 239 </InputLabel> 240 <Box 241 withBorders={withBorder} 242 sx={{ 243 padding: 15, 244 height: 150, 245 overflowY: "auto", 246 position: "relative", 247 marginTop: 15, 248 }} 249 > 250 {inputs} 251 <div ref={bottomList} /> 252 </Box> 253 </Grid> 254 </Fragment> 255 ); 256 }; 257 export default QueryMultiSelector;