github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-parameters/source-panel.tsx (about)

     1  import {AutocompleteField, DataLoader, DropDownMenu, FormField} from 'argo-ui';
     2  import * as deepMerge from 'deepmerge';
     3  import * as React from 'react';
     4  import {Form, FormApi, FormErrors, Text} from 'react-form';
     5  import {ApplicationParameters} from '../../../applications/components/application-parameters/application-parameters';
     6  import {RevisionFormField} from '../../../applications/components/revision-form-field/revision-form-field';
     7  import {RevisionHelpIcon} from '../../../shared/components';
     8  import * as models from '../../../shared/models';
     9  import {services} from '../../../shared/services';
    10  import './source-panel.scss';
    11  
    12  // This is similar to what is in application-create-panel.tsx. If the create panel
    13  // is modified to support multi-source apps, then we should refactor and common these up
    14  const appTypes = new Array<{field: string; type: models.AppSourceType}>(
    15      {type: 'Helm', field: 'helm'},
    16      {type: 'Kustomize', field: 'kustomize'},
    17      {type: 'Directory', field: 'directory'},
    18      {type: 'Plugin', field: 'plugin'}
    19  );
    20  
    21  // This is similar to the same function in application-create-panel.tsx. If the create panel
    22  // is modified to support multi-source apps, then we should refactor and common these up
    23  function normalizeAppSource(app: models.Application, type: string): boolean {
    24      const source = app.spec.source;
    25      // eslint-disable-next-line no-prototype-builtins
    26      const repoType = source.repoURL.startsWith('oci://') ? 'oci' : (source.hasOwnProperty('chart') && 'helm') || 'git';
    27  
    28      if (repoType !== type) {
    29          if (type === 'git' || type === 'oci') {
    30              source.path = source.chart;
    31              delete source.chart;
    32              source.targetRevision = 'HEAD';
    33          } else {
    34              source.chart = source.path;
    35              delete source.path;
    36              source.targetRevision = '';
    37          }
    38          return true;
    39      }
    40      return false;
    41  }
    42  
    43  // Use a single source app to represent the 'new source'. This panel will make use of the source field only.
    44  // However, we need to use a template based on an Application so that we can reuse the application-parameters code
    45  const DEFAULT_APP: Partial<models.Application> = {
    46      apiVersion: 'argoproj.io/v1alpha1',
    47      kind: 'Application',
    48      metadata: {
    49          name: ''
    50      },
    51      spec: {
    52          destination: {
    53              name: '',
    54              namespace: '',
    55              server: ''
    56          },
    57          source: {
    58              path: '',
    59              repoURL: '',
    60              ref: '',
    61              name: '',
    62              targetRevision: 'HEAD'
    63          },
    64          sources: [],
    65          project: ''
    66      }
    67  };
    68  
    69  export const SourcePanel = (props: {
    70      appCurrent: models.Application;
    71      onSubmitFailure: (error: string) => any;
    72      updateApp: (app: models.Application) => any;
    73      getFormApi: (api: FormApi) => any;
    74  }) => {
    75      const [explicitPathType, setExplicitPathType] = React.useState<{path: string; type: models.AppSourceType}>(null);
    76      const appInEdit = deepMerge(DEFAULT_APP, {});
    77  
    78      function normalizeTypeFields(formApi: FormApi, type: models.AppSourceType) {
    79          const appToNormalize = formApi.getFormState().values;
    80          for (const item of appTypes) {
    81              if (item.type !== type) {
    82                  delete appToNormalize.spec.source[item.field];
    83              }
    84          }
    85          formApi.setAllValues(appToNormalize);
    86      }
    87  
    88      return (
    89          <DataLoader key='add-new-source' load={() => Promise.all([services.repos.list()]).then(([reposInfo]) => ({reposInfo}))}>
    90              {({reposInfo}) => {
    91                  const repos = reposInfo.map(info => info.repo).sort();
    92                  return (
    93                      <div className='new-source-panel'>
    94                          <Form
    95                              validateError={(a: models.Application) => {
    96                                  let samePath = false;
    97                                  let sameChartVersion = false;
    98                                  let pathError = null;
    99                                  let chartError = null;
   100                                  if (a.spec.source.repoURL && a.spec.source.path) {
   101                                      props.appCurrent.spec.sources.forEach(source => {
   102                                          if (source.repoURL === a.spec.source.repoURL && source.path === a.spec.source.path) {
   103                                              samePath = true;
   104                                              pathError = 'Provided path in the selected repository URL was already added to this multi-source application';
   105                                          }
   106                                      });
   107                                  }
   108                                  if (a.spec?.source?.repoURL && a.spec?.source?.chart) {
   109                                      props.appCurrent.spec.sources.forEach(source => {
   110                                          if (
   111                                              source?.repoURL === a.spec?.source?.repoURL &&
   112                                              source?.chart === a.spec?.source?.chart &&
   113                                              source?.targetRevision === a.spec?.source?.targetRevision
   114                                          ) {
   115                                              sameChartVersion = true;
   116                                              chartError =
   117                                                  'Version ' +
   118                                                  source?.targetRevision +
   119                                                  ' of chart ' +
   120                                                  source?.chart +
   121                                                  ' from the selected repository was already added to this multi-source application';
   122                                          }
   123                                      });
   124                                  }
   125                                  if (!samePath) {
   126                                      if (!a.spec?.source?.path && !a.spec?.source?.chart && !a.spec?.source?.ref) {
   127                                          pathError = 'Path or Ref is required';
   128                                      }
   129                                  }
   130                                  if (!sameChartVersion) {
   131                                      if (!a.spec?.source?.chart && !a.spec?.source?.path && !a.spec?.source?.ref) {
   132                                          chartError = 'Chart is required';
   133                                      }
   134                                  }
   135                                  return {
   136                                      'spec.source.repoURL': !a.spec?.source?.repoURL && 'Repository URL is required',
   137                                      // eslint-disable-next-line no-prototype-builtins
   138                                      'spec.source.targetRevision': !a.spec?.source?.targetRevision && a.spec?.source?.hasOwnProperty('chart') && 'Version is required',
   139                                      'spec.source.path': pathError,
   140                                      'spec.source.chart': chartError
   141                                  };
   142                              }}
   143                              defaultValues={appInEdit}
   144                              onSubmitFailure={(errors: FormErrors) => {
   145                                  let errorString: string = '';
   146                                  let i = 0;
   147                                  for (const key in errors) {
   148                                      if (errors[key]) {
   149                                          i++;
   150                                          errorString = errorString.concat(i + '. ' + errors[key] + ' ');
   151                                      }
   152                                  }
   153                                  props.onSubmitFailure(errorString);
   154                              }}
   155                              onSubmit={values => {
   156                                  props.updateApp(values as models.Application);
   157                              }}
   158                              getApi={props.getFormApi}>
   159                              {api => {
   160                                  const repoType = api.getFormState().values.spec?.source?.repoURL.startsWith('oci://')
   161                                      ? 'oci'
   162                                      : (api.getFormState().values.spec?.source?.chart && 'helm') || 'git';
   163                                  const repoInfo = reposInfo.find(info => info.repo === api.getFormState().values.spec?.source?.repoURL);
   164                                  if (repoInfo) {
   165                                      normalizeAppSource(appInEdit, repoInfo.type || 'git');
   166                                  }
   167                                  const sourcePanel = () => (
   168                                      <div className='white-box'>
   169                                          <p>SOURCE</p>
   170                                          <div className='row argo-form-row'>
   171                                              <div className='columns small-10'>
   172                                                  <FormField
   173                                                      formApi={api}
   174                                                      label='Repository URL'
   175                                                      field='spec.source.repoURL'
   176                                                      component={AutocompleteField}
   177                                                      componentProps={{items: repos}}
   178                                                  />
   179                                              </div>
   180                                              <div className='columns small-2'>
   181                                                  <div style={{paddingTop: '1.5em'}}>
   182                                                      {(repoInfo && (
   183                                                          <React.Fragment>
   184                                                              <span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
   185                                                          </React.Fragment>
   186                                                      )) || (
   187                                                          <DropDownMenu
   188                                                              anchor={() => (
   189                                                                  <p>
   190                                                                      {repoType.toUpperCase()} <i className='fa fa-caret-down' />
   191                                                                  </p>
   192                                                              )}
   193                                                              items={['git', 'helm', 'oci'].map((type: 'git' | 'helm' | 'oci') => ({
   194                                                                  title: type.toUpperCase(),
   195                                                                  action: () => {
   196                                                                      if (repoType !== type) {
   197                                                                          const updatedApp = api.getFormState().values as models.Application;
   198                                                                          if (normalizeAppSource(updatedApp, type)) {
   199                                                                              api.setAllValues(updatedApp);
   200                                                                          }
   201                                                                      }
   202                                                                  }
   203                                                              }))}
   204                                                          />
   205                                                      )}
   206                                                  </div>
   207                                              </div>
   208                                          </div>
   209                                          <div className='row argo-form-row'>
   210                                              <div className='columns small-10'>
   211                                                  <FormField formApi={api} label='Name' field={'spec.source.name'} component={Text}></FormField>
   212                                              </div>
   213                                          </div>
   214                                          {(repoType === 'oci' && (
   215                                              <React.Fragment>
   216                                                  <RevisionFormField
   217                                                      formApi={api}
   218                                                      helpIconTop={'2.5em'}
   219                                                      repoURL={api.getFormState().values.spec?.source?.repoURL}
   220                                                      repoType={repoType}
   221                                                  />
   222                                                  <div className='argo-form-row'>
   223                                                      <DataLoader
   224                                                          input={{
   225                                                              repoURL: api.getFormState().values.spec?.source?.repoURL,
   226                                                              revision: api.getFormState().values.spec?.source?.targetRevision
   227                                                          }}
   228                                                          load={async src =>
   229                                                              src.repoURL &&
   230                                                              // TODO: for autocomplete we need to fetch paths that are used by other apps within the same project making use of the same OCI repo
   231                                                              new Array<string>()
   232                                                          }>
   233                                                          {(paths: string[]) => (
   234                                                              <FormField
   235                                                                  formApi={api}
   236                                                                  label='Path'
   237                                                                  field='spec.source.path'
   238                                                                  component={AutocompleteField}
   239                                                                  componentProps={{
   240                                                                      items: paths,
   241                                                                      filterSuggestions: true
   242                                                                  }}
   243                                                              />
   244                                                          )}
   245                                                      </DataLoader>
   246                                                  </div>
   247                                                  <div className='argo-form-row'>
   248                                                      <FormField formApi={api} label='Ref' field={'spec.source.ref'} component={Text}></FormField>
   249                                                  </div>
   250                                              </React.Fragment>
   251                                          )) ||
   252                                              (repoType === 'git' && (
   253                                                  <React.Fragment>
   254                                                      <RevisionFormField
   255                                                          formApi={api}
   256                                                          helpIconTop={'2.5em'}
   257                                                          repoURL={api.getFormState().values.spec?.source?.repoURL}
   258                                                          repoType={repoType}
   259                                                      />
   260                                                      <div className='argo-form-row'>
   261                                                          <DataLoader
   262                                                              input={{
   263                                                                  repoURL: api.getFormState().values.spec?.source?.repoURL,
   264                                                                  revision: api.getFormState().values.spec?.source?.targetRevision
   265                                                              }}
   266                                                              load={async src =>
   267                                                                  (src.repoURL &&
   268                                                                      (await services.repos
   269                                                                          .apps(src.repoURL, src.revision, appInEdit.metadata.name, props.appCurrent.spec.project)
   270                                                                          .then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
   271                                                                          .catch(() => new Array<string>()))) ||
   272                                                                  new Array<string>()
   273                                                              }>
   274                                                              {(apps: string[]) => (
   275                                                                  <FormField
   276                                                                      formApi={api}
   277                                                                      label='Path'
   278                                                                      field='spec.source.path'
   279                                                                      component={AutocompleteField}
   280                                                                      componentProps={{
   281                                                                          items: apps,
   282                                                                          filterSuggestions: true
   283                                                                      }}
   284                                                                  />
   285                                                              )}
   286                                                          </DataLoader>
   287                                                      </div>
   288                                                      <div className='argo-form-row'>
   289                                                          <FormField formApi={api} label='Ref' field={'spec.source.ref'} component={Text}></FormField>
   290                                                      </div>
   291                                                  </React.Fragment>
   292                                              )) || (
   293                                                  <DataLoader
   294                                                      input={{repoURL: api.getFormState().values.spec.source.repoURL}}
   295                                                      load={async src =>
   296                                                          (src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
   297                                                          new Array<models.HelmChart>()
   298                                                      }>
   299                                                      {(charts: models.HelmChart[]) => {
   300                                                          const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec?.source?.chart);
   301                                                          return (
   302                                                              <div className='row argo-form-row'>
   303                                                                  <div className='columns small-10'>
   304                                                                      <FormField
   305                                                                          formApi={api}
   306                                                                          label='Chart'
   307                                                                          field='spec.source.chart'
   308                                                                          component={AutocompleteField}
   309                                                                          componentProps={{
   310                                                                              items: charts.map(chart => chart.name),
   311                                                                              filterSuggestions: true
   312                                                                          }}
   313                                                                      />
   314                                                                  </div>
   315                                                                  <div className='columns small-2'>
   316                                                                      <FormField
   317                                                                          formApi={api}
   318                                                                          field='spec.source.targetRevision'
   319                                                                          component={AutocompleteField}
   320                                                                          componentProps={{
   321                                                                              items: (selectedChart && selectedChart.versions) || []
   322                                                                          }}
   323                                                                      />
   324                                                                      <RevisionHelpIcon type='helm' />
   325                                                                  </div>
   326                                                              </div>
   327                                                          );
   328                                                      }}
   329                                                  </DataLoader>
   330                                              )}
   331                                      </div>
   332                                  );
   333  
   334                                  const typePanel = () => (
   335                                      <DataLoader
   336                                          input={{
   337                                              repoURL: appInEdit.spec?.source?.repoURL,
   338                                              path: appInEdit.spec?.source?.path,
   339                                              chart: appInEdit.spec?.source?.chart,
   340                                              targetRevision: appInEdit.spec?.source?.targetRevision,
   341                                              appName: appInEdit.metadata.name
   342                                          }}
   343                                          load={async src => {
   344                                              if (src?.repoURL && src?.targetRevision && (src?.path || src?.chart)) {
   345                                                  return services.repos.appDetails(src, src?.appName, props.appCurrent.spec?.project, 0, 0).catch(() => ({
   346                                                      type: 'Directory',
   347                                                      details: {}
   348                                                  }));
   349                                              } else {
   350                                                  return {
   351                                                      type: 'Directory',
   352                                                      details: {}
   353                                                  };
   354                                              }
   355                                          }}>
   356                                          {(details: models.RepoAppDetails) => {
   357                                              const type = (explicitPathType && explicitPathType.path === appInEdit.spec?.source?.path && explicitPathType.type) || details.type;
   358                                              if (details.type !== type) {
   359                                                  switch (type) {
   360                                                      case 'Helm':
   361                                                          details = {
   362                                                              type,
   363                                                              path: details.path,
   364                                                              helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
   365                                                          };
   366                                                          break;
   367                                                      case 'Kustomize':
   368                                                          details = {type, path: details.path, kustomize: {path: ''}};
   369                                                          break;
   370                                                      case 'Plugin':
   371                                                          details = {type, path: details.path, plugin: {name: '', env: []}};
   372                                                          break;
   373                                                      // Directory
   374                                                      default:
   375                                                          details = {type, path: details.path, directory: {}};
   376                                                          break;
   377                                                  }
   378                                              }
   379                                              return (
   380                                                  <React.Fragment>
   381                                                      <DropDownMenu
   382                                                          anchor={() => (
   383                                                              <p>
   384                                                                  {type} <i className='fa fa-caret-down' />
   385                                                              </p>
   386                                                          )}
   387                                                          items={appTypes.map(item => ({
   388                                                              title: item.type,
   389                                                              action: () => {
   390                                                                  setExplicitPathType({type: item.type, path: appInEdit.spec?.source?.path});
   391                                                                  normalizeTypeFields(api, item.type);
   392                                                              }
   393                                                          }))}
   394                                                      />
   395                                                      <ApplicationParameters
   396                                                          noReadonlyMode={true}
   397                                                          application={api.getFormState().values as models.Application}
   398                                                          details={details}
   399                                                          tempSource={appInEdit.spec.source}
   400                                                          save={async updatedApp => {
   401                                                              api.setAllValues(updatedApp);
   402                                                          }}
   403                                                      />
   404                                                  </React.Fragment>
   405                                              );
   406                                          }}
   407                                      </DataLoader>
   408                                  );
   409  
   410                                  return (
   411                                      <form onSubmit={api.submitForm} role='form' className='width-control'>
   412                                          {sourcePanel()}
   413  
   414                                          {typePanel()}
   415                                      </form>
   416                                  );
   417                              }}
   418                          </Form>
   419                      </div>
   420                  );
   421              }}
   422          </DataLoader>
   423      );
   424  };