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