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 }