argo-cd

Форк
0
567 строк · 26.0 Кб
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
import {cloneDeep} from 'lodash-es';
5
import {
6
    ArrayInputField,
7
    ArrayValueField,
8
    CheckboxField,
9
    EditablePanel,
10
    EditablePanelItem,
11
    Expandable,
12
    MapValueField,
13
    NameValueEditor,
14
    StringValueField,
15
    NameValue,
16
    TagsInputField,
17
    ValueEditor
18
} from '../../../shared/components';
19
import * as models from '../../../shared/models';
20
import {ApplicationSourceDirectory, Plugin} from '../../../shared/models';
21
import {services} from '../../../shared/services';
22
import {ImageTagFieldEditor} from './kustomize';
23
import * as kustomize from './kustomize-image';
24
import {VarsInputField} from './vars-input-field';
25
import {concatMaps} from '../../../shared/utils';
26
import {getAppDefaultSource} from '../utils';
27
import * as jsYaml from 'js-yaml';
28

29
const TextWithMetadataField = ReactFormField((props: {metadata: {value: string}; fieldApi: FieldApi; className: string}) => {
30
    const {
31
        fieldApi: {getValue, setValue}
32
    } = props;
33
    const metadata = getValue() || props.metadata;
34

35
    return <input className={props.className} value={metadata.value} onChange={el => setValue({...metadata, value: el.target.value})} />;
36
});
37

38
function distinct<T>(first: IterableIterator<T>, second: IterableIterator<T>) {
39
    return Array.from(new Set(Array.from(first).concat(Array.from(second))));
40
}
41

42
function overridesFirst(first: {overrideIndex: number; metadata: {name: string}}, second: {overrideIndex: number; metadata: {name: string}}) {
43
    if (first.overrideIndex === second.overrideIndex) {
44
        return first.metadata.name.localeCompare(second.metadata.name);
45
    }
46
    if (first.overrideIndex < 0) {
47
        return 1;
48
    } else if (second.overrideIndex < 0) {
49
        return -1;
50
    }
51
    return first.overrideIndex - second.overrideIndex;
52
}
53

54
function getParamsEditableItems(
55
    app: models.Application,
56
    title: string,
57
    fieldsPath: string,
58
    removedOverrides: boolean[],
59
    setRemovedOverrides: React.Dispatch<boolean[]>,
60
    params: {
61
        key?: string;
62
        overrideIndex: number;
63
        original: string;
64
        metadata: {name: string; value: string};
65
    }[],
66
    component: React.ComponentType = TextWithMetadataField
67
) {
68
    return params
69
        .sort(overridesFirst)
70
        .map((param, i) => ({
71
            key: param.key,
72
            title: param.metadata.name,
73
            view: (
74
                <span title={param.metadata.value}>
75
                    {param.overrideIndex > -1 && <span className='fa fa-gavel' title={`Original value: ${param.original}`} />} {param.metadata.value}
76
                </span>
77
            ),
78
            edit: (formApi: FormApi) => {
79
                const labelStyle = {position: 'absolute', right: 0, top: 0, zIndex: 11} as any;
80
                const overrideRemoved = removedOverrides[i];
81
                const fieldItemPath = `${fieldsPath}[${i}]`;
82
                return (
83
                    <React.Fragment>
84
                        {(overrideRemoved && <span>{param.original}</span>) || (
85
                            <FormField
86
                                formApi={formApi}
87
                                field={fieldItemPath}
88
                                component={component}
89
                                componentProps={{
90
                                    metadata: param.metadata
91
                                }}
92
                            />
93
                        )}
94
                        {param.metadata.value !== param.original && !overrideRemoved && (
95
                            <a
96
                                onClick={() => {
97
                                    formApi.setValue(fieldItemPath, null);
98
                                    removedOverrides[i] = true;
99
                                    setRemovedOverrides(removedOverrides);
100
                                }}
101
                                style={labelStyle}>
102
                                Remove override
103
                            </a>
104
                        )}
105
                        {overrideRemoved && (
106
                            <a
107
                                onClick={() => {
108
                                    formApi.setValue(fieldItemPath, getNestedField(app, fieldsPath)[i]);
109
                                    removedOverrides[i] = false;
110
                                    setRemovedOverrides(removedOverrides);
111
                                }}
112
                                style={labelStyle}>
113
                                Keep override
114
                            </a>
115
                        )}
116
                    </React.Fragment>
117
                );
118
            }
119
        }))
120
        .map((item, i) => ({...item, before: (i === 0 && <p style={{marginTop: '1em'}}>{title}</p>) || null}));
121
}
122

123
export const ApplicationParameters = (props: {
124
    application: models.Application;
125
    details: models.RepoAppDetails;
126
    save?: (application: models.Application, query: {validate?: boolean}) => Promise<any>;
127
    noReadonlyMode?: boolean;
128
}) => {
129
    const app = cloneDeep(props.application);
130
    const source = getAppDefaultSource(app);
131
    const [removedOverrides, setRemovedOverrides] = React.useState(new Array<boolean>());
132

133
    let attributes: EditablePanelItem[] = [];
134
    const isValuesObject = source?.helm?.valuesObject;
135
    const helmValues = isValuesObject ? jsYaml.safeDump(source.helm.valuesObject) : source?.helm?.values;
136
    const [appParamsDeletedState, setAppParamsDeletedState] = React.useState([]);
137

138
    if (props.details.type === 'Kustomize' && props.details.kustomize) {
139
        attributes.push({
140
            title: 'VERSION',
141
            view: (source.kustomize && source.kustomize.version) || <span>default</span>,
142
            edit: (formApi: FormApi) => (
143
                <DataLoader load={() => services.authService.settings()}>
144
                    {settings =>
145
                        ((settings.kustomizeVersions || []).length > 0 && (
146
                            <FormField formApi={formApi} field='spec.source.kustomize.version' component={AutocompleteField} componentProps={{items: settings.kustomizeVersions}} />
147
                        )) || <span>default</span>
148
                    }
149
                </DataLoader>
150
            )
151
        });
152

153
        attributes.push({
154
            title: 'NAME PREFIX',
155
            view: source.kustomize && source.kustomize.namePrefix,
156
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.kustomize.namePrefix' component={Text} />
157
        });
158

159
        attributes.push({
160
            title: 'NAME SUFFIX',
161
            view: source.kustomize && source.kustomize.nameSuffix,
162
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.kustomize.nameSuffix' component={Text} />
163
        });
164

165
        attributes.push({
166
            title: 'NAMESPACE',
167
            view: app.spec.source.kustomize && app.spec.source.kustomize.namespace,
168
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.kustomize.namespace' component={Text} />
169
        });
170

171
        const srcImages = ((props.details && props.details.kustomize && props.details.kustomize.images) || []).map(val => kustomize.parse(val));
172
        const images = ((source.kustomize && source.kustomize.images) || []).map(val => kustomize.parse(val));
173

174
        if (srcImages.length > 0) {
175
            const imagesByName = new Map<string, kustomize.Image>();
176
            srcImages.forEach(img => imagesByName.set(img.name, img));
177

178
            const overridesByName = new Map<string, number>();
179
            images.forEach((override, i) => overridesByName.set(override.name, i));
180

181
            attributes = attributes.concat(
182
                getParamsEditableItems(
183
                    app,
184
                    'IMAGES',
185
                    'spec.source.kustomize.images',
186
                    removedOverrides,
187
                    setRemovedOverrides,
188
                    distinct(imagesByName.keys(), overridesByName.keys()).map(name => {
189
                        const param = imagesByName.get(name);
190
                        const original = param && kustomize.format(param);
191
                        let overrideIndex = overridesByName.get(name);
192
                        if (overrideIndex === undefined) {
193
                            overrideIndex = -1;
194
                        }
195
                        const value = (overrideIndex > -1 && kustomize.format(images[overrideIndex])) || original;
196
                        return {overrideIndex, original, metadata: {name, value}};
197
                    }),
198
                    ImageTagFieldEditor
199
                )
200
            );
201
        }
202
    } else if (props.details.type === 'Helm' && props.details.helm) {
203
        attributes.push({
204
            title: 'VALUES FILES',
205
            view: (source.helm && (source.helm.valueFiles || []).join(', ')) || 'No values files selected',
206
            edit: (formApi: FormApi) => (
207
                <FormField
208
                    formApi={formApi}
209
                    field='spec.source.helm.valueFiles'
210
                    component={TagsInputField}
211
                    componentProps={{
212
                        options: props.details.helm.valueFiles,
213
                        noTagsLabel: 'No values files selected'
214
                    }}
215
                />
216
            )
217
        });
218
        attributes.push({
219
            title: 'VALUES',
220
            view: source.helm && (
221
                <Expandable>
222
                    <pre>{helmValues}</pre>
223
                </Expandable>
224
            ),
225
            edit: (formApi: FormApi) => {
226
                // In case source.helm.valuesObject is set, set source.helm.values to its value
227
                if (source.helm) {
228
                    source.helm.values = helmValues;
229
                }
230

231
                return (
232
                    <div>
233
                        <pre>
234
                            <FormField formApi={formApi} field='spec.source.helm.values' component={TextArea} />
235
                        </pre>
236
                    </div>
237
                );
238
            }
239
        });
240
        const paramsByName = new Map<string, models.HelmParameter>();
241
        (props.details.helm.parameters || []).forEach(param => paramsByName.set(param.name, param));
242
        const overridesByName = new Map<string, number>();
243
        ((source.helm && source.helm.parameters) || []).forEach((override, i) => overridesByName.set(override.name, i));
244
        attributes = attributes.concat(
245
            getParamsEditableItems(
246
                app,
247
                'PARAMETERS',
248
                'spec.source.helm.parameters',
249
                removedOverrides,
250
                setRemovedOverrides,
251
                distinct(paramsByName.keys(), overridesByName.keys()).map(name => {
252
                    const param = paramsByName.get(name);
253
                    const original = (param && param.value) || '';
254
                    let overrideIndex = overridesByName.get(name);
255
                    if (overrideIndex === undefined) {
256
                        overrideIndex = -1;
257
                    }
258
                    const value = (overrideIndex > -1 && source.helm.parameters[overrideIndex].value) || original;
259
                    return {overrideIndex, original, metadata: {name, value}};
260
                })
261
            )
262
        );
263
        const fileParamsByName = new Map<string, models.HelmFileParameter>();
264
        (props.details.helm.fileParameters || []).forEach(param => fileParamsByName.set(param.name, param));
265
        const fileOverridesByName = new Map<string, number>();
266
        ((source.helm && source.helm.fileParameters) || []).forEach((override, i) => fileOverridesByName.set(override.name, i));
267
        attributes = attributes.concat(
268
            getParamsEditableItems(
269
                app,
270
                'PARAMETERS',
271
                'spec.source.helm.parameters',
272
                removedOverrides,
273
                setRemovedOverrides,
274
                distinct(fileParamsByName.keys(), fileOverridesByName.keys()).map(name => {
275
                    const param = fileParamsByName.get(name);
276
                    const original = (param && param.path) || '';
277
                    let overrideIndex = fileOverridesByName.get(name);
278
                    if (overrideIndex === undefined) {
279
                        overrideIndex = -1;
280
                    }
281
                    const value = (overrideIndex > -1 && source.helm.fileParameters[overrideIndex].path) || original;
282
                    return {overrideIndex, original, metadata: {name, value}};
283
                })
284
            )
285
        );
286
    } else if (props.details.type === 'Plugin') {
287
        attributes.push({
288
            title: 'NAME',
289
            view: <div style={{marginTop: 15, marginBottom: 5}}>{ValueEditor(app.spec.source.plugin && app.spec.source.plugin.name, null)}</div>,
290
            edit: (formApi: FormApi) => (
291
                <DataLoader load={() => services.authService.plugins()}>
292
                    {(plugins: Plugin[]) => (
293
                        <FormField formApi={formApi} field='spec.source.plugin.name' component={FormSelect} componentProps={{options: plugins.map(p => p.name)}} />
294
                    )}
295
                </DataLoader>
296
            )
297
        });
298
        attributes.push({
299
            title: 'ENV',
300
            view: (
301
                <div style={{marginTop: 15}}>
302
                    {app.spec.source.plugin &&
303
                        (app.spec.source.plugin.env || []).map(val => (
304
                            <span key={val.name} style={{display: 'block', marginBottom: 5}}>
305
                                {NameValueEditor(val, null)}
306
                            </span>
307
                        ))}
308
                </div>
309
            ),
310
            edit: (formApi: FormApi) => <FormField field='spec.source.plugin.env' formApi={formApi} component={ArrayInputField} />
311
        });
312
        const parametersSet = new Set<string>();
313
        if (props.details?.plugin?.parametersAnnouncement) {
314
            for (const announcement of props.details.plugin.parametersAnnouncement) {
315
                parametersSet.add(announcement.name);
316
            }
317
        }
318
        if (app.spec.source.plugin?.parameters) {
319
            for (const appParameter of app.spec.source.plugin.parameters) {
320
                parametersSet.add(appParameter.name);
321
            }
322
        }
323

324
        for (const key of appParamsDeletedState) {
325
            parametersSet.delete(key);
326
        }
327
        parametersSet.forEach(name => {
328
            const announcement = props.details.plugin.parametersAnnouncement?.find(param => param.name === name);
329
            const liveParam = app.spec.source.plugin?.parameters?.find(param => param.name === name);
330
            const pluginIcon =
331
                announcement && liveParam ? 'This parameter has been provided by plugin, but is overridden in application manifest.' : 'This parameter is provided by the plugin.';
332
            const isPluginPar = !!announcement;
333
            if ((announcement?.collectionType === undefined && liveParam?.map) || announcement?.collectionType === 'map') {
334
                let liveParamMap;
335
                if (liveParam) {
336
                    liveParamMap = liveParam.map ?? new Map<string, string>();
337
                }
338
                const map = concatMaps(liveParamMap ?? announcement?.map, new Map<string, string>());
339
                const entries = map.entries();
340
                const items = new Array<NameValue>();
341
                Array.from(entries).forEach(([key, value]) => items.push({name: key, value: `${value}`}));
342
                attributes.push({
343
                    title: announcement?.title ?? announcement?.name ?? name,
344
                    customTitle: (
345
                        <span>
346
                            {isPluginPar && <i className='fa solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
347
                            {announcement?.title ?? announcement?.name ?? name}
348
                        </span>
349
                    ),
350
                    view: (
351
                        <div style={{marginTop: 15, marginBottom: 5}}>
352
                            {items.length === 0 && <span style={{color: 'dimgray'}}>-- NO ITEMS --</span>}
353
                            {items.map(val => (
354
                                <span key={val.name} style={{display: 'block', marginBottom: 5}}>
355
                                    {NameValueEditor(val)}
356
                                </span>
357
                            ))}
358
                        </div>
359
                    ),
360
                    edit: (formApi: FormApi) => (
361
                        <FormField
362
                            field='spec.source.plugin.parameters'
363
                            componentProps={{
364
                                name: announcement?.name ?? name,
365
                                defaultVal: announcement?.map,
366
                                isPluginPar,
367
                                setAppParamsDeletedState
368
                            }}
369
                            formApi={formApi}
370
                            component={MapValueField}
371
                        />
372
                    )
373
                });
374
            } else if ((announcement?.collectionType === undefined && liveParam?.array) || announcement?.collectionType === 'array') {
375
                let liveParamArray;
376
                if (liveParam) {
377
                    liveParamArray = liveParam?.array ?? [];
378
                }
379
                attributes.push({
380
                    title: announcement?.title ?? announcement?.name ?? name,
381
                    customTitle: (
382
                        <span>
383
                            {isPluginPar && <i className='fa-solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
384
                            {announcement?.title ?? announcement?.name ?? name}
385
                        </span>
386
                    ),
387
                    view: (
388
                        <div style={{marginTop: 15, marginBottom: 5}}>
389
                            {(liveParamArray ?? announcement?.array ?? []).length === 0 && <span style={{color: 'dimgray'}}>-- NO ITEMS --</span>}
390
                            {(liveParamArray ?? announcement?.array ?? []).map((val, index) => (
391
                                <span key={index} style={{display: 'block', marginBottom: 5}}>
392
                                    {ValueEditor(val, null)}
393
                                </span>
394
                            ))}
395
                        </div>
396
                    ),
397
                    edit: (formApi: FormApi) => (
398
                        <FormField
399
                            field='spec.source.plugin.parameters'
400
                            componentProps={{
401
                                name: announcement?.name ?? name,
402
                                defaultVal: announcement?.array,
403
                                isPluginPar,
404
                                setAppParamsDeletedState
405
                            }}
406
                            formApi={formApi}
407
                            component={ArrayValueField}
408
                        />
409
                    )
410
                });
411
            } else if (
412
                (announcement?.collectionType === undefined && liveParam?.string) ||
413
                announcement?.collectionType === '' ||
414
                announcement?.collectionType === 'string' ||
415
                announcement?.collectionType === undefined
416
            ) {
417
                let liveParamString;
418
                if (liveParam) {
419
                    liveParamString = liveParam?.string ?? '';
420
                }
421
                attributes.push({
422
                    title: announcement?.title ?? announcement?.name ?? name,
423
                    customTitle: (
424
                        <span>
425
                            {isPluginPar && <i className='fa-solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
426
                            {announcement?.title ?? announcement?.name ?? name}
427
                        </span>
428
                    ),
429
                    view: (
430
                        <div
431
                            style={{
432
                                marginTop: 15,
433
                                marginBottom: 5
434
                            }}>
435
                            {ValueEditor(liveParamString ?? announcement?.string, null)}
436
                        </div>
437
                    ),
438
                    edit: (formApi: FormApi) => (
439
                        <FormField
440
                            field='spec.source.plugin.parameters'
441
                            componentProps={{
442
                                name: announcement?.name ?? name,
443
                                defaultVal: announcement?.string,
444
                                isPluginPar,
445
                                setAppParamsDeletedState
446
                            }}
447
                            formApi={formApi}
448
                            component={StringValueField}
449
                        />
450
                    )
451
                });
452
            }
453
        });
454
    } else if (props.details.type === 'Directory') {
455
        const directory = source.directory || ({} as ApplicationSourceDirectory);
456
        attributes.push({
457
            title: 'DIRECTORY RECURSE',
458
            view: (!!directory.recurse).toString(),
459
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.directory.recurse' component={CheckboxField} />
460
        });
461
        attributes.push({
462
            title: 'TOP-LEVEL ARGUMENTS',
463
            view: ((directory?.jsonnet && directory?.jsonnet.tlas) || []).map((i, j) => (
464
                <label key={j}>
465
                    {i.name}='{i.value}' {i.code && 'code'}
466
                </label>
467
            )),
468
            edit: (formApi: FormApi) => <FormField field='spec.source.directory.jsonnet.tlas' formApi={formApi} component={VarsInputField} />
469
        });
470
        attributes.push({
471
            title: 'EXTERNAL VARIABLES',
472
            view: ((directory.jsonnet && directory.jsonnet.extVars) || []).map((i, j) => (
473
                <label key={j}>
474
                    {i.name}='{i.value}' {i.code && 'code'}
475
                </label>
476
            )),
477
            edit: (formApi: FormApi) => <FormField field='spec.source.directory.jsonnet.extVars' formApi={formApi} component={VarsInputField} />
478
        });
479

480
        attributes.push({
481
            title: 'INCLUDE',
482
            view: directory && directory.include,
483
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.directory.include' component={Text} />
484
        });
485

486
        attributes.push({
487
            title: 'EXCLUDE',
488
            view: directory && directory.exclude,
489
            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.directory.exclude' component={Text} />
490
        });
491
    }
492

493
    return (
494
        <EditablePanel
495
            save={
496
                props.save &&
497
                (async (input: models.Application) => {
498
                    const src = getAppDefaultSource(input);
499

500
                    function isDefined(item: any) {
501
                        return item !== null && item !== undefined;
502
                    }
503
                    function isDefinedWithVersion(item: any) {
504
                        return item !== null && item !== undefined && item.match(/:/);
505
                    }
506

507
                    if (src.helm && src.helm.parameters) {
508
                        src.helm.parameters = src.helm.parameters.filter(isDefined);
509
                    }
510
                    if (src.kustomize && src.kustomize.images) {
511
                        src.kustomize.images = src.kustomize.images.filter(isDefinedWithVersion);
512
                    }
513

514
                    let params = input.spec?.source?.plugin?.parameters;
515
                    if (params) {
516
                        for (const param of params) {
517
                            if (param.map && param.array) {
518
                                // @ts-ignore
519
                                param.map = param.array.reduce((acc, {name, value}) => {
520
                                    // @ts-ignore
521
                                    acc[name] = value;
522
                                    return acc;
523
                                }, {});
524
                                delete param.array;
525
                            }
526
                        }
527

528
                        params = params.filter(param => !appParamsDeletedState.includes(param.name));
529
                        input.spec.source.plugin.parameters = params;
530
                    }
531
                    if (input.spec.source.helm && input.spec.source.helm.valuesObject) {
532
                        input.spec.source.helm.valuesObject = jsYaml.safeLoad(input.spec.source.helm.values); // Deserialize json
533
                        input.spec.source.helm.values = '';
534
                    }
535
                    await props.save(input, {});
536
                    setRemovedOverrides(new Array<boolean>());
537
                })
538
            }
539
            values={((props.details.plugin || app?.spec?.source?.plugin) && cloneDeep(app)) || app}
540
            validate={updatedApp => {
541
                const errors = {} as any;
542

543
                for (const fieldPath of ['spec.source.directory.jsonnet.tlas', 'spec.source.directory.jsonnet.extVars']) {
544
                    const invalid = ((getNestedField(updatedApp, fieldPath) || []) as Array<models.JsonnetVar>).filter(item => !item.name && !item.code);
545
                    errors[fieldPath] = invalid.length > 0 ? 'All fields must have name' : null;
546
                }
547

548
                if (updatedApp.spec.source.helm && updatedApp.spec.source.helm.values) {
549
                    const parsedValues = jsYaml.safeLoad(updatedApp.spec.source.helm.values);
550
                    errors['spec.source.helm.values'] = typeof parsedValues === 'object' ? null : 'Values must be a map';
551
                }
552

553
                return errors;
554
            }}
555
            onModeSwitch={
556
                props.details.plugin &&
557
                (() => {
558
                    setAppParamsDeletedState([]);
559
                })
560
            }
561
            title={props.details.type.toLocaleUpperCase()}
562
            items={attributes}
563
            noReadonlyMode={props.noReadonlyMode}
564
            hasMultipleSources={app.spec.sources && app.spec.sources.length > 0}
565
        />
566
    );
567
};
568

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.