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  };