github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx (about)

     1  import {AutocompleteField, Checkbox, DataLoader, DropDownMenu, FormField, HelpIcon, Select} from 'argo-ui';
     2  import * as deepMerge from 'deepmerge';
     3  import * as React from 'react';
     4  import {FieldApi, Form, FormApi, FormField as ReactFormField, Text} from 'react-form';
     5  import {RevisionHelpIcon, YamlEditor} from '../../../shared/components';
     6  import * as models from '../../../shared/models';
     7  import {services} from '../../../shared/services';
     8  import {ApplicationParameters} from '../application-parameters/application-parameters';
     9  import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
    10  import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options';
    11  import {RevisionFormField} from '../revision-form-field/revision-form-field';
    12  import {SetFinalizerOnApplication} from './set-finalizer-on-application';
    13  import './application-create-panel.scss';
    14  import {getAppDefaultSource} from '../utils';
    15  
    16  const jsonMergePatch = require('json-merge-patch');
    17  
    18  const appTypes = new Array<{field: string; type: models.AppSourceType}>(
    19      {type: 'Helm', field: 'helm'},
    20      {type: 'Kustomize', field: 'kustomize'},
    21      {type: 'Directory', field: 'directory'},
    22      {type: 'Plugin', field: 'plugin'}
    23  );
    24  
    25  const DEFAULT_APP: Partial<models.Application> = {
    26      apiVersion: 'argoproj.io/v1alpha1',
    27      kind: 'Application',
    28      metadata: {
    29          name: ''
    30      },
    31      spec: {
    32          destination: {
    33              name: '',
    34              namespace: '',
    35              server: ''
    36          },
    37          source: {
    38              path: '',
    39              repoURL: '',
    40              targetRevision: 'HEAD'
    41          },
    42          sources: [],
    43          project: ''
    44      }
    45  };
    46  
    47  const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi; className: string}) => {
    48      const manual = 'Manual';
    49      const auto = 'Automatic';
    50      const {
    51          fieldApi: {getValue, setValue}
    52      } = props;
    53      const automated = getValue() as models.Automated;
    54  
    55      return (
    56          <React.Fragment>
    57              <label>Sync Policy</label>
    58              <Select
    59                  value={automated ? auto : manual}
    60                  options={[manual, auto]}
    61                  onChange={opt => {
    62                      setValue(opt.value === auto ? {prune: false, selfHeal: false} : null);
    63                  }}
    64              />
    65              {automated && (
    66                  <div className='application-create-panel__sync-params'>
    67                      <div className='checkbox-container'>
    68                          <Checkbox onChange={val => setValue({...automated, prune: val})} checked={!!automated.prune} id='policyPrune' />
    69                          <label htmlFor='policyPrune'>Prune Resources</label>
    70                          <HelpIcon title='If checked, Argo will delete resources if they are no longer defined in Git' />
    71                      </div>
    72                      <div className='checkbox-container'>
    73                          <Checkbox onChange={val => setValue({...automated, selfHeal: val})} checked={!!automated.selfHeal} id='policySelfHeal' />
    74                          <label htmlFor='policySelfHeal'>Self Heal</label>
    75                          <HelpIcon title='If checked, Argo will force the state defined in Git into the cluster when a deviation in the cluster is detected' />
    76                      </div>
    77                  </div>
    78              )}
    79          </React.Fragment>
    80      );
    81  });
    82  
    83  function normalizeAppSource(app: models.Application, type: string): boolean {
    84      const source = getAppDefaultSource(app);
    85      const repoType = (source.hasOwnProperty('chart') && 'helm') || 'git';
    86      if (repoType !== type) {
    87          if (type === 'git') {
    88              source.path = source.chart;
    89              delete source.chart;
    90              source.targetRevision = 'HEAD';
    91          } else {
    92              source.chart = source.path;
    93              delete source.path;
    94              source.targetRevision = '';
    95          }
    96          return true;
    97      }
    98      return false;
    99  }
   100  
   101  export const ApplicationCreatePanel = (props: {
   102      app: models.Application;
   103      onAppChanged: (app: models.Application) => any;
   104      createApp: (app: models.Application) => any;
   105      getFormApi: (api: FormApi) => any;
   106  }) => {
   107      const [yamlMode, setYamlMode] = React.useState(false);
   108      const [explicitPathType, setExplicitPathType] = React.useState<{path: string; type: models.AppSourceType}>(null);
   109      const [destFormat, setDestFormat] = React.useState('URL');
   110      const [retry, setRetry] = React.useState(false);
   111      const app = deepMerge(DEFAULT_APP, props.app || {});
   112  
   113      React.useEffect(() => {
   114          if (app?.spec?.destination?.name && app.spec.destination.name !== '') {
   115              setDestFormat('NAME');
   116          } else {
   117              setDestFormat('URL');
   118          }
   119      }, []);
   120  
   121      function normalizeTypeFields(formApi: FormApi, type: models.AppSourceType) {
   122          const appToNormalize = formApi.getFormState().values;
   123          for (const item of appTypes) {
   124              if (item.type !== type) {
   125                  delete appToNormalize.spec.source[item.field];
   126              }
   127          }
   128          formApi.setAllValues(appToNormalize);
   129      }
   130  
   131      return (
   132          <React.Fragment>
   133              <DataLoader
   134                  key='creation-deps'
   135                  load={() =>
   136                      Promise.all([
   137                          services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort()),
   138                          services.clusters.list().then(clusters => clusters.sort()),
   139                          services.repos.list()
   140                      ]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo}))
   141                  }>
   142                  {({projects, clusters, reposInfo}) => {
   143                      const repos = reposInfo.map(info => info.repo).sort();
   144                      const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL);
   145                      if (repoInfo) {
   146                          normalizeAppSource(app, repoInfo.type || 'git');
   147                      }
   148                      return (
   149                          <div className='application-create-panel'>
   150                              {(yamlMode && (
   151                                  <YamlEditor
   152                                      minHeight={800}
   153                                      initialEditMode={true}
   154                                      input={app}
   155                                      onCancel={() => setYamlMode(false)}
   156                                      onSave={async patch => {
   157                                          props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch)));
   158                                          setYamlMode(false);
   159                                          return true;
   160                                      }}
   161                                  />
   162                              )) || (
   163                                  <Form
   164                                      validateError={(a: models.Application) => ({
   165                                          'metadata.name': !a.metadata.name && 'Application Name is required',
   166                                          'spec.project': !a.spec.project && 'Project Name is required',
   167                                          'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required',
   168                                          'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required',
   169                                          'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required',
   170                                          'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required',
   171                                          // Verify cluster URL when there is no cluster name field or the name value is empty
   172                                          'spec.destination.server':
   173                                              !a.spec.destination.server &&
   174                                              (!a.spec.destination.hasOwnProperty('name') || a.spec.destination.name === '') &&
   175                                              'Cluster URL is required',
   176                                          // Verify cluster name when there is no cluster URL field or the URL value is empty
   177                                          'spec.destination.name':
   178                                              !a.spec.destination.name &&
   179                                              (!a.spec.destination.hasOwnProperty('server') || a.spec.destination.server === '') &&
   180                                              'Cluster name is required'
   181                                      })}
   182                                      defaultValues={app}
   183                                      formDidUpdate={state => props.onAppChanged(state.values as any)}
   184                                      onSubmit={props.createApp}
   185                                      getApi={props.getFormApi}>
   186                                      {api => {
   187                                          const generalPanel = () => (
   188                                              <div className='white-box'>
   189                                                  <p>GENERAL</p>
   190                                                  {/*
   191                                                      Need to specify "type='button'" because the default type 'submit'
   192                                                      will activate yaml mode whenever enter is pressed while in the panel.
   193                                                      This causes problems with some entry fields that require enter to be
   194                                                      pressed for the value to be accepted.
   195  
   196                                                      See https://github.com/argoproj/argo-cd/issues/4576
   197                                                  */}
   198                                                  {!yamlMode && (
   199                                                      <button
   200                                                          type='button'
   201                                                          className='argo-button argo-button--base application-create-panel__yaml-button'
   202                                                          onClick={() => setYamlMode(true)}>
   203                                                          Edit as YAML
   204                                                      </button>
   205                                                  )}
   206                                                  <div className='argo-form-row'>
   207                                                      <FormField
   208                                                          formApi={api}
   209                                                          label='Application Name'
   210                                                          qeId='application-create-field-app-name'
   211                                                          field='metadata.name'
   212                                                          component={Text}
   213                                                      />
   214                                                  </div>
   215                                                  <div className='argo-form-row'>
   216                                                      <FormField
   217                                                          formApi={api}
   218                                                          label='Project Name'
   219                                                          qeId='application-create-field-project'
   220                                                          field='spec.project'
   221                                                          component={AutocompleteField}
   222                                                          componentProps={{
   223                                                              items: projects,
   224                                                              filterSuggestions: true
   225                                                          }}
   226                                                      />
   227                                                  </div>
   228                                                  <div className='argo-form-row'>
   229                                                      <FormField
   230                                                          formApi={api}
   231                                                          field='spec.syncPolicy.automated'
   232                                                          qeId='application-create-field-sync-policy'
   233                                                          component={AutoSyncFormField}
   234                                                      />
   235                                                  </div>
   236                                                  <div className='argo-form-row'>
   237                                                      <FormField formApi={api} field='metadata.finalizers' component={SetFinalizerOnApplication} />
   238                                                  </div>
   239                                                  <div className='argo-form-row'>
   240                                                      <label>Sync Options</label>
   241                                                      <FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
   242                                                      <ApplicationRetryOptions
   243                                                          formApi={api}
   244                                                          field='spec.syncPolicy.retry'
   245                                                          retry={retry || (api.getFormState().values.spec.syncPolicy && api.getFormState().values.spec.syncPolicy.retry)}
   246                                                          setRetry={setRetry}
   247                                                          initValues={api.getFormState().values.spec.syncPolicy ? api.getFormState().values.spec.syncPolicy.retry : null}
   248                                                      />
   249                                                  </div>
   250                                              </div>
   251                                          );
   252  
   253                                          const repoType = (api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm') || 'git';
   254                                          const sourcePanel = () => (
   255                                              <div className='white-box'>
   256                                                  <p>SOURCE</p>
   257                                                  <div className='row argo-form-row'>
   258                                                      <div className='columns small-10'>
   259                                                          <FormField
   260                                                              formApi={api}
   261                                                              label='Repository URL'
   262                                                              qeId='application-create-field-repository-url'
   263                                                              field='spec.source.repoURL'
   264                                                              component={AutocompleteField}
   265                                                              componentProps={{items: repos}}
   266                                                          />
   267                                                      </div>
   268                                                      <div className='columns small-2'>
   269                                                          <div style={{paddingTop: '1.5em'}}>
   270                                                              {(repoInfo && (
   271                                                                  <React.Fragment>
   272                                                                      <span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
   273                                                                  </React.Fragment>
   274                                                              )) || (
   275                                                                  <DropDownMenu
   276                                                                      anchor={() => (
   277                                                                          <p>
   278                                                                              {repoType.toUpperCase()} <i className='fa fa-caret-down' />
   279                                                                          </p>
   280                                                                      )}
   281                                                                      qeId='application-create-dropdown-source-repository'
   282                                                                      items={['git', 'helm'].map((type: 'git' | 'helm') => ({
   283                                                                          title: type.toUpperCase(),
   284                                                                          action: () => {
   285                                                                              if (repoType !== type) {
   286                                                                                  const updatedApp = api.getFormState().values as models.Application;
   287                                                                                  if (normalizeAppSource(updatedApp, type)) {
   288                                                                                      api.setAllValues(updatedApp);
   289                                                                                  }
   290                                                                              }
   291                                                                          }
   292                                                                      }))}
   293                                                                  />
   294                                                              )}
   295                                                          </div>
   296                                                      </div>
   297                                                  </div>
   298                                                  {(repoType === 'git' && (
   299                                                      <React.Fragment>
   300                                                          <RevisionFormField formApi={api} helpIconTop={'2.5em'} repoURL={app.spec.source.repoURL} />
   301                                                          <div className='argo-form-row'>
   302                                                              <DataLoader
   303                                                                  input={{repoURL: app.spec.source.repoURL, revision: app.spec.source.targetRevision}}
   304                                                                  load={async src =>
   305                                                                      (src.repoURL &&
   306                                                                          services.repos
   307                                                                              .apps(src.repoURL, src.revision, app.metadata.name, app.spec.project)
   308                                                                              .then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
   309                                                                              .catch(() => new Array<string>())) ||
   310                                                                      new Array<string>()
   311                                                                  }>
   312                                                                  {(apps: string[]) => (
   313                                                                      <FormField
   314                                                                          formApi={api}
   315                                                                          label='Path'
   316                                                                          qeId='application-create-field-path'
   317                                                                          field='spec.source.path'
   318                                                                          component={AutocompleteField}
   319                                                                          componentProps={{
   320                                                                              items: apps,
   321                                                                              filterSuggestions: true
   322                                                                          }}
   323                                                                      />
   324                                                                  )}
   325                                                              </DataLoader>
   326                                                          </div>
   327                                                      </React.Fragment>
   328                                                  )) || (
   329                                                      <DataLoader
   330                                                          input={{repoURL: app.spec.source.repoURL}}
   331                                                          load={async src =>
   332                                                              (src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
   333                                                              new Array<models.HelmChart>()
   334                                                          }>
   335                                                          {(charts: models.HelmChart[]) => {
   336                                                              const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec.source.chart);
   337                                                              return (
   338                                                                  <div className='row argo-form-row'>
   339                                                                      <div className='columns small-10'>
   340                                                                          <FormField
   341                                                                              formApi={api}
   342                                                                              label='Chart'
   343                                                                              field='spec.source.chart'
   344                                                                              component={AutocompleteField}
   345                                                                              componentProps={{
   346                                                                                  items: charts.map(chart => chart.name),
   347                                                                                  filterSuggestions: true
   348                                                                              }}
   349                                                                          />
   350                                                                      </div>
   351                                                                      <div className='columns small-2'>
   352                                                                          <FormField
   353                                                                              formApi={api}
   354                                                                              field='spec.source.targetRevision'
   355                                                                              component={AutocompleteField}
   356                                                                              componentProps={{
   357                                                                                  items: (selectedChart && selectedChart.versions) || []
   358                                                                              }}
   359                                                                          />
   360                                                                          <RevisionHelpIcon type='helm' />
   361                                                                      </div>
   362                                                                  </div>
   363                                                              );
   364                                                          }}
   365                                                      </DataLoader>
   366                                                  )}
   367                                              </div>
   368                                          );
   369                                          const destinationPanel = () => (
   370                                              <div className='white-box'>
   371                                                  <p>DESTINATION</p>
   372                                                  <div className='row argo-form-row'>
   373                                                      {(destFormat.toUpperCase() === 'URL' && (
   374                                                          <div className='columns small-10'>
   375                                                              <FormField
   376                                                                  formApi={api}
   377                                                                  label='Cluster URL'
   378                                                                  qeId='application-create-field-cluster-url'
   379                                                                  field='spec.destination.server'
   380                                                                  componentProps={{items: clusters.map(cluster => cluster.server)}}
   381                                                                  component={AutocompleteField}
   382                                                              />
   383                                                          </div>
   384                                                      )) || (
   385                                                          <div className='columns small-10'>
   386                                                              <FormField
   387                                                                  formApi={api}
   388                                                                  label='Cluster Name'
   389                                                                  qeId='application-create-field-cluster-name'
   390                                                                  field='spec.destination.name'
   391                                                                  componentProps={{items: clusters.map(cluster => cluster.name)}}
   392                                                                  component={AutocompleteField}
   393                                                              />
   394                                                          </div>
   395                                                      )}
   396                                                      <div className='columns small-2'>
   397                                                          <div style={{paddingTop: '1.5em'}}>
   398                                                              <DropDownMenu
   399                                                                  anchor={() => (
   400                                                                      <p>
   401                                                                          {destFormat} <i className='fa fa-caret-down' />
   402                                                                      </p>
   403                                                                  )}
   404                                                                  qeId='application-create-dropdown-destination'
   405                                                                  items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
   406                                                                      title: type,
   407                                                                      action: () => {
   408                                                                          if (destFormat !== type) {
   409                                                                              const updatedApp = api.getFormState().values as models.Application;
   410                                                                              if (type === 'URL') {
   411                                                                                  delete updatedApp.spec.destination.name;
   412                                                                              } else {
   413                                                                                  delete updatedApp.spec.destination.server;
   414                                                                              }
   415                                                                              api.setAllValues(updatedApp);
   416                                                                              setDestFormat(type);
   417                                                                          }
   418                                                                      }
   419                                                                  }))}
   420                                                              />
   421                                                          </div>
   422                                                      </div>
   423                                                  </div>
   424                                                  <div className='argo-form-row'>
   425                                                      <FormField
   426                                                          qeId='application-create-field-namespace'
   427                                                          formApi={api}
   428                                                          label='Namespace'
   429                                                          field='spec.destination.namespace'
   430                                                          component={Text}
   431                                                      />
   432                                                  </div>
   433                                              </div>
   434                                          );
   435  
   436                                          const typePanel = () => (
   437                                              <DataLoader
   438                                                  input={{
   439                                                      repoURL: app.spec.source.repoURL,
   440                                                      path: app.spec.source.path,
   441                                                      chart: app.spec.source.chart,
   442                                                      targetRevision: app.spec.source.targetRevision,
   443                                                      appName: app.metadata.name
   444                                                  }}
   445                                                  load={async src => {
   446                                                      if (src.repoURL && src.targetRevision && (src.path || src.chart)) {
   447                                                          return services.repos.appDetails(src, src.appName, app.spec.project).catch(() => ({
   448                                                              type: 'Directory',
   449                                                              details: {}
   450                                                          }));
   451                                                      } else {
   452                                                          return {
   453                                                              type: 'Directory',
   454                                                              details: {}
   455                                                          };
   456                                                      }
   457                                                  }}>
   458                                                  {(details: models.RepoAppDetails) => {
   459                                                      const type = (explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type) || details.type;
   460                                                      if (details.type !== type) {
   461                                                          switch (type) {
   462                                                              case 'Helm':
   463                                                                  details = {
   464                                                                      type,
   465                                                                      path: details.path,
   466                                                                      helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
   467                                                                  };
   468                                                                  break;
   469                                                              case 'Kustomize':
   470                                                                  details = {type, path: details.path, kustomize: {path: ''}};
   471                                                                  break;
   472                                                              case 'Plugin':
   473                                                                  details = {type, path: details.path, plugin: {name: '', env: []}};
   474                                                                  break;
   475                                                              // Directory
   476                                                              default:
   477                                                                  details = {type, path: details.path, directory: {}};
   478                                                                  break;
   479                                                          }
   480                                                      }
   481                                                      return (
   482                                                          <React.Fragment>
   483                                                              <DropDownMenu
   484                                                                  anchor={() => (
   485                                                                      <p>
   486                                                                          {type} <i className='fa fa-caret-down' />
   487                                                                      </p>
   488                                                                  )}
   489                                                                  qeId='application-create-dropdown-source'
   490                                                                  items={appTypes.map(item => ({
   491                                                                      title: item.type,
   492                                                                      action: () => {
   493                                                                          setExplicitPathType({type: item.type, path: app.spec.source.path});
   494                                                                          normalizeTypeFields(api, item.type);
   495                                                                      }
   496                                                                  }))}
   497                                                              />
   498                                                              <ApplicationParameters
   499                                                                  noReadonlyMode={true}
   500                                                                  application={app}
   501                                                                  details={details}
   502                                                                  save={async updatedApp => {
   503                                                                      api.setAllValues(updatedApp);
   504                                                                  }}
   505                                                              />
   506                                                          </React.Fragment>
   507                                                      );
   508                                                  }}
   509                                              </DataLoader>
   510                                          );
   511  
   512                                          return (
   513                                              <form onSubmit={api.submitForm} role='form' className='width-control'>
   514                                                  {generalPanel()}
   515  
   516                                                  {sourcePanel()}
   517  
   518                                                  {destinationPanel()}
   519  
   520                                                  {typePanel()}
   521                                              </form>
   522                                          );
   523                                      }}
   524                                  </Form>
   525                              )}
   526                          </div>
   527                      );
   528                  }}
   529              </DataLoader>
   530          </React.Fragment>
   531      );
   532  };