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;