github.com/argoproj/argo-cd@v1.8.7/ui/src/app/settings/components/repos-list/repos-list.tsx (about)

     1  import {DropDownMenu, FormField, FormSelect, HelpIcon, NotificationType, SlidingPanel} from 'argo-ui';
     2  import * as PropTypes from 'prop-types';
     3  import * as React from 'react';
     4  import {Form, FormApi, Text, TextArea} from 'react-form';
     5  import {RouteComponentProps} from 'react-router';
     6  
     7  import {CheckboxField, ConnectionStateIcon, DataLoader, EmptyState, ErrorNotification, Page, Repo} from '../../../shared/components';
     8  import {Spinner} from '../../../shared/components';
     9  import {AppContext} from '../../../shared/context';
    10  import * as models from '../../../shared/models';
    11  import {services} from '../../../shared/services';
    12  
    13  require('./repos-list.scss');
    14  
    15  interface NewSSHRepoParams {
    16      type: string;
    17      name: string;
    18      url: string;
    19      sshPrivateKey: string;
    20      insecure: boolean;
    21      enableLfs: boolean;
    22  }
    23  
    24  interface NewHTTPSRepoParams {
    25      type: string;
    26      name: string;
    27      url: string;
    28      username: string;
    29      password: string;
    30      tlsClientCertData: string;
    31      tlsClientCertKey: string;
    32      insecure: boolean;
    33      enableLfs: boolean;
    34  }
    35  
    36  interface NewSSHRepoCredsParams {
    37      url: string;
    38      sshPrivateKey: string;
    39  }
    40  
    41  interface NewHTTPSRepoCredsParams {
    42      url: string;
    43      username: string;
    44      password: string;
    45      tlsClientCertData: string;
    46      tlsClientCertKey: string;
    47  }
    48  
    49  export class ReposList extends React.Component<RouteComponentProps<any>, {connecting: boolean}> {
    50      public static contextTypes = {
    51          router: PropTypes.object,
    52          apis: PropTypes.object,
    53          history: PropTypes.object
    54      };
    55  
    56      private formApiSSH: FormApi;
    57      private formApiHTTPS: FormApi;
    58      private credsTemplate: boolean;
    59      private repoLoader: DataLoader;
    60      private credsLoader: DataLoader;
    61  
    62      constructor(props: RouteComponentProps<any>) {
    63          super(props);
    64          this.state = {connecting: false};
    65      }
    66  
    67      public render() {
    68          return (
    69              <Page
    70                  title='Repositories'
    71                  toolbar={{
    72                      breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Repositories'}],
    73                      actionMenu: {
    74                          items: [
    75                              {
    76                                  iconClassName: 'fa fa-plus',
    77                                  title: 'Connect Repo using SSH',
    78                                  action: () => (this.showConnectSSHRepo = true)
    79                              },
    80                              {
    81                                  iconClassName: 'fa fa-plus',
    82                                  title: 'Connect Repo using HTTPS',
    83                                  action: () => (this.showConnectHTTPSRepo = true)
    84                              },
    85                              {
    86                                  iconClassName: 'fa fa-redo',
    87                                  title: 'Refresh list',
    88                                  action: () => {
    89                                      this.refreshRepoList();
    90                                  }
    91                              }
    92                          ]
    93                      }
    94                  }}>
    95                  <div className='repos-list'>
    96                      <div className='argo-container'>
    97                          <DataLoader load={() => services.repos.list()} ref={loader => (this.repoLoader = loader)}>
    98                              {(repos: models.Repository[]) =>
    99                                  (repos.length > 0 && (
   100                                      <div className='argo-table-list'>
   101                                          <div className='argo-table-list__head'>
   102                                              <div className='row'>
   103                                                  <div className='columns small-1' />
   104                                                  <div className='columns small-1'>TYPE</div>
   105                                                  <div className='columns small-2'>NAME</div>
   106                                                  <div className='columns small-5'>REPOSITORY</div>
   107                                                  <div className='columns small-3'>CONNECTION STATUS</div>
   108                                              </div>
   109                                          </div>
   110                                          {repos.map(repo => (
   111                                              <div className='argo-table-list__row' key={repo.repo}>
   112                                                  <div className='row'>
   113                                                      <div className='columns small-1'>
   114                                                          <i className={'icon argo-icon-' + (repo.type || 'git')} />
   115                                                      </div>
   116                                                      <div className='columns small-1'>{repo.type || 'git'}</div>
   117                                                      <div className='columns small-2'>{repo.name}</div>
   118                                                      <div className='columns small-5'>
   119                                                          <Repo url={repo.repo} />
   120                                                      </div>
   121                                                      <div className='columns small-3'>
   122                                                          <ConnectionStateIcon state={repo.connectionState} /> {repo.connectionState.status}
   123                                                          <DropDownMenu
   124                                                              anchor={() => (
   125                                                                  <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   126                                                                      <i className='fa fa-ellipsis-v' />
   127                                                                  </button>
   128                                                              )}
   129                                                              items={[
   130                                                                  {
   131                                                                      title: 'Create application',
   132                                                                      action: () =>
   133                                                                          this.appContext.apis.navigation.goto('/applications', {
   134                                                                              new: JSON.stringify({spec: {source: {repoURL: repo.repo}}})
   135                                                                          })
   136                                                                  },
   137                                                                  {
   138                                                                      title: 'Disconnect',
   139                                                                      action: () => this.disconnectRepo(repo.repo)
   140                                                                  }
   141                                                              ]}
   142                                                          />
   143                                                      </div>
   144                                                  </div>
   145                                              </div>
   146                                          ))}
   147                                      </div>
   148                                  )) || (
   149                                      <EmptyState icon='argo-icon-git'>
   150                                          <h4>No repositories connected</h4>
   151                                          <h5>Connect your repo to deploy apps.</h5>
   152                                          <button className='argo-button argo-button--base' onClick={() => (this.showConnectSSHRepo = true)}>
   153                                              Connect Repo using SSH
   154                                          </button>{' '}
   155                                          <button className='argo-button argo-button--base' onClick={() => (this.showConnectHTTPSRepo = true)}>
   156                                              Connect Repo using HTTPS
   157                                          </button>
   158                                      </EmptyState>
   159                                  )
   160                              }
   161                          </DataLoader>
   162                      </div>
   163                      <div className='argo-container'>
   164                          <DataLoader load={() => services.repocreds.list()} ref={loader => (this.credsLoader = loader)}>
   165                              {(creds: models.RepoCreds[]) =>
   166                                  creds.length > 0 && (
   167                                      <div className='argo-table-list'>
   168                                          <div className='argo-table-list__head'>
   169                                              <div className='row'>
   170                                                  <div className='columns small-9'>CREDENTIALS TEMPLATE URL</div>
   171                                                  <div className='columns small-3'>CREDS</div>
   172                                              </div>
   173                                          </div>
   174                                          {creds.map(repo => (
   175                                              <div className='argo-table-list__row' key={repo.url}>
   176                                                  <div className='row'>
   177                                                      <div className='columns small-9'>
   178                                                          <i className='icon argo-icon-git' /> <Repo url={repo.url} />
   179                                                      </div>
   180                                                      <div className='columns small-3'>
   181                                                          -
   182                                                          <DropDownMenu
   183                                                              anchor={() => (
   184                                                                  <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   185                                                                      <i className='fa fa-ellipsis-v' />
   186                                                                  </button>
   187                                                              )}
   188                                                              items={[{title: 'Remove', action: () => this.removeRepoCreds(repo.url)}]}
   189                                                          />
   190                                                      </div>
   191                                                  </div>
   192                                              </div>
   193                                          ))}
   194                                      </div>
   195                                  )
   196                              }
   197                          </DataLoader>
   198                      </div>
   199                  </div>
   200                  <SlidingPanel
   201                      isShown={this.showConnectHTTPSRepo}
   202                      onClose={() => (this.showConnectHTTPSRepo = false)}
   203                      header={
   204                          <div>
   205                              <button
   206                                  className='argo-button argo-button--base'
   207                                  onClick={() => {
   208                                      this.credsTemplate = false;
   209                                      this.formApiHTTPS.submitForm(null);
   210                                  }}>
   211                                  <Spinner show={this.state.connecting} style={{marginRight: '5px'}} />
   212                                  Connect
   213                              </button>{' '}
   214                              <button
   215                                  className='argo-button argo-button--base'
   216                                  onClick={() => {
   217                                      this.credsTemplate = true;
   218                                      this.formApiHTTPS.submitForm(null);
   219                                  }}>
   220                                  Save as credentials template
   221                              </button>{' '}
   222                              <button onClick={() => (this.showConnectHTTPSRepo = false)} className='argo-button argo-button--base-o'>
   223                                  Cancel
   224                              </button>
   225                          </div>
   226                      }>
   227                      <h4>Connect repo using HTTPS</h4>
   228                      <Form
   229                          onSubmit={params => this.connectHTTPSRepo(params as NewHTTPSRepoParams)}
   230                          getApi={api => (this.formApiHTTPS = api)}
   231                          defaultValues={{type: 'git'}}
   232                          validateError={(params: NewHTTPSRepoParams) => ({
   233                              url: (!params.url && 'Repo URL is required') || (this.credsTemplate && !this.isHTTPSUrl(params.url) && 'Not a valid HTTPS URL'),
   234                              name: params.type === 'helm' && !params.name && 'Name is required',
   235                              password: !params.password && params.username && 'Password is required if username is given.',
   236                              tlsClientCertKey: !params.tlsClientCertKey && params.tlsClientCertData && 'TLS client cert key is required if TLS client cert is given.'
   237                          })}>
   238                          {formApi => (
   239                              <form onSubmit={formApi.submitForm} role='form' className='repos-list width-control'>
   240                                  <div className='argo-form-row'>
   241                                      <FormField formApi={formApi} label='Type' field='type' component={FormSelect} componentProps={{options: ['git', 'helm']}} />
   242                                  </div>
   243                                  {formApi.getFormState().values.type === 'helm' && (
   244                                      <div className='argo-form-row'>
   245                                          <FormField formApi={formApi} label='Name' field='name' component={Text} />
   246                                      </div>
   247                                  )}
   248                                  <div className='argo-form-row'>
   249                                      <FormField formApi={formApi} label='Repository URL' field='url' component={Text} />
   250                                  </div>
   251                                  <div className='argo-form-row'>
   252                                      <FormField formApi={formApi} label='Username (optional)' field='username' component={Text} />
   253                                  </div>
   254                                  <div className='argo-form-row'>
   255                                      <FormField formApi={formApi} label='Password (optional)' field='password' component={Text} componentProps={{type: 'password'}} />
   256                                  </div>
   257                                  <div className='argo-form-row'>
   258                                      <FormField formApi={formApi} label='TLS client certificate (optional)' field='tlsClientCertData' component={TextArea} />
   259                                  </div>
   260                                  <div className='argo-form-row'>
   261                                      <FormField formApi={formApi} label='TLS client certificate key (optional)' field='tlsClientCertKey' component={TextArea} />
   262                                  </div>
   263                                  {formApi.getFormState().values.type === 'git' && (
   264                                      <React.Fragment>
   265                                          <div className='argo-form-row'>
   266                                              <FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} />
   267                                              <HelpIcon title='This setting is ignored when creating as credential template.' />
   268                                          </div>
   269                                          <div className='argo-form-row'>
   270                                              <FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} />
   271                                              <HelpIcon title='This setting is ignored when creating as credential template.' />
   272                                          </div>
   273                                      </React.Fragment>
   274                                  )}
   275                              </form>
   276                          )}
   277                      </Form>
   278                  </SlidingPanel>
   279                  <SlidingPanel
   280                      isShown={this.showConnectSSHRepo}
   281                      onClose={() => (this.showConnectSSHRepo = false)}
   282                      header={
   283                          <div>
   284                              <button
   285                                  className='argo-button argo-button--base'
   286                                  onClick={() => {
   287                                      this.credsTemplate = false;
   288                                      this.formApiSSH.submitForm(null);
   289                                  }}>
   290                                  <Spinner show={this.state.connecting} style={{marginRight: '5px'}} />
   291                                  Connect
   292                              </button>{' '}
   293                              <button
   294                                  className='argo-button argo-button--base'
   295                                  onClick={() => {
   296                                      this.credsTemplate = true;
   297                                      this.formApiSSH.submitForm(null);
   298                                  }}>
   299                                  Save as credentials template
   300                              </button>{' '}
   301                              <button onClick={() => (this.showConnectSSHRepo = false)} className='argo-button argo-button--base-o'>
   302                                  Cancel
   303                              </button>
   304                          </div>
   305                      }>
   306                      <h4>Connect repo using SSH</h4>
   307                      <Form
   308                          onSubmit={params => this.connectSSHRepo(params as NewSSHRepoParams)}
   309                          getApi={api => (this.formApiSSH = api)}
   310                          defaultValues={{type: 'git'}}
   311                          validateError={(params: NewSSHRepoParams) => ({
   312                              url: !params.url && 'Repo URL is required'
   313                          })}>
   314                          {formApi => (
   315                              <form onSubmit={formApi.submitForm} role='form' className='repos-list width-control'>
   316                                  <div className='argo-form-row'>
   317                                      <FormField formApi={formApi} label='Name (mandatory for Helm)' field='name' component={Text} />
   318                                  </div>
   319                                  <div className='argo-form-row'>
   320                                      <FormField formApi={formApi} label='Repository URL' field='url' component={Text} />
   321                                  </div>
   322                                  <div className='argo-form-row'>
   323                                      <FormField formApi={formApi} label='SSH private key data' field='sshPrivateKey' component={TextArea} />
   324                                  </div>
   325                                  <div className='argo-form-row'>
   326                                      <FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} />
   327                                      <HelpIcon title='This setting is ignored when creating as credential template.' />
   328                                  </div>
   329                                  <div className='argo-form-row'>
   330                                      <FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} />
   331                                      <HelpIcon title='This setting is ignored when creating as credential template.' />
   332                                  </div>
   333                              </form>
   334                          )}
   335                      </Form>
   336                  </SlidingPanel>
   337              </Page>
   338          );
   339      }
   340  
   341      // Whether url is a https url (simple version)
   342      private isHTTPSUrl(url: string) {
   343          if (url.match(/^https:\/\/.*$/gi)) {
   344              return true;
   345          } else {
   346              return false;
   347          }
   348      }
   349  
   350      // Forces a reload of configured repositories, circumventing the cache
   351      private async refreshRepoList() {
   352          try {
   353              await services.repos.listNoCache();
   354              await services.repocreds.list();
   355              this.repoLoader.reload();
   356              this.appContext.apis.notifications.show({
   357                  content: 'Successfully reloaded list of repositories',
   358                  type: NotificationType.Success
   359              });
   360          } catch (e) {
   361              this.appContext.apis.notifications.show({
   362                  content: <ErrorNotification title='Could not refresh list of repositories' e={e} />,
   363                  type: NotificationType.Error
   364              });
   365          }
   366      }
   367  
   368      // Empty all fields in SSH repository form
   369      private clearConnectSSHForm() {
   370          this.credsTemplate = false;
   371          this.formApiSSH.resetAll();
   372      }
   373  
   374      // Empty all fields in HTTPS repository form
   375      private clearConnectHTTPSForm() {
   376          this.credsTemplate = false;
   377          this.formApiHTTPS.resetAll();
   378      }
   379  
   380      // Connect a new repository or create a repository credentials for SSH repositories
   381      private async connectSSHRepo(params: NewSSHRepoParams) {
   382          if (this.credsTemplate) {
   383              this.createSSHCreds({url: params.url, sshPrivateKey: params.sshPrivateKey});
   384          } else {
   385              this.setState({connecting: true});
   386              try {
   387                  await services.repos.createSSH(params);
   388                  this.repoLoader.reload();
   389                  this.showConnectSSHRepo = false;
   390              } catch (e) {
   391                  this.appContext.apis.notifications.show({
   392                      content: <ErrorNotification title='Unable to connect SSH repository' e={e} />,
   393                      type: NotificationType.Error
   394                  });
   395              } finally {
   396                  this.setState({connecting: false});
   397              }
   398          }
   399      }
   400  
   401      // Connect a new repository or create a repository credentials for HTTPS repositories
   402      private async connectHTTPSRepo(params: NewHTTPSRepoParams) {
   403          if (this.credsTemplate) {
   404              this.createHTTPSCreds({
   405                  url: params.url,
   406                  username: params.username,
   407                  password: params.password,
   408                  tlsClientCertData: params.tlsClientCertData,
   409                  tlsClientCertKey: params.tlsClientCertKey
   410              });
   411          } else {
   412              this.setState({connecting: true});
   413              try {
   414                  await services.repos.createHTTPS(params);
   415                  this.repoLoader.reload();
   416                  this.showConnectHTTPSRepo = false;
   417              } catch (e) {
   418                  this.appContext.apis.notifications.show({
   419                      content: <ErrorNotification title='Unable to connect HTTPS repository' e={e} />,
   420                      type: NotificationType.Error
   421                  });
   422              } finally {
   423                  this.setState({connecting: false});
   424              }
   425          }
   426      }
   427  
   428      private async createHTTPSCreds(params: NewHTTPSRepoCredsParams) {
   429          try {
   430              await services.repocreds.createHTTPS(params);
   431              this.credsLoader.reload();
   432              this.showConnectHTTPSRepo = false;
   433          } catch (e) {
   434              this.appContext.apis.notifications.show({
   435                  content: <ErrorNotification title='Unable to create HTTPS credentials' e={e} />,
   436                  type: NotificationType.Error
   437              });
   438          }
   439      }
   440  
   441      private async createSSHCreds(params: NewSSHRepoCredsParams) {
   442          try {
   443              await services.repocreds.createSSH(params);
   444              this.credsLoader.reload();
   445              this.showConnectSSHRepo = false;
   446          } catch (e) {
   447              this.appContext.apis.notifications.show({
   448                  content: <ErrorNotification title='Unable to create SSH credentials' e={e} />,
   449                  type: NotificationType.Error
   450              });
   451          }
   452      }
   453  
   454      // Remove a repository from the configuration
   455      private async disconnectRepo(repo: string) {
   456          const confirmed = await this.appContext.apis.popup.confirm('Disconnect repository', `Are you sure you want to disconnect '${repo}'?`);
   457          if (confirmed) {
   458              await services.repos.delete(repo);
   459              this.repoLoader.reload();
   460          }
   461      }
   462  
   463      // Remove repository credentials from the configuration
   464      private async removeRepoCreds(url: string) {
   465          const confirmed = await this.appContext.apis.popup.confirm('Remove repository credentials', `Are you sure you want to remove credentials for URL prefix '${url}'?`);
   466          if (confirmed) {
   467              await services.repocreds.delete(url);
   468              this.credsLoader.reload();
   469          }
   470      }
   471  
   472      // Whether to show the HTTPS repository connection dialogue on the page
   473      private get showConnectHTTPSRepo() {
   474          return new URLSearchParams(this.props.location.search).get('addHTTPSRepo') === 'true';
   475      }
   476  
   477      private set showConnectHTTPSRepo(val: boolean) {
   478          this.clearConnectHTTPSForm();
   479          this.appContext.router.history.push(`${this.props.match.url}?addHTTPSRepo=${val}`);
   480      }
   481  
   482      // Whether to show the SSH repository connection dialogue on the page
   483      private get showConnectSSHRepo() {
   484          return new URLSearchParams(this.props.location.search).get('addSSHRepo') === 'true';
   485      }
   486  
   487      private set showConnectSSHRepo(val: boolean) {
   488          this.clearConnectSSHForm();
   489          this.appContext.router.history.push(`${this.props.match.url}?addSSHRepo=${val}`);
   490      }
   491  
   492      private get appContext(): AppContext {
   493          return this.context as AppContext;
   494      }
   495  }