github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/shared/components/array-input/array-input.tsx (about) 1 import * as React from 'react'; 2 import * as ReactForm from 'react-form'; 3 import {FormValue} from 'react-form'; 4 5 /* 6 This provide a way to may a form field to an array of items. It allows you to 7 8 * Add a new (maybe duplicate) item. 9 * Replace an item. 10 * Remove an item. 11 12 E.g. 13 env: 14 - name: FOO 15 value: bar 16 - name: BAZ 17 value: qux 18 # You can have dup items 19 - name: FOO 20 value: bar 21 22 It does not allow re-ordering of elements (maybe in a v2). 23 */ 24 25 export interface NameValue { 26 name: string; 27 value: string; 28 } 29 30 export const NameValueEditor = (item: NameValue, onChange?: (item: NameValue) => any) => { 31 return ( 32 <React.Fragment> 33 <input 34 // disable chrome autocomplete 35 autoComplete='fake' 36 className='argo-field' 37 style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}} 38 placeholder='Name' 39 value={item.name} 40 onChange={e => onChange({...item, name: e.target.value})} 41 // onBlur={e=>onChange({...item, name: e.target.value})} 42 title='Name' 43 readOnly={!onChange} 44 /> 45 = 46 <input 47 // disable chrome autocomplete 48 autoComplete='fake' 49 className='argo-field' 50 style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}} 51 placeholder='Value' 52 value={item.value || ''} 53 onChange={e => onChange({...item, value: e.target.value})} 54 title='Value' 55 readOnly={!onChange} 56 /> 57 58 </React.Fragment> 59 ); 60 }; 61 62 export const ValueEditor = (item: string, onChange: (item: string) => any) => { 63 return ( 64 <input 65 // disable chrome autocomplete 66 autoComplete='fake' 67 className='argo-field' 68 style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}} 69 placeholder='Value' 70 value={item || ''} 71 onChange={e => onChange(e.target.value)} 72 title='Value' 73 readOnly={!onChange} 74 /> 75 ); 76 }; 77 78 interface Props<T> { 79 items: T[]; 80 onChange: (items: T[]) => void; 81 editor: (item: T, onChange: (updated: T) => any) => React.ReactNode; 82 } 83 84 export function ArrayInput<T>(props: Props<T>) { 85 const addItem = (item: T) => { 86 props.onChange([...props.items, item]); 87 }; 88 89 const replaceItem = (item: T, i: number) => { 90 const items = props.items.slice(); 91 items[i] = item; 92 props.onChange(items); 93 }; 94 95 const removeItem = (i: number) => { 96 const items = props.items.slice(); 97 items.splice(i, 1); 98 props.onChange(items); 99 }; 100 101 return ( 102 <div className='argo-field' style={{border: 0, marginTop: '15px', zIndex: 1}}> 103 {props.items.map((item, i) => ( 104 <div key={`item-${i}`} style={{marginBottom: '5px'}}> 105 {props.editor(item, (updated: T) => replaceItem(updated, i))} 106 107 <button> 108 <i className='fa fa-times' style={{cursor: 'pointer'}} onClick={() => removeItem(i)} /> 109 </button>{' '} 110 </div> 111 ))} 112 {props.items.length === 0 && <label>No items</label>} 113 <div> 114 <button className='argo-button argo-button--base argo-button--short' onClick={() => addItem({} as T)}> 115 <i style={{cursor: 'pointer'}} className='fa fa-plus' /> 116 </button> 117 </div> 118 </div> 119 ); 120 } 121 122 export const ResetOrDeleteButton = (props: { 123 isPluginPar: boolean; 124 getValue: () => FormValue; 125 name: string; 126 index: number; 127 setValue: (value: FormValue) => void; 128 setAppParamsDeletedState: any; 129 }) => { 130 const handleDeleteChange = () => { 131 if (props.index >= 0) { 132 props.setAppParamsDeletedState((val: string[]) => val.concat(props.name)); 133 } 134 }; 135 136 const handleResetChange = () => { 137 if (props.index >= 0) { 138 const items = [...props.getValue()]; 139 items.splice(props.index, 1); 140 props.setValue(items); 141 } 142 }; 143 144 const disabled = props.index === -1; 145 146 const content = props.isPluginPar ? 'Reset' : 'Delete'; 147 let tooltip = ''; 148 if (content === 'Reset' && !disabled) { 149 tooltip = 'Resets the parameter to the value provided by the plugin. This removes the parameter override from the application manifest'; 150 } else if (content === 'Delete' && !disabled) { 151 tooltip = 'Deletes this parameter values from the application manifest.'; 152 } 153 154 return ( 155 <button 156 className='argo-button argo-button--base' 157 disabled={disabled} 158 title={tooltip} 159 style={{fontSize: '12px', display: 'flex', marginLeft: 'auto', marginTop: '8px'}} 160 onClick={props.isPluginPar ? handleResetChange : handleDeleteChange}> 161 {content} 162 </button> 163 ); 164 }; 165 166 export const ArrayInputField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => { 167 const { 168 fieldApi: {getValue, setValue} 169 } = props; 170 return <ArrayInput editor={NameValueEditor} items={getValue() || []} onChange={setValue} />; 171 }); 172 173 export const ArrayValueField = ReactForm.FormField( 174 (props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: string[]; isPluginPar: boolean; setAppParamsDeletedState: any}) => { 175 const { 176 fieldApi: {getValue, setValue} 177 } = props; 178 179 let liveParamArray; 180 const liveParam = getValue()?.find((val: {name: string; array: object}) => val.name === props.name); 181 if (liveParam) { 182 liveParamArray = liveParam?.array ?? []; 183 } 184 const index = getValue()?.findIndex((val: {name: string; array: object}) => val.name === props.name) ?? -1; 185 const values = liveParamArray ?? props.defaultVal ?? []; 186 187 return ( 188 <React.Fragment> 189 <ResetOrDeleteButton 190 isPluginPar={props.isPluginPar} 191 getValue={getValue} 192 name={props.name} 193 index={index} 194 setValue={setValue} 195 setAppParamsDeletedState={props.setAppParamsDeletedState} 196 /> 197 <ArrayInput 198 editor={ValueEditor} 199 items={values || []} 200 onChange={change => { 201 const update = change.map((val: string | object) => (typeof val !== 'string' ? '' : val)); 202 if (index >= 0) { 203 getValue()[index].array = update; 204 setValue([...getValue()]); 205 } else { 206 setValue([...(getValue() || []), {name: props.name, array: update}]); 207 } 208 }} 209 /> 210 </React.Fragment> 211 ); 212 } 213 ); 214 215 export const StringValueField = ReactForm.FormField( 216 (props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: string; isPluginPar: boolean; setAppParamsDeletedState: any}) => { 217 const { 218 fieldApi: {getValue, setValue} 219 } = props; 220 let liveParamString; 221 const liveParam = getValue()?.find((val: {name: string; string: string}) => val.name === props.name); 222 if (liveParam) { 223 liveParamString = liveParam?.string ? liveParam?.string : ''; 224 } 225 const values = liveParamString ?? props.defaultVal ?? ''; 226 const index = getValue()?.findIndex((val: {name: string; string: string}) => val.name === props.name) ?? -1; 227 228 return ( 229 <React.Fragment> 230 <ResetOrDeleteButton 231 isPluginPar={props.isPluginPar} 232 getValue={getValue} 233 name={props.name} 234 index={index} 235 setValue={setValue} 236 setAppParamsDeletedState={props.setAppParamsDeletedState} 237 /> 238 <div> 239 <input 240 // disable chrome autocomplete 241 autoComplete='fake' 242 className='argo-field' 243 style={{width: '40%', display: 'inline-block', marginTop: 25}} 244 placeholder='Value' 245 value={values || ''} 246 onChange={e => { 247 if (index >= 0) { 248 getValue()[index].string = e.target.value; 249 setValue([...getValue()]); 250 } else { 251 setValue([...(getValue() || []), {name: props.name, string: e.target.value}]); 252 } 253 }} 254 title='Value' 255 /> 256 </div> 257 </React.Fragment> 258 ); 259 } 260 ); 261 262 export const MapInputField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => { 263 const { 264 fieldApi: {getValue, setValue} 265 } = props; 266 const items = new Array<NameValue>(); 267 const map = getValue() || {}; 268 Object.keys(map).forEach(key => items.push({name: key, value: map[key]})); 269 return ( 270 <ArrayInput 271 editor={NameValueEditor} 272 items={items} 273 onChange={array => { 274 const newMap = {} as any; 275 array.forEach(item => (newMap[item.name || ''] = item.value || '')); 276 setValue(newMap); 277 }} 278 /> 279 ); 280 }); 281 282 export const MapValueField = ReactForm.FormField( 283 (props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: Map<string, string>; isPluginPar: boolean; setAppParamsDeletedState: any}) => { 284 const { 285 fieldApi: {getValue, setValue} 286 } = props; 287 const items = new Array<NameValue>(); 288 const liveParam = getValue()?.find((val: {name: string; map: object}) => val.name === props.name); 289 const index = getValue()?.findIndex((val: {name: string; map: object}) => val.name === props.name) ?? -1; 290 if (liveParam) { 291 liveParam.map = liveParam.map ? liveParam.map : new Map<string, string>(); 292 } 293 if (liveParam?.array) { 294 items.push(...liveParam.array); 295 } else { 296 const map = liveParam?.map ?? props.defaultVal ?? new Map<string, string>(); 297 Object.keys(map).forEach(item => items.push({name: item || '', value: map[item] || ''})); 298 if (liveParam?.map) { 299 getValue()[index].array = items; 300 } 301 } 302 303 return ( 304 <React.Fragment> 305 <ResetOrDeleteButton 306 isPluginPar={props.isPluginPar} 307 getValue={getValue} 308 name={props.name} 309 index={index} 310 setValue={setValue} 311 setAppParamsDeletedState={props.setAppParamsDeletedState} 312 /> 313 314 <ArrayInput 315 editor={NameValueEditor} 316 items={items || []} 317 onChange={change => { 318 if (index === -1) { 319 getValue().push({ 320 name: props.name, 321 array: change 322 }); 323 } else { 324 getValue()[index].array = change; 325 } 326 setValue([...getValue()]); 327 }} 328 /> 329 </React.Fragment> 330 ); 331 } 332 );