github.com/argoproj/argo-cd@v1.8.7/ui/src/app/applications/components/application-parameters/application-parameters.tsx (about) 1 import {AutocompleteField, DataLoader, FormField, FormSelect, getNestedField} from 'argo-ui'; 2 import * as React from 'react'; 3 import {FieldApi, FormApi, FormField as ReactFormField, Text, TextArea} from 'react-form'; 4 5 import {ArrayInputField, CheckboxField, EditablePanel, EditablePanelItem, Expandable, TagsInputField} from '../../../shared/components'; 6 import * as models from '../../../shared/models'; 7 import {ApplicationSourceDirectory, AuthSettings} from '../../../shared/models'; 8 import {services} from '../../../shared/services'; 9 import {ImageTagFieldEditor} from './kustomize'; 10 import * as kustomize from './kustomize-image'; 11 import {VarsInputField} from './vars-input-field'; 12 13 const TextWithMetadataField = ReactFormField((props: {metadata: {value: string}; fieldApi: FieldApi; className: string}) => { 14 const { 15 fieldApi: {getValue, setValue} 16 } = props; 17 const metadata = getValue() || props.metadata; 18 19 return <input className={props.className} value={metadata.value} onChange={el => setValue({...metadata, value: el.target.value})} />; 20 }); 21 22 function distinct<T>(first: IterableIterator<T>, second: IterableIterator<T>) { 23 return Array.from(new Set(Array.from(first).concat(Array.from(second)))); 24 } 25 26 function overridesFirst(first: {overrideIndex: number}, second: {overrideIndex: number}) { 27 if (first.overrideIndex < 0) { 28 return 1; 29 } else if (second.overrideIndex < 0) { 30 return -1; 31 } 32 return first.overrideIndex - second.overrideIndex; 33 } 34 35 function getParamsEditableItems( 36 app: models.Application, 37 title: string, 38 fieldsPath: string, 39 removedOverrides: boolean[], 40 setRemovedOverrides: React.Dispatch<boolean[]>, 41 params: { 42 key?: string; 43 overrideIndex: number; 44 original: string; 45 metadata: {name: string; value: string}; 46 }[], 47 component: React.ComponentType = TextWithMetadataField 48 ) { 49 return params 50 .sort(overridesFirst) 51 .map((param, i) => ({ 52 key: param.key, 53 title: param.metadata.name, 54 view: ( 55 <span title={param.metadata.value}> 56 {param.overrideIndex > -1 && <span className='fa fa-exclamation-triangle' title={`Original value: ${param.original}`} />} {param.metadata.value} 57 </span> 58 ), 59 edit: (formApi: FormApi) => { 60 const labelStyle = {position: 'absolute', right: 0, top: 0, zIndex: 1} as any; 61 const overrideRemoved = removedOverrides[i]; 62 const fieldItemPath = `${fieldsPath}[${i}]`; 63 return ( 64 <React.Fragment> 65 {(overrideRemoved && <span>{param.original}</span>) || ( 66 <FormField 67 formApi={formApi} 68 field={fieldItemPath} 69 component={component} 70 componentProps={{ 71 metadata: param.metadata 72 }} 73 /> 74 )} 75 {param.metadata.value !== param.original && !overrideRemoved && ( 76 <a 77 onClick={() => { 78 formApi.setValue(fieldItemPath, null); 79 removedOverrides[i] = true; 80 setRemovedOverrides(removedOverrides); 81 }} 82 style={labelStyle}> 83 Remove override 84 </a> 85 )} 86 {overrideRemoved && ( 87 <a 88 onClick={() => { 89 formApi.setValue(fieldItemPath, getNestedField(app, fieldsPath)[i]); 90 removedOverrides[i] = false; 91 setRemovedOverrides(removedOverrides); 92 }} 93 style={labelStyle}> 94 Keep override 95 </a> 96 )} 97 </React.Fragment> 98 ); 99 } 100 })) 101 .sort((first, second) => { 102 const firstSortBy = first.key || first.title; 103 const secondSortBy = second.key || second.title; 104 return firstSortBy.localeCompare(secondSortBy); 105 }) 106 .map((item, i) => ({...item, before: (i === 0 && <p style={{marginTop: '1em'}}>{title}</p>) || null})); 107 } 108 109 export const ApplicationParameters = (props: { 110 application: models.Application; 111 details: models.RepoAppDetails; 112 save?: (application: models.Application) => Promise<any>; 113 noReadonlyMode?: boolean; 114 }) => { 115 const app = props.application; 116 const source = props.application.spec.source; 117 const [removedOverrides, setRemovedOverrides] = React.useState(new Array<boolean>()); 118 119 let attributes: EditablePanelItem[] = []; 120 121 if (props.details.type === 'Ksonnet' && props.details.ksonnet) { 122 attributes.push({ 123 title: 'ENVIRONMENT', 124 view: app.spec.source.ksonnet && app.spec.source.ksonnet.environment, 125 edit: (formApi: FormApi) => ( 126 <FormField 127 formApi={formApi} 128 field='spec.source.ksonnet.environment' 129 component={FormSelect} 130 componentProps={{options: Object.keys(props.details.ksonnet.environments || {})}} 131 /> 132 ) 133 }); 134 const paramsByComponentName = new Map<string, models.KsonnetParameter>(); 135 ((props.details.ksonnet && props.details.ksonnet.parameters) || []).forEach(param => paramsByComponentName.set(`${param.component}-${param.name}`, param)); 136 const overridesByComponentName = new Map<string, number>(); 137 ((source.ksonnet && source.ksonnet.parameters) || []).forEach((override, i) => overridesByComponentName.set(`${override.component}-${override.name}`, i)); 138 attributes = attributes.concat( 139 getParamsEditableItems( 140 app, 141 'PARAMETERS', 142 'spec.source.ksonnet.parameters', 143 removedOverrides, 144 setRemovedOverrides, 145 distinct(paramsByComponentName.keys(), overridesByComponentName.keys()).map(componentName => { 146 let param = paramsByComponentName.get(componentName); 147 const original = (param && param.value) || ''; 148 let overrideIndex = overridesByComponentName.get(componentName); 149 if (overrideIndex === undefined) { 150 overrideIndex = -1; 151 } 152 if (!param && overrideIndex > -1) { 153 param = {...source.ksonnet.parameters[overrideIndex]}; 154 } 155 const value = (overrideIndex > -1 && source.ksonnet.parameters[overrideIndex].value) || original; 156 return {key: componentName, overrideIndex, original, metadata: {name: param.name, component: param.component, value}}; 157 }) 158 ) 159 ); 160 } else if (props.details.type === 'Kustomize' && props.details.kustomize) { 161 attributes.push({ 162 title: 'VERSION', 163 view: (app.spec.source.kustomize && app.spec.source.kustomize.version) || <span>default</span>, 164 edit: (formApi: FormApi) => ( 165 <DataLoader load={() => services.authService.settings()}> 166 {settings => 167 ((settings.kustomizeVersions || []).length > 0 && ( 168 <FormField formApi={formApi} field='spec.source.kustomize.version' component={AutocompleteField} componentProps={{items: settings.kustomizeVersions}} /> 169 )) || <span>default</span> 170 } 171 </DataLoader> 172 ) 173 }); 174 175 attributes.push({ 176 title: 'NAME PREFIX', 177 view: app.spec.source.kustomize && app.spec.source.kustomize.namePrefix, 178 edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.kustomize.namePrefix' component={Text} /> 179 }); 180 181 attributes.push({ 182 title: 'NAME SUFFIX', 183 view: app.spec.source.kustomize && app.spec.source.kustomize.nameSuffix, 184 edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.kustomize.nameSuffix' component={Text} /> 185 }); 186 187 const srcImages = ((props.details && props.details.kustomize && props.details.kustomize.images) || []).map(val => kustomize.parse(val)); 188 const images = ((source.kustomize && source.kustomize.images) || []).map(val => kustomize.parse(val)); 189 190 if (srcImages.length > 0) { 191 const imagesByName = new Map<string, kustomize.Image>(); 192 srcImages.forEach(img => imagesByName.set(img.name, img)); 193 194 const overridesByName = new Map<string, number>(); 195 images.forEach((override, i) => overridesByName.set(override.name, i)); 196 197 attributes = attributes.concat( 198 getParamsEditableItems( 199 app, 200 'IMAGES', 201 'spec.source.kustomize.images', 202 removedOverrides, 203 setRemovedOverrides, 204 distinct(imagesByName.keys(), overridesByName.keys()).map(name => { 205 const param = imagesByName.get(name); 206 const original = param && kustomize.format(param); 207 let overrideIndex = overridesByName.get(name); 208 if (overrideIndex === undefined) { 209 overrideIndex = -1; 210 } 211 const value = (overrideIndex > -1 && kustomize.format(images[overrideIndex])) || original; 212 return {overrideIndex, original, metadata: {name, value}}; 213 }), 214 ImageTagFieldEditor 215 ) 216 ); 217 } 218 } else if (props.details.type === 'Helm' && props.details.helm) { 219 attributes.push({ 220 title: 'VALUES FILES', 221 view: (app.spec.source.helm && (app.spec.source.helm.valueFiles || []).join(', ')) || 'No values files selected', 222 edit: (formApi: FormApi) => ( 223 <FormField 224 formApi={formApi} 225 field='spec.source.helm.valueFiles' 226 component={TagsInputField} 227 componentProps={{ 228 options: props.details.helm.valueFiles, 229 noTagsLabel: 'No values files selected' 230 }} 231 /> 232 ) 233 }); 234 attributes.push({ 235 title: 'VALUES', 236 view: app.spec.source.helm && ( 237 <Expandable> 238 <pre>{app.spec.source.helm.values}</pre> 239 </Expandable> 240 ), 241 edit: (formApi: FormApi) => ( 242 <div> 243 <pre> 244 <FormField formApi={formApi} field='spec.source.helm.values' component={TextArea} /> 245 </pre> 246 {props.details.helm.values && ( 247 <div> 248 <label>values.yaml</label> 249 <Expandable> 250 <pre>{props.details.helm.values}</pre> 251 </Expandable> 252 </div> 253 )} 254 </div> 255 ) 256 }); 257 const paramsByName = new Map<string, models.HelmParameter>(); 258 (props.details.helm.parameters || []).forEach(param => paramsByName.set(param.name, param)); 259 const overridesByName = new Map<string, number>(); 260 ((source.helm && source.helm.parameters) || []).forEach((override, i) => overridesByName.set(override.name, i)); 261 attributes = attributes.concat( 262 getParamsEditableItems( 263 app, 264 'PARAMETERS', 265 'spec.source.helm.parameters', 266 removedOverrides, 267 setRemovedOverrides, 268 distinct(paramsByName.keys(), overridesByName.keys()).map(name => { 269 const param = paramsByName.get(name); 270 const original = (param && param.value) || ''; 271 let overrideIndex = overridesByName.get(name); 272 if (overrideIndex === undefined) { 273 overrideIndex = -1; 274 } 275 const value = (overrideIndex > -1 && source.helm.parameters[overrideIndex].value) || original; 276 return {overrideIndex, original, metadata: {name, value}}; 277 }) 278 ) 279 ); 280 const fileParamsByName = new Map<string, models.HelmFileParameter>(); 281 (props.details.helm.fileParameters || []).forEach(param => fileParamsByName.set(param.name, param)); 282 const fileOverridesByName = new Map<string, number>(); 283 ((source.helm && source.helm.fileParameters) || []).forEach((override, i) => fileOverridesByName.set(override.name, i)); 284 attributes = attributes.concat( 285 getParamsEditableItems( 286 app, 287 'PARAMETERS', 288 'spec.source.helm.parameters', 289 removedOverrides, 290 setRemovedOverrides, 291 distinct(fileParamsByName.keys(), fileOverridesByName.keys()).map(name => { 292 const param = fileParamsByName.get(name); 293 const original = (param && param.path) || ''; 294 let overrideIndex = fileOverridesByName.get(name); 295 if (overrideIndex === undefined) { 296 overrideIndex = -1; 297 } 298 const value = (overrideIndex > -1 && source.helm.fileParameters[overrideIndex].path) || original; 299 return {overrideIndex, original, metadata: {name, value}}; 300 }) 301 ) 302 ); 303 } else if (props.details.type === 'Plugin') { 304 attributes.push({ 305 title: 'NAME', 306 view: app.spec.source.plugin && app.spec.source.plugin.name, 307 edit: (formApi: FormApi) => ( 308 <DataLoader load={() => services.authService.settings()}> 309 {(settings: AuthSettings) => ( 310 <FormField formApi={formApi} field='spec.source.plugin.name' component={FormSelect} componentProps={{options: (settings.plugins || []).map(p => p.name)}} /> 311 )} 312 </DataLoader> 313 ) 314 }); 315 attributes.push({ 316 title: 'ENV', 317 view: app.spec.source.plugin && (app.spec.source.plugin.env || []).map(i => `${i.name}='${i.value}'`).join(' '), 318 edit: (formApi: FormApi) => <FormField field='spec.source.plugin.env' formApi={formApi} component={ArrayInputField} /> 319 }); 320 } else if (props.details.type === 'Directory') { 321 const directory = app.spec.source.directory || ({} as ApplicationSourceDirectory); 322 attributes.push({ 323 title: 'DIRECTORY RECURSE', 324 view: (!!directory.recurse).toString(), 325 edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.directory.recurse' component={CheckboxField} /> 326 }); 327 attributes.push({ 328 title: 'TOP-LEVEL ARGUMENTS', 329 view: ((directory.jsonnet && directory.jsonnet.tlas) || []).map((i, j) => ( 330 <label key={j}> 331 {i.name}='{i.value}' {i.code && 'code'} 332 </label> 333 )), 334 edit: (formApi: FormApi) => <FormField field='spec.source.directory.jsonnet.tlas' formApi={formApi} component={VarsInputField} /> 335 }); 336 attributes.push({ 337 title: 'EXTERNAL VARIABLES', 338 view: ((directory.jsonnet && directory.jsonnet.extVars) || []).map((i, j) => ( 339 <label key={j}> 340 {i.name}='{i.value}' {i.code && 'code'} 341 </label> 342 )), 343 edit: (formApi: FormApi) => <FormField field='spec.source.directory.jsonnet.extVars' formApi={formApi} component={VarsInputField} /> 344 }); 345 } 346 347 return ( 348 <EditablePanel 349 save={ 350 props.save && 351 (async (input: models.Application) => { 352 function isDefined(item: any) { 353 return item !== null && item !== undefined; 354 } 355 356 if (input.spec.source.helm && input.spec.source.helm.parameters) { 357 input.spec.source.helm.parameters = input.spec.source.helm.parameters.filter(isDefined); 358 } 359 if (input.spec.source.ksonnet && input.spec.source.ksonnet.parameters) { 360 input.spec.source.ksonnet.parameters = input.spec.source.ksonnet.parameters.filter(isDefined); 361 } 362 if (input.spec.source.kustomize && input.spec.source.kustomize.images) { 363 input.spec.source.kustomize.images = input.spec.source.kustomize.images.filter(isDefined); 364 } 365 await props.save(input); 366 setRemovedOverrides(new Array<boolean>()); 367 }) 368 } 369 values={app} 370 validate={updatedApp => { 371 const errors = {} as any; 372 373 for (const fieldPath of ['spec.source.directory.jsonnet.tlas', 'spec.source.directory.jsonnet.extVars']) { 374 const invalid = ((getNestedField(updatedApp, fieldPath) || []) as Array<models.JsonnetVar>).filter(item => !item.name && !item.code); 375 errors[fieldPath] = invalid.length > 0 ? 'All fields must have name' : null; 376 } 377 378 return errors; 379 }} 380 title={props.details.type.toLocaleUpperCase()} 381 items={attributes} 382 noReadonlyMode={props.noReadonlyMode} 383 /> 384 ); 385 };