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;