github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/inputs/ComboInput/index.tsx (about)

     1  import CreatableSelect from "react-select/creatable";
     2  import useSelectInputStyles from "../common/useSelectInputStyles";
     3  import useSelectInputValues from "../common/useSelectInputValues";
     4  import { DashboardActions, DashboardDataModeLive } from "../../../../types";
     5  import { InputProps, SelectOption } from "../types";
     6  import {
     7    MultiValueLabelWithTags,
     8    OptionWithTags,
     9    SingleValueWithTags,
    10  } from "../common/Common";
    11  import { useDashboard } from "../../../../hooks/useDashboard";
    12  import { useEffect, useState } from "react";
    13  
    14  type SelectInputProps = InputProps & {
    15    multi?: boolean;
    16    name: string;
    17  };
    18  
    19  const getValueForState = (multi, option) => {
    20    if (multi) {
    21      // @ts-ignore
    22      return option.map((v) => v.value).join(",");
    23    } else {
    24      return option.value;
    25    }
    26  };
    27  
    28  const findOptionsForUrlValue = (
    29    options,
    30    multi,
    31    urlValue
    32  ): SelectOption | SelectOption[] => {
    33    // If we can't find any of the options in the data, we accept it, as this is a
    34    // combo box and the user can enter anything they like.
    35    if (multi) {
    36      const matchingOptions: SelectOption[] = [];
    37      for (const urlValuePart of urlValue) {
    38        const existingOption = options.find(
    39          (option) => option.value === urlValuePart
    40        );
    41        if (existingOption) {
    42          matchingOptions.push(existingOption);
    43        } else {
    44          matchingOptions.push({
    45            label: urlValuePart,
    46            value: urlValuePart,
    47          } as SelectOption);
    48        }
    49      }
    50      return matchingOptions;
    51    } else {
    52      const existingOption = options.find((option) => option.value === urlValue);
    53      if (existingOption) {
    54        return existingOption;
    55      } else {
    56        return {
    57          label: urlValue,
    58          value: urlValue,
    59        } as SelectOption;
    60      }
    61    }
    62  };
    63  
    64  const ComboInput = ({
    65    data,
    66    multi,
    67    name,
    68    properties,
    69    status,
    70  }: SelectInputProps) => {
    71    const { dataMode, dispatch, selectedDashboardInputs } = useDashboard();
    72    const [initialisedFromState, setInitialisedFromState] = useState(false);
    73    const [value, setValue] = useState<SelectOption | SelectOption[] | null>(
    74      null
    75    );
    76  
    77    // Get the options for the select
    78    const options = useSelectInputValues(properties.options, data, status);
    79  
    80    const stateValue = selectedDashboardInputs[name];
    81  
    82    // Bind the selected option to the reducer state
    83    useEffect(() => {
    84      // If we haven't got the data we need yet...
    85      if (
    86        // This property is only present in workspaces >=v0.16.x
    87        (status !== undefined && status !== "complete") ||
    88        !options ||
    89        options.length === 0
    90      ) {
    91        return;
    92      }
    93  
    94      // If this is first load, and we have a value from state, initialise it
    95      if (!initialisedFromState && stateValue) {
    96        const parsedUrlValue = multi ? stateValue.split(",") : stateValue;
    97        const foundOptions = findOptionsForUrlValue(
    98          options,
    99          multi,
   100          parsedUrlValue
   101        );
   102        setValue(foundOptions);
   103        setInitialisedFromState(true);
   104      } else if (!initialisedFromState && !stateValue && properties.placeholder) {
   105        setInitialisedFromState(true);
   106      } else if (
   107        !initialisedFromState &&
   108        !stateValue &&
   109        !properties.placeholder
   110      ) {
   111        setInitialisedFromState(true);
   112        const newValue = multi ? [options[0]] : options[0];
   113        setValue(newValue);
   114        dispatch({
   115          type: DashboardActions.SET_DASHBOARD_INPUT,
   116          name,
   117          value: getValueForState(multi, newValue),
   118          recordInputsHistory: false,
   119        });
   120      } else if (initialisedFromState && stateValue) {
   121        const parsedUrlValue = multi ? stateValue.split(",") : stateValue;
   122        const foundOptions = findOptionsForUrlValue(
   123          options,
   124          multi,
   125          parsedUrlValue
   126        );
   127        setValue(foundOptions);
   128      } else if (initialisedFromState && !stateValue) {
   129        if (properties.placeholder) {
   130          setValue(null);
   131        } else {
   132          const newValue = multi ? [options[0]] : options[0];
   133          setValue(newValue);
   134          dispatch({
   135            type: DashboardActions.SET_DASHBOARD_INPUT,
   136            name,
   137            value: getValueForState(multi, newValue),
   138            recordInputsHistory: false,
   139          });
   140        }
   141      }
   142    }, [
   143      dispatch,
   144      initialisedFromState,
   145      multi,
   146      name,
   147      options,
   148      properties.placeholder,
   149      stateValue,
   150      status,
   151    ]);
   152  
   153    const updateValue = (newValue) => {
   154      setValue(newValue);
   155      if (!newValue || newValue.length === 0) {
   156        dispatch({
   157          type: DashboardActions.DELETE_DASHBOARD_INPUT,
   158          name,
   159          recordInputsHistory: true,
   160        });
   161      } else {
   162        dispatch({
   163          type: DashboardActions.SET_DASHBOARD_INPUT,
   164          name,
   165          value: getValueForState(multi, newValue),
   166          recordInputsHistory: true,
   167        });
   168      }
   169    };
   170  
   171    const styles = useSelectInputStyles();
   172  
   173    if (!styles) {
   174      return null;
   175    }
   176  
   177    return (
   178      <form>
   179        {properties && properties.label && (
   180          <label
   181            className="block mb-1 text-sm"
   182            id={`${name}.label`}
   183            htmlFor={`${name}.input`}
   184          >
   185            {properties.label}
   186          </label>
   187        )}
   188        <CreatableSelect
   189          aria-labelledby={`${name}.input`}
   190          className="basic-single"
   191          classNamePrefix="select"
   192          components={{
   193            // @ts-ignore
   194            MultiValueLabel: MultiValueLabelWithTags,
   195            // @ts-ignore
   196            Option: OptionWithTags,
   197            // @ts-ignore
   198            SingleValue: SingleValueWithTags,
   199          }}
   200          createOptionPosition="first"
   201          formatCreateLabel={(inputValue) => `Use "${inputValue}"`}
   202          // @ts-ignore as this element definitely exists
   203          menuPortalTarget={document.getElementById("portals")}
   204          inputId={`${name}.input`}
   205          isDisabled={
   206            (!properties.options && !data) || dataMode !== DashboardDataModeLive
   207          }
   208          isLoading={!properties.options && !data}
   209          isClearable={!!properties.placeholder}
   210          isRtl={false}
   211          isSearchable
   212          isMulti={multi}
   213          // menuIsOpen
   214          name={name}
   215          // @ts-ignore
   216          onChange={updateValue}
   217          options={options}
   218          placeholder={
   219            properties && properties.placeholder ? properties.placeholder : null
   220          }
   221          styles={styles}
   222          value={value}
   223        />
   224      </form>
   225    );
   226  };
   227  
   228  export default ComboInput;