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;