github.com/minio/console@v1.4.1/web-app/src/screens/Console/Tools/Inspect.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 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 17 import React, { Fragment, useEffect, useState } from "react"; 18 import { 19 Box, 20 breakPoints, 21 Button, 22 FormLayout, 23 HelpBox, 24 InputBox, 25 InspectMenuIcon, 26 PageLayout, 27 PasswordKeyIcon, 28 Switch, 29 } from "mds"; 30 import { useNavigate } from "react-router-dom"; 31 import { useSelector } from "react-redux"; 32 import { 33 deleteCookie, 34 encodeURLString, 35 getCookieValue, 36 performDownload, 37 } from "../../../common/utils"; 38 import { 39 selDistSet, 40 setErrorSnackMessage, 41 setHelpName, 42 } from "../../../systemSlice"; 43 import { useAppDispatch } from "../../../store"; 44 import { registeredCluster } from "../../../config"; 45 import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; 46 import DistributedOnly from "../Common/DistributedOnly/DistributedOnly"; 47 import KeyRevealer from "./KeyRevealer"; 48 import RegisterCluster from "../Support/RegisterCluster"; 49 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 50 import HelpMenu from "../HelpMenu"; 51 52 const ExampleBlock = ({ 53 volumeVal, 54 pathVal, 55 }: { 56 volumeVal: string; 57 pathVal: string; 58 }) => { 59 return ( 60 <Box className="code-block-container"> 61 <Box className="example-code-block"> 62 <Box 63 sx={{ 64 display: "flex", 65 marginBottom: "5px", 66 flexFlow: "row", 67 [`@media (max-width: ${breakPoints.sm}px)`]: { 68 flexFlow: "column", 69 }, 70 }} 71 > 72 <label>Volume/bucket Name :</label> <code>{volumeVal}</code> 73 </Box> 74 <Box 75 sx={{ 76 display: "flex", 77 flexFlow: "row", 78 [`@media (max-width: ${breakPoints.sm}px)`]: { 79 flexFlow: "column", 80 }, 81 }} 82 > 83 <label>Path : </label> 84 <code>{pathVal}</code> 85 </Box> 86 </Box> 87 </Box> 88 ); 89 }; 90 91 const Inspect = () => { 92 const dispatch = useAppDispatch(); 93 const navigate = useNavigate(); 94 const distributedSetup = useSelector(selDistSet); 95 96 const [volumeName, setVolumeName] = useState<string>(""); 97 const [inspectPath, setInspectPath] = useState<string>(""); 98 const [isEncrypt, setIsEncrypt] = useState<boolean>(true); 99 100 const [decryptionKey, setDecryptionKey] = useState<string>(""); 101 102 const [insFileName, setInsFileName] = useState<string>(""); 103 104 const [isFormValid, setIsFormValid] = useState<boolean>(false); 105 const [volumeError, setVolumeError] = useState<string>(""); 106 const [pathError, setPathError] = useState<string>(""); 107 const clusterRegistered = registeredCluster(); 108 /** 109 * Validation Effect 110 */ 111 useEffect(() => { 112 let isVolValid; 113 let isPathValid; 114 115 isVolValid = volumeName.trim().length > 0; 116 if (!isVolValid) { 117 setVolumeError("This field is required"); 118 } else if (volumeName.slice(0, 1) === "/") { 119 isVolValid = false; 120 setVolumeError("Volume/Bucket name cannot start with /"); 121 } 122 isPathValid = inspectPath.trim().length > 0; 123 if (!inspectPath) { 124 setPathError("This field is required"); 125 } else if (inspectPath.slice(0, 1) === "/") { 126 isPathValid = false; 127 setPathError("Path cannot start with /"); 128 } 129 const isValid = isVolValid && isPathValid; 130 131 if (isVolValid) { 132 setVolumeError(""); 133 } 134 if (isPathValid) { 135 setPathError(""); 136 } 137 138 setIsFormValid(isValid); 139 }, [volumeName, inspectPath]); 140 141 const makeRequest = async (url: string) => { 142 return await fetch(url, { method: "GET" }); 143 }; 144 145 const performInspect = async () => { 146 const file = encodeURLString(inspectPath); 147 const volume = encodeURLString(volumeName); 148 149 let basename = document.baseURI.replace(window.location.origin, ""); 150 const urlOfInspectApi = `${basename}/api/v1/admin/inspect?volume=${volume}&file=${file}&encrypt=${isEncrypt}`; 151 152 makeRequest(urlOfInspectApi) 153 .then(async (res) => { 154 if (!res.ok) { 155 const resErr: any = await res.json(); 156 157 dispatch( 158 setErrorSnackMessage({ 159 errorMessage: resErr.message, 160 detailedError: resErr.code, 161 }), 162 ); 163 } 164 const blob: Blob = await res.blob(); 165 166 //@ts-ignore 167 const filename = res.headers.get("content-disposition").split('"')[1]; 168 const decryptKey = getCookieValue(filename) || ""; 169 170 performDownload(blob, filename); 171 setInsFileName(filename); 172 setDecryptionKey(decryptKey); 173 }) 174 .catch((err) => { 175 dispatch(setErrorSnackMessage(err)); 176 }); 177 }; 178 179 const resetForm = () => { 180 setVolumeName(""); 181 setInspectPath(""); 182 setIsEncrypt(true); 183 }; 184 185 const onCloseDecKeyModal = () => { 186 deleteCookie(insFileName); 187 setDecryptionKey(""); 188 resetForm(); 189 }; 190 191 useEffect(() => { 192 dispatch(setHelpName("inspect")); 193 // eslint-disable-next-line react-hooks/exhaustive-deps 194 }, []); 195 196 return ( 197 <Fragment> 198 <PageHeaderWrapper label={"Inspect"} actions={<HelpMenu />} /> 199 200 <PageLayout> 201 {!clusterRegistered && <RegisterCluster compactMode />} 202 {!distributedSetup ? ( 203 <DistributedOnly 204 iconComponent={<InspectMenuIcon />} 205 entity={"Inspect"} 206 /> 207 ) : ( 208 <FormLayout 209 helpBox={ 210 <HelpBox 211 title={"Learn more about the Inspect feature"} 212 iconComponent={<InspectMenuIcon />} 213 help={ 214 <Fragment> 215 <Box 216 sx={{ 217 marginTop: "16px", 218 fontWeight: 600, 219 fontStyle: "italic", 220 fontSize: "14px", 221 }} 222 > 223 Examples: 224 </Box> 225 226 <Box 227 sx={{ 228 display: "flex", 229 flexFlow: "column", 230 fontSize: "14px", 231 flex: "2", 232 233 "& .step-row": { 234 fontSize: "14px", 235 display: "flex", 236 marginTop: "15px", 237 marginBottom: "15px", 238 239 "&.step-text": { 240 fontWeight: 400, 241 }, 242 "&:before": { 243 content: "' '", 244 height: "7px", 245 width: "7px", 246 backgroundColor: "#2781B0", 247 marginRight: "10px", 248 marginTop: "7px", 249 flexShrink: 0, 250 }, 251 }, 252 253 "& .code-block-container": { 254 flex: "1", 255 marginTop: "15px", 256 marginLeft: "35px", 257 258 "& input": { 259 color: "#737373", 260 }, 261 }, 262 263 "& .example-code-block label": { 264 display: "inline-block", 265 width: 160, 266 fontWeight: 600, 267 fontSize: 14, 268 [`@media (max-width: ${breakPoints.sm}px)`]: { 269 width: "100%", 270 }, 271 }, 272 273 "& code": { 274 width: 100, 275 paddingLeft: "10px", 276 fontFamily: "monospace", 277 paddingRight: "10px", 278 paddingTop: "3px", 279 paddingBottom: "3px", 280 borderRadius: "2px", 281 border: "1px solid #eaeaea", 282 fontSize: "10px", 283 fontWeight: 500, 284 [`@media (max-width: ${breakPoints.sm}px)`]: { 285 width: "100%", 286 }, 287 }, 288 "& .spacer": { 289 marginBottom: "5px", 290 }, 291 }} 292 > 293 <Box> 294 <Box className="step-row"> 295 <div className="step-text"> 296 To Download 'xl.meta' for a specific object from all 297 the drives in a zip file: 298 </div> 299 </Box> 300 301 <ExampleBlock 302 pathVal={`test*/xl.meta`} 303 volumeVal={`test-bucket`} 304 /> 305 </Box> 306 307 <Box> 308 <Box className="step-row"> 309 <div className="step-text"> 310 To Download all constituent parts for a specific 311 object, and optionally encrypt the downloaded zip: 312 </div> 313 </Box> 314 315 <ExampleBlock 316 pathVal={`test*/xl.meta`} 317 volumeVal={`test*/*/part.*`} 318 /> 319 </Box> 320 <Box> 321 <Box className="step-row"> 322 <div className="step-text"> 323 To Download recursively all objects at a prefix. 324 <br /> 325 NOTE: This can be an expensive operation use it with 326 caution. 327 </div> 328 </Box> 329 <ExampleBlock 330 pathVal={`test*/xl.meta`} 331 volumeVal={`test/**`} 332 /> 333 </Box> 334 </Box> 335 336 <Box 337 sx={{ 338 marginTop: "30px", 339 marginLeft: "15px", 340 fontSize: "14px", 341 }} 342 > 343 You can learn more at our{" "} 344 <a 345 href="https://github.com/minio/minio/tree/master/docs/debugging?ref=con" 346 target="_blank" 347 rel="noopener" 348 > 349 documentation 350 </a> 351 . 352 </Box> 353 </Fragment> 354 } 355 /> 356 } 357 > 358 <form 359 noValidate 360 autoComplete="off" 361 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 362 e.preventDefault(); 363 if (!clusterRegistered) { 364 navigate("/support/register"); 365 return; 366 } 367 performInspect(); 368 }} 369 > 370 <InputBox 371 id="inspect_volume" 372 name="inspect_volume" 373 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 374 setVolumeName(e.target.value); 375 }} 376 label="Volume or Bucket Name" 377 value={volumeName} 378 error={volumeError} 379 required 380 placeholder={"test-bucket"} 381 disabled={!clusterRegistered} 382 /> 383 <InputBox 384 id="inspect_path" 385 name="inspect_path" 386 error={pathError} 387 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 388 setInspectPath(e.target.value); 389 }} 390 label="File or Path to inspect" 391 value={inspectPath} 392 required 393 placeholder={"test*/xl.meta"} 394 disabled={!clusterRegistered} 395 /> 396 <Switch 397 label="Encrypt" 398 indicatorLabels={["True", "False"]} 399 checked={isEncrypt} 400 value={"true"} 401 id="inspect_encrypt" 402 name="inspect_encrypt" 403 onChange={() => { 404 setIsEncrypt(!isEncrypt); 405 }} 406 disabled={!clusterRegistered} 407 /> 408 <Box 409 sx={{ 410 display: "flex", 411 alignItems: "center", 412 justifyContent: "flex-end", 413 marginTop: "55px", 414 }} 415 > 416 <Button 417 id={"inspect-clear-button"} 418 style={{ 419 marginRight: "15px", 420 }} 421 type="button" 422 variant="regular" 423 data-test-id="inspect-clear-button" 424 onClick={resetForm} 425 label={"Clear"} 426 disabled={!clusterRegistered} 427 /> 428 <Button 429 id={"inspect-start"} 430 type="submit" 431 variant={!clusterRegistered ? "regular" : "callAction"} 432 data-test-id="inspect-submit-button" 433 disabled={!isFormValid || !clusterRegistered} 434 label={"Inspect"} 435 /> 436 </Box> 437 </form> 438 </FormLayout> 439 )} 440 {decryptionKey ? ( 441 <ModalWrapper 442 modalOpen={true} 443 title="Inspect Decryption Key" 444 onClose={onCloseDecKeyModal} 445 titleIcon={<PasswordKeyIcon />} 446 > 447 <Fragment> 448 <Box> 449 This will be displayed only once. It cannot be recovered. 450 <br /> 451 Use secure medium to share this key. 452 </Box> 453 <form 454 noValidate 455 onSubmit={() => { 456 return false; 457 }} 458 > 459 <KeyRevealer value={decryptionKey} /> 460 </form> 461 </Fragment> 462 </ModalWrapper> 463 ) : null} 464 </PageLayout> 465 </Fragment> 466 ); 467 }; 468 469 export default Inspect;