github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/settings/components/repos-list/repos-list.tsx (about) 1 /* eslint-disable no-case-declarations */ 2 import {AutocompleteField, DropDownMenu, FormField, FormSelect, HelpIcon, NotificationType, SlidingPanel, Tooltip} from 'argo-ui'; 3 import * as PropTypes from 'prop-types'; 4 import * as React from 'react'; 5 import {Form, FormValues, FormApi, Text, TextArea, FormErrors} from 'react-form'; 6 import {RouteComponentProps} from 'react-router'; 7 8 import {CheckboxField, ConnectionStateIcon, DataLoader, EmptyState, ErrorNotification, NumberField, Page, Repo, Spinner} from '../../../shared/components'; 9 import {AppContext} from '../../../shared/context'; 10 import * as models from '../../../shared/models'; 11 import {services} from '../../../shared/services'; 12 import {RepoDetails} from '../repo-details/repo-details'; 13 14 require('./repos-list.scss'); 15 16 interface NewSSHRepoParams { 17 type: string; 18 name: string; 19 url: string; 20 sshPrivateKey: string; 21 insecure: boolean; 22 enableLfs: boolean; 23 proxy: string; 24 noProxy: string; 25 project?: string; 26 // write should be true if saving as a write credential. 27 write: boolean; 28 } 29 30 export interface NewHTTPSRepoParams { 31 type: string; 32 name: string; 33 url: string; 34 username: string; 35 password: string; 36 bearerToken: string; 37 tlsClientCertData: string; 38 tlsClientCertKey: string; 39 insecure: boolean; 40 enableLfs: boolean; 41 proxy: string; 42 noProxy: string; 43 project?: string; 44 forceHttpBasicAuth?: boolean; 45 enableOCI: boolean; 46 insecureOCIForceHttp: boolean; 47 // write should be true if saving as a write credential. 48 write: boolean; 49 useAzureWorkloadIdentity: boolean; 50 } 51 52 interface NewGitHubAppRepoParams { 53 type: string; 54 name: string; 55 url: string; 56 githubAppPrivateKey: string; 57 githubAppId: bigint; 58 githubAppInstallationId: bigint; 59 githubAppEnterpriseBaseURL: string; 60 tlsClientCertData: string; 61 tlsClientCertKey: string; 62 insecure: boolean; 63 enableLfs: boolean; 64 proxy: string; 65 noProxy: string; 66 project?: string; 67 // write should be true if saving as a write credential. 68 write: boolean; 69 } 70 71 interface NewGoogleCloudSourceRepoParams { 72 type: string; 73 name: string; 74 url: string; 75 gcpServiceAccountKey: string; 76 proxy: string; 77 noProxy: string; 78 project?: string; 79 // write should be true if saving as a write credential. 80 write: boolean; 81 } 82 83 interface NewSSHRepoCredsParams { 84 url: string; 85 sshPrivateKey: string; 86 // write should be true if saving as a write credential. 87 write: boolean; 88 } 89 90 interface NewHTTPSRepoCredsParams { 91 url: string; 92 type: string; 93 username: string; 94 password: string; 95 bearerToken: string; 96 tlsClientCertData: string; 97 tlsClientCertKey: string; 98 proxy: string; 99 noProxy: string; 100 forceHttpBasicAuth: boolean; 101 enableOCI: boolean; 102 insecureOCIForceHttp: boolean; 103 // write should be true if saving as a write credential. 104 write: boolean; 105 useAzureWorkloadIdentity: boolean; 106 } 107 108 interface NewGitHubAppRepoCredsParams { 109 url: string; 110 githubAppPrivateKey: string; 111 githubAppId: bigint; 112 githubAppInstallationId: bigint; 113 githubAppEnterpriseBaseURL: string; 114 tlsClientCertData: string; 115 tlsClientCertKey: string; 116 proxy: string; 117 noProxy: string; 118 // write should be true if saving as a write credential. 119 write: boolean; 120 } 121 122 interface NewGoogleCloudSourceRepoCredsParams { 123 url: string; 124 gcpServiceAccountKey: string; 125 // write should be true if saving as a write credential. 126 write: boolean; 127 } 128 129 export enum ConnectionMethod { 130 SSH = 'via SSH', 131 HTTPS = 'via HTTP/HTTPS', 132 GITHUBAPP = 'via GitHub App', 133 GOOGLECLOUD = 'via Google Cloud' 134 } 135 136 export class ReposList extends React.Component< 137 RouteComponentProps<any>, 138 { 139 connecting: boolean; 140 method: string; 141 currentRepo: models.Repository; 142 displayEditPanel: boolean; 143 authSettings: models.AuthSettings; 144 statusProperty: 'all' | 'Successful' | 'Failed' | 'Unknown'; 145 projectProperty: string; 146 typeProperty: 'all' | 'git' | 'helm'; 147 name: string; 148 } 149 > { 150 public static contextTypes = { 151 router: PropTypes.object, 152 apis: PropTypes.object, 153 history: PropTypes.object 154 }; 155 156 private formApi: FormApi; 157 private credsTemplate: boolean; 158 private repoLoader: DataLoader; 159 private credsLoader: DataLoader; 160 161 constructor(props: RouteComponentProps<any>) { 162 super(props); 163 this.state = { 164 connecting: false, 165 method: ConnectionMethod.SSH, 166 currentRepo: null, 167 displayEditPanel: false, 168 authSettings: null, 169 statusProperty: 'all', 170 projectProperty: 'all', 171 typeProperty: 'all', 172 name: '' 173 }; 174 } 175 176 public async componentDidMount() { 177 this.setState({ 178 authSettings: await services.authService.settings() 179 }); 180 } 181 182 private ConnectRepoFormButton(method: string, onSelection: (method: string) => void) { 183 return ( 184 <div className='white-box'> 185 <p>Choose your connection method:</p> 186 <DropDownMenu 187 anchor={() => ( 188 <p> 189 {method.toUpperCase()} <i className='fa fa-caret-down' /> 190 </p> 191 )} 192 items={[ConnectionMethod.SSH, ConnectionMethod.HTTPS, ConnectionMethod.GITHUBAPP, ConnectionMethod.GOOGLECLOUD].map( 193 (connectMethod: ConnectionMethod.SSH | ConnectionMethod.HTTPS | ConnectionMethod.GITHUBAPP | ConnectionMethod.GOOGLECLOUD) => ({ 194 title: connectMethod.toUpperCase(), 195 action: () => { 196 onSelection(connectMethod); 197 const formState = this.formApi.getFormState(); 198 this.formApi.setFormState({ 199 ...formState, 200 errors: {} 201 }); 202 } 203 }) 204 )} 205 /> 206 </div> 207 ); 208 } 209 210 private onChooseDefaultValues = (): FormValues => { 211 return {type: 'git', ghType: 'GitHub', write: false}; 212 }; 213 214 private onValidateErrors(params: FormValues): FormErrors { 215 switch (this.state.method) { 216 case ConnectionMethod.SSH: 217 const sshValues = params as NewSSHRepoParams; 218 return { 219 url: !sshValues.url && 'Repository URL is required' 220 }; 221 case ConnectionMethod.HTTPS: 222 const validURLValues = params as NewHTTPSRepoParams; 223 return { 224 url: 225 (!validURLValues.url && 'Repository URL is required') || 226 (this.credsTemplate && !this.isHTTPOrHTTPSUrl(validURLValues.url) && !validURLValues.enableOCI && params.type != 'oci' && 'Not a valid HTTP/HTTPS URL') || 227 (this.credsTemplate && !this.isOCIUrl(validURLValues.url) && params.type == 'oci' && 'Not a valid OCI URL'), 228 name: validURLValues.type === 'helm' && !validURLValues.name && 'Name is required', 229 username: !validURLValues.username && validURLValues.password && 'Username is required if password is given.', 230 password: !validURLValues.password && validURLValues.username && 'Password is required if username is given.', 231 tlsClientCertKey: !validURLValues.tlsClientCertKey && validURLValues.tlsClientCertData && 'TLS client cert key is required if TLS client cert is given.', 232 bearerToken: 233 (validURLValues.password && validURLValues.bearerToken && 'Either the password or the bearer token must be set, but not both.') || 234 (validURLValues.bearerToken && validURLValues.type != 'git' && 'Bearer token is only supported for Git BitBucket Data Center repositories.') 235 }; 236 case ConnectionMethod.GITHUBAPP: 237 const githubAppValues = params as NewGitHubAppRepoParams; 238 return { 239 url: 240 (!githubAppValues.url && 'Repository URL is required') || 241 (this.credsTemplate && !this.isHTTPOrHTTPSUrl(githubAppValues.url) && 'Not a valid HTTP/HTTPS URL'), 242 githubAppId: !githubAppValues.githubAppId && 'GitHub App ID is required', 243 githubAppInstallationId: !githubAppValues.githubAppInstallationId && 'GitHub App installation ID is required', 244 githubAppPrivateKey: !githubAppValues.githubAppPrivateKey && 'GitHub App private Key is required' 245 }; 246 case ConnectionMethod.GOOGLECLOUD: 247 const googleCloudValues = params as NewGoogleCloudSourceRepoParams; 248 return { 249 url: 250 (!googleCloudValues.url && 'Repo URL is required') || (this.credsTemplate && !this.isHTTPOrHTTPSUrl(googleCloudValues.url) && 'Not a valid HTTP/HTTPS URL'), 251 gcpServiceAccountKey: !googleCloudValues.gcpServiceAccountKey && 'GCP service account key is required' 252 }; 253 } 254 } 255 256 private SlidingPanelHeader() { 257 return ( 258 <> 259 {this.showConnectRepo && ( 260 <> 261 <button 262 className='argo-button argo-button--base' 263 onClick={() => { 264 this.credsTemplate = false; 265 this.formApi.submitForm(null); 266 }}> 267 <Spinner show={this.state.connecting} style={{marginRight: '5px'}} /> 268 Connect 269 </button>{' '} 270 <button 271 className='argo-button argo-button--base' 272 onClick={() => { 273 this.credsTemplate = true; 274 this.formApi.submitForm(null); 275 }}> 276 Save as credentials template 277 </button>{' '} 278 <button onClick={() => (this.showConnectRepo = false)} className='argo-button argo-button--base-o'> 279 Cancel 280 </button> 281 </> 282 )} 283 {this.state.displayEditPanel && ( 284 <button onClick={() => this.setState({displayEditPanel: false})} className='argo-button argo-button--base-o'> 285 Cancel 286 </button> 287 )} 288 </> 289 ); 290 } 291 292 private onSubmitForm() { 293 switch (this.state.method) { 294 case ConnectionMethod.SSH: 295 return (params: FormValues) => this.connectSSHRepo(params as NewSSHRepoParams); 296 case ConnectionMethod.HTTPS: 297 return (params: FormValues) => { 298 params.url = params.enableOCI && params.type != 'oci' ? this.stripProtocol(params.url) : params.url; 299 return this.connectHTTPSRepo(params as NewHTTPSRepoParams); 300 }; 301 case ConnectionMethod.GITHUBAPP: 302 return (params: FormValues) => this.connectGitHubAppRepo(params as NewGitHubAppRepoParams); 303 case ConnectionMethod.GOOGLECLOUD: 304 return (params: FormValues) => this.connectGoogleCloudSourceRepo(params as NewGoogleCloudSourceRepoParams); 305 } 306 } 307 308 public render() { 309 return ( 310 <Page 311 title='Repositories' 312 toolbar={{ 313 breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Repositories'}], 314 actionMenu: { 315 items: [ 316 { 317 iconClassName: 'fa fa-plus', 318 title: 'Connect Repo', 319 action: () => (this.showConnectRepo = true) 320 }, 321 { 322 iconClassName: 'fa fa-redo', 323 title: 'Refresh list', 324 action: () => { 325 this.refreshRepoList(); 326 } 327 } 328 ] 329 } 330 }}> 331 <div className='repos-list'> 332 <div className='argo-container'> 333 <div style={{display: 'flex', margin: '20px 0', justifyContent: 'space-between'}}> 334 <div style={{display: 'flex', gap: '8px', width: '50%'}}> 335 <DropDownMenu 336 items={[ 337 { 338 title: 'all', 339 action: () => this.setState({typeProperty: 'all'}) 340 }, 341 { 342 title: 'git', 343 action: () => this.setState({typeProperty: 'git'}) 344 }, 345 { 346 title: 'helm', 347 action: () => this.setState({typeProperty: 'helm'}) 348 } 349 ]} 350 anchor={() => ( 351 <> 352 <a style={{whiteSpace: 'nowrap'}}> 353 Type: {this.state.typeProperty} <i className='fa fa-caret-down' /> 354 </a> 355 356 </> 357 )} 358 qeId='type-menu' 359 /> 360 <DataLoader load={services.repos.list} ref={loader => (this.repoLoader = loader)}> 361 {(repos: models.Repository[]) => { 362 const projectValues = Array.from(new Set(repos.map(repo => repo.project))); 363 364 const projectItems = [ 365 { 366 title: 'all', 367 action: () => this.setState({projectProperty: 'all'}) 368 }, 369 ...projectValues 370 .filter(project => project && project.trim() !== '') 371 .map(project => ({ 372 title: project, 373 action: () => this.setState({projectProperty: project}) 374 })) 375 ]; 376 377 return ( 378 <DropDownMenu 379 items={projectItems} 380 anchor={() => ( 381 <> 382 <a style={{whiteSpace: 'nowrap'}}> 383 Project: {this.state.projectProperty} <i className='fa fa-caret-down' /> 384 </a> 385 386 </> 387 )} 388 qeId='project-menu' 389 /> 390 ); 391 }} 392 </DataLoader> 393 <DropDownMenu 394 items={[ 395 { 396 title: 'all', 397 action: () => this.setState({statusProperty: 'all'}) 398 }, 399 { 400 title: 'Successful', 401 action: () => this.setState({statusProperty: 'Successful'}) 402 }, 403 { 404 title: 'Failed', 405 action: () => this.setState({statusProperty: 'Failed'}) 406 }, 407 { 408 title: 'Unknown', 409 action: () => this.setState({statusProperty: 'Unknown'}) 410 } 411 ]} 412 anchor={() => ( 413 <> 414 <a style={{whiteSpace: 'nowrap'}}> 415 Status: {this.state.statusProperty} <i className='fa fa-caret-down' /> 416 </a> 417 418 </> 419 )} 420 qeId='status-menu' 421 /> 422 </div> 423 <div className='search-bar' style={{display: 'flex', alignItems: 'flex-end', width: '100%'}}></div> 424 <input type='text' className='argo-field' placeholder='Search Name' value={this.state.name} onChange={e => this.setState({name: e.target.value})} /> 425 </div> 426 <DataLoader load={services.repos.list} ref={loader => (this.repoLoader = loader)}> 427 {(repos: models.Repository[]) => { 428 const filteredRepos = this.filteredRepos(repos, this.state.typeProperty, this.state.projectProperty, this.state.statusProperty, this.state.name); 429 430 return ( 431 (filteredRepos.length > 0 && ( 432 <div className='argo-table-list'> 433 <div className='argo-table-list__head'> 434 <div className='row'> 435 <div className='columns small-1' /> 436 <div className='columns small-1'>TYPE</div> 437 <div className='columns small-2'>NAME</div> 438 <div className='columns small-2'>PROJECT</div> 439 <div className='columns small-4'>REPOSITORY</div> 440 <div className='columns small-2'>CONNECTION STATUS</div> 441 </div> 442 </div> 443 {filteredRepos.map(repo => ( 444 <div 445 className={`argo-table-list__row ${this.isRepoUpdatable(repo) ? 'item-clickable' : ''}`} 446 key={repo.repo} 447 onClick={() => (this.isRepoUpdatable(repo) ? this.displayEditSliding(repo) : null)}> 448 <div className='row'> 449 <div className='columns small-1'> 450 <i className={'icon argo-icon-' + (repo.type || 'git')} /> 451 </div> 452 <div className='columns small-1'> 453 <span>{repo.type || 'git'}</span> 454 {repo.enableOCI && <span> OCI</span>} 455 </div> 456 <div className='columns small-2'> 457 <Tooltip content={repo.name}> 458 <span>{repo.name}</span> 459 </Tooltip> 460 </div> 461 <div className='columns small-2'> 462 <Tooltip content={repo.project}> 463 <span>{repo.project}</span> 464 </Tooltip> 465 </div> 466 <div className='columns small-4'> 467 <Tooltip content={repo.repo}> 468 <span> 469 <Repo url={repo.repo} /> 470 </span> 471 </Tooltip> 472 </div> 473 <div className='columns small-2'> 474 <ConnectionStateIcon state={repo.connectionState} /> {repo.connectionState.status} 475 <DropDownMenu 476 anchor={() => ( 477 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 478 <i className='fa fa-ellipsis-v' /> 479 </button> 480 )} 481 items={[ 482 { 483 title: 'Create application', 484 action: () => 485 this.appContext.apis.navigation.goto('/applications', { 486 new: JSON.stringify({spec: {source: {repoURL: repo.repo}}}) 487 }) 488 }, 489 { 490 title: 'Disconnect', 491 action: () => this.disconnectRepo(repo.repo, repo.project, false) 492 } 493 ]} 494 /> 495 </div> 496 </div> 497 </div> 498 ))} 499 </div> 500 )) || ( 501 <EmptyState icon='argo-icon-git'> 502 <h4>No repositories connected</h4> 503 <h5>Connect your repo to deploy apps.</h5> 504 </EmptyState> 505 ) 506 ); 507 }} 508 </DataLoader> 509 </div> 510 <div className='argo-container'> 511 <DataLoader load={() => services.repocreds.list()} ref={loader => (this.credsLoader = loader)}> 512 {(creds: models.RepoCreds[]) => 513 creds.length > 0 && ( 514 <div className='argo-table-list'> 515 <div className='argo-table-list__head'> 516 <div className='row'> 517 <div className='columns small-9'>CREDENTIALS TEMPLATE URL</div> 518 <div className='columns small-3'>CREDS</div> 519 </div> 520 </div> 521 {creds.map(repo => ( 522 <div className='argo-table-list__row' key={repo.url}> 523 <div className='row'> 524 <div className='columns small-9'> 525 <i className='icon argo-icon-git' /> <Repo url={repo.url} /> 526 </div> 527 <div className='columns small-3'> 528 - 529 <DropDownMenu 530 anchor={() => ( 531 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 532 <i className='fa fa-ellipsis-v' /> 533 </button> 534 )} 535 items={[ 536 { 537 title: 'Remove', 538 action: () => this.removeRepoCreds(repo.url, false) 539 } 540 ]} 541 /> 542 </div> 543 </div> 544 </div> 545 ))} 546 </div> 547 ) 548 } 549 </DataLoader> 550 </div> 551 {this.state.authSettings?.hydratorEnabled && ( 552 <div className='argo-container'> 553 <DataLoader load={() => services.repos.listWrite()} ref={loader => (this.repoLoader = loader)}> 554 {(repos: models.Repository[]) => 555 (repos.length > 0 && ( 556 <div className='argo-table-list'> 557 <div className='argo-table-list__head'> 558 <div className='row'> 559 <div className='columns small-1' /> 560 <div className='columns small-1'>TYPE</div> 561 <div className='columns small-2'>NAME</div> 562 <div className='columns small-2'>PROJECT</div> 563 <div className='columns small-4'>REPOSITORY</div> 564 <div className='columns small-2'>CONNECTION STATUS</div> 565 </div> 566 </div> 567 {repos.map(repo => ( 568 <div 569 className={`argo-table-list__row ${this.isRepoUpdatable(repo) ? 'item-clickable' : ''}`} 570 key={repo.repo} 571 onClick={() => (this.isRepoUpdatable(repo) ? this.displayEditSliding(repo) : null)}> 572 <div className='row'> 573 <div className='columns small-1'> 574 <i className='icon argo-icon-git' /> 575 </div> 576 <div className='columns small-1'>write</div> 577 <div className='columns small-2'> 578 <Tooltip content={repo.name}> 579 <span>{repo.name}</span> 580 </Tooltip> 581 </div> 582 <div className='columns small-2'> 583 <Tooltip content={repo.project}> 584 <span>{repo.project}</span> 585 </Tooltip> 586 </div> 587 <div className='columns small-4'> 588 <Tooltip content={repo.repo}> 589 <span> 590 <Repo url={repo.repo} /> 591 </span> 592 </Tooltip> 593 </div> 594 <div className='columns small-2'> 595 <ConnectionStateIcon state={repo.connectionState} /> {repo.connectionState.status} 596 <DropDownMenu 597 anchor={() => ( 598 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 599 <i className='fa fa-ellipsis-v' /> 600 </button> 601 )} 602 items={[ 603 { 604 title: 'Create application', 605 action: () => 606 this.appContext.apis.navigation.goto('/applications', { 607 new: JSON.stringify({spec: {sourceHydrator: {drySource: {repoURL: repo.repo}}}}) 608 }) 609 }, 610 { 611 title: 'Disconnect', 612 action: () => this.disconnectRepo(repo.repo, repo.project, true) 613 } 614 ]} 615 /> 616 </div> 617 </div> 618 </div> 619 ))} 620 </div> 621 )) || ( 622 <EmptyState icon='argo-icon-git'> 623 <h4>No repositories connected</h4> 624 <h5>Connect your repo to deploy apps.</h5> 625 </EmptyState> 626 ) 627 } 628 </DataLoader> 629 </div> 630 )} 631 {this.state.authSettings?.hydratorEnabled && ( 632 <div className='argo-container'> 633 <DataLoader load={() => services.repocreds.listWrite()} ref={loader => (this.credsLoader = loader)}> 634 {(creds: models.RepoCreds[]) => 635 creds.length > 0 && ( 636 <div className='argo-table-list'> 637 <div className='argo-table-list__head'> 638 <div className='row'> 639 <div className='columns small-9'>CREDENTIALS TEMPLATE URL</div> 640 <div className='columns small-3'>CREDS</div> 641 </div> 642 </div> 643 {creds.map(repo => ( 644 <div className='argo-table-list__row' key={repo.url}> 645 <div className='row'> 646 <div className='columns small-9'> 647 <i className='icon argo-icon-git' /> <Repo url={repo.url} /> 648 </div> 649 <div className='columns small-3'> 650 - 651 <DropDownMenu 652 anchor={() => ( 653 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 654 <i className='fa fa-ellipsis-v' /> 655 </button> 656 )} 657 items={[ 658 { 659 title: 'Remove', 660 action: () => this.removeRepoCreds(repo.url, true) 661 } 662 ]} 663 /> 664 </div> 665 </div> 666 </div> 667 ))} 668 </div> 669 ) 670 } 671 </DataLoader> 672 </div> 673 )} 674 </div> 675 <SlidingPanel 676 isShown={this.showConnectRepo || this.state.displayEditPanel} 677 onClose={() => { 678 if (!this.state.displayEditPanel && this.showConnectRepo) { 679 this.showConnectRepo = false; 680 } 681 if (this.state.displayEditPanel) { 682 this.setState({displayEditPanel: false}); 683 } 684 }} 685 header={this.SlidingPanelHeader()}> 686 {this.showConnectRepo && 687 this.ConnectRepoFormButton(this.state.method, method => { 688 this.setState({method}); 689 })} 690 {this.state.displayEditPanel && <RepoDetails repo={this.state.currentRepo} save={(params: NewHTTPSRepoParams) => this.updateHTTPSRepo(params)} />} 691 {!this.state.displayEditPanel && ( 692 <DataLoader load={() => services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort())}> 693 {projects => ( 694 <Form 695 onSubmit={this.onSubmitForm()} 696 getApi={api => (this.formApi = api)} 697 defaultValues={this.onChooseDefaultValues()} 698 validateError={(values: FormValues) => this.onValidateErrors(values)}> 699 {formApi => ( 700 <form onSubmit={formApi.submitForm} role='form' className='repos-list width-control'> 701 {this.state.authSettings?.hydratorEnabled && ( 702 <div className='white-box'> 703 <p>SAVE AS WRITE CREDENTIAL (ALPHA)</p> 704 <p> 705 The Source Hydrator is an Alpha feature which enables Applications to push hydrated manifests to git before syncing. To use 706 the Source Hydrator for a repository, you must save two credentials: a read credential for pulling manifests and a write 707 credential for pushing hydrated manifests. If you add a write credential for a repository, then{' '} 708 <strong>any Application that can sync from the repo can also push hydrated manifests to that repo.</strong> Do not use this 709 feature until you've read its documentation and understand the security implications. 710 </p> 711 <div className='argo-form-row'> 712 <FormField formApi={formApi} label='Save as write credential' field='write' component={CheckboxField} /> 713 </div> 714 </div> 715 )} 716 {this.state.method === ConnectionMethod.SSH && ( 717 <div className='white-box'> 718 <p>CONNECT REPO USING SSH</p> 719 {formApi.getFormState().values.write === false && ( 720 <div className='argo-form-row'> 721 <FormField formApi={formApi} label='Name (mandatory for Helm)' field='name' component={Text} /> 722 </div> 723 )} 724 {formApi.getFormState().values.write === false && ( 725 <div className='argo-form-row'> 726 <FormField 727 formApi={formApi} 728 label='Project' 729 field='project' 730 component={AutocompleteField} 731 componentProps={{items: projects}} 732 /> 733 </div> 734 )} 735 <div className='argo-form-row'> 736 <FormField formApi={formApi} label='Repository URL' field='url' component={Text} /> 737 </div> 738 <div className='argo-form-row'> 739 <FormField formApi={formApi} label='SSH private key data' field='sshPrivateKey' component={TextArea} /> 740 </div> 741 <div className='argo-form-row'> 742 <FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} /> 743 <HelpIcon title='This setting is ignored when creating as credential template.' /> 744 </div> 745 {formApi.getFormState().values.write === false && ( 746 <div className='argo-form-row'> 747 <FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} /> 748 <HelpIcon title='This setting is ignored when creating as credential template.' /> 749 </div> 750 )} 751 <div className='argo-form-row'> 752 <FormField formApi={formApi} label='Proxy (optional)' field='proxy' component={Text} /> 753 </div> 754 <div className='argo-form-row'> 755 <FormField formApi={formApi} label='NoProxy (optional)' field='noProxy' component={Text} /> 756 </div> 757 </div> 758 )} 759 {this.state.method === ConnectionMethod.HTTPS && ( 760 <div className='white-box'> 761 <p>CONNECT REPO USING HTTP/HTTPS</p> 762 <div className='argo-form-row'> 763 <FormField 764 formApi={formApi} 765 label='Type' 766 field='type' 767 component={FormSelect} 768 componentProps={{options: ['git', 'helm', 'oci']}} 769 /> 770 </div> 771 {(formApi.getFormState().values.type === 'helm' || formApi.getFormState().values.type === 'git') && ( 772 <div className='argo-form-row'> 773 <FormField 774 formApi={formApi} 775 label={`Name ${formApi.getFormState().values.type === 'git' ? '(optional)' : ''}`} 776 field='name' 777 component={Text} 778 /> 779 </div> 780 )} 781 {formApi.getFormState().values.write === false && ( 782 <div className='argo-form-row'> 783 <FormField 784 formApi={formApi} 785 label='Project' 786 field='project' 787 component={AutocompleteField} 788 componentProps={{items: projects}} 789 /> 790 </div> 791 )} 792 <div className='argo-form-row'> 793 <FormField formApi={formApi} label='Repository URL' field='url' component={Text} /> 794 </div> 795 <div className='argo-form-row'> 796 <FormField formApi={formApi} label='Username (optional)' field='username' component={Text} /> 797 </div> 798 <div className='argo-form-row'> 799 <FormField 800 formApi={formApi} 801 label='Password (optional)' 802 field='password' 803 component={Text} 804 componentProps={{type: 'password'}} 805 /> 806 </div> 807 {formApi.getFormState().values.type === 'git' && ( 808 <div className='argo-form-row'> 809 <FormField 810 formApi={formApi} 811 label='Bearer token (optional, for BitBucket Data Center only)' 812 field='bearerToken' 813 component={Text} 814 componentProps={{type: 'password'}} 815 /> 816 </div> 817 )} 818 <div className='argo-form-row'> 819 <FormField formApi={formApi} label='TLS client certificate (optional)' field='tlsClientCertData' component={TextArea} /> 820 </div> 821 <div className='argo-form-row'> 822 <FormField formApi={formApi} label='TLS client certificate key (optional)' field='tlsClientCertKey' component={TextArea} /> 823 </div> 824 <div className='argo-form-row'> 825 <FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} /> 826 <HelpIcon title='This setting is ignored when creating as credential template.' /> 827 </div> 828 {formApi.getFormState().values.type === 'git' && ( 829 <React.Fragment> 830 <div className='argo-form-row'> 831 <FormField formApi={formApi} label='Force HTTP basic auth' field='forceHttpBasicAuth' component={CheckboxField} /> 832 </div> 833 <div className='argo-form-row'> 834 <FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} /> 835 <HelpIcon title='This setting is ignored when creating as credential template.' /> 836 </div> 837 </React.Fragment> 838 )} 839 <div className='argo-form-row'> 840 <FormField formApi={formApi} label='Proxy (optional)' field='proxy' component={Text} /> 841 </div> 842 <div className='argo-form-row'> 843 <FormField formApi={formApi} label='NoProxy (optional)' field='noProxy' component={Text} /> 844 </div> 845 <div className='argo-form-row'> 846 {formApi.getFormState().values.type !== 'oci' ? ( 847 <FormField formApi={formApi} label='Enable OCI' field='enableOCI' component={CheckboxField} /> 848 ) : ( 849 <FormField formApi={formApi} label='Insecure HTTP Only' field='insecureOCIForceHttp' component={CheckboxField} /> 850 )} 851 </div> 852 <div className='argo-form-row'> 853 <FormField 854 formApi={formApi} 855 label='Use Azure Workload Identity' 856 field='useAzureWorkloadIdentity' 857 component={CheckboxField} 858 /> 859 </div> 860 </div> 861 )} 862 {this.state.method === ConnectionMethod.GITHUBAPP && ( 863 <div className='white-box'> 864 <p>CONNECT REPO USING GITHUB APP</p> 865 <div className='argo-form-row'> 866 <FormField 867 formApi={formApi} 868 label='Type' 869 field='ghType' 870 component={FormSelect} 871 componentProps={{options: ['GitHub', 'GitHub Enterprise']}} 872 /> 873 </div> 874 {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( 875 <div className='argo-form-row'> 876 <FormField 877 formApi={formApi} 878 label='GitHub Enterprise Base URL (e.g. https://ghe.example.com/api/v3)' 879 field='githubAppEnterpriseBaseURL' 880 component={Text} 881 /> 882 </div> 883 )} 884 <div className='argo-form-row'> 885 <FormField 886 formApi={formApi} 887 label='Project' 888 field='project' 889 component={AutocompleteField} 890 componentProps={{items: projects}} 891 /> 892 </div> 893 <div className='argo-form-row'> 894 <FormField formApi={formApi} label='Repository URL' field='url' component={Text} /> 895 </div> 896 <div className='argo-form-row'> 897 <FormField formApi={formApi} label='GitHub App ID' field='githubAppId' component={NumberField} /> 898 </div> 899 <div className='argo-form-row'> 900 <FormField formApi={formApi} label='GitHub App Installation ID' field='githubAppInstallationId' component={NumberField} /> 901 </div> 902 <div className='argo-form-row'> 903 <FormField formApi={formApi} label='GitHub App private key' field='githubAppPrivateKey' component={TextArea} /> 904 </div> 905 <div className='argo-form-row'> 906 <FormField formApi={formApi} label='Skip server verification' field='insecure' component={CheckboxField} /> 907 <HelpIcon title='This setting is ignored when creating as credential template.' /> 908 </div> 909 <div className='argo-form-row'> 910 <FormField formApi={formApi} label='Enable LFS support (Git only)' field='enableLfs' component={CheckboxField} /> 911 <HelpIcon title='This setting is ignored when creating as credential template.' /> 912 </div> 913 {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( 914 <React.Fragment> 915 <div className='argo-form-row'> 916 <FormField 917 formApi={formApi} 918 label='TLS client certificate (optional)' 919 field='tlsClientCertData' 920 component={TextArea} 921 /> 922 </div> 923 <div className='argo-form-row'> 924 <FormField 925 formApi={formApi} 926 label='TLS client certificate key (optional)' 927 field='tlsClientCertKey' 928 component={TextArea} 929 /> 930 </div> 931 </React.Fragment> 932 )} 933 <div className='argo-form-row'> 934 <FormField formApi={formApi} label='Proxy (optional)' field='proxy' component={Text} /> 935 </div> 936 <div className='argo-form-row'> 937 <FormField formApi={formApi} label='NoProxy (optional)' field='noProxy' component={Text} /> 938 </div> 939 </div> 940 )} 941 {this.state.method === ConnectionMethod.GOOGLECLOUD && ( 942 <div className='white-box'> 943 <p>CONNECT REPO USING GOOGLE CLOUD</p> 944 <div className='argo-form-row'> 945 <FormField 946 formApi={formApi} 947 label='Project' 948 field='project' 949 component={AutocompleteField} 950 componentProps={{items: projects}} 951 /> 952 </div> 953 <div className='argo-form-row'> 954 <FormField formApi={formApi} label='Repository URL' field='url' component={Text} /> 955 </div> 956 <div className='argo-form-row'> 957 <FormField formApi={formApi} label='GCP service account key' field='gcpServiceAccountKey' component={TextArea} /> 958 </div> 959 <div className='argo-form-row'> 960 <FormField formApi={formApi} label='Proxy (optional)' field='proxy' component={Text} /> 961 </div> 962 <div className='argo-form-row'> 963 <FormField formApi={formApi} label='NoProxy (optional)' field='noProxy' component={Text} /> 964 </div> 965 </div> 966 )} 967 </form> 968 )} 969 </Form> 970 )} 971 </DataLoader> 972 )} 973 </SlidingPanel> 974 </Page> 975 ); 976 } 977 978 private displayEditSliding(repo: models.Repository) { 979 this.setState({currentRepo: repo}); 980 this.setState({displayEditPanel: true}); 981 } 982 983 // Whether url is a http or https url 984 private isHTTPOrHTTPSUrl(url: string) { 985 if (url.match(/^https?:\/\/.*$/gi)) { 986 return true; 987 } else { 988 return false; 989 } 990 } 991 992 // Whether url is an oci url (simple version) 993 private isOCIUrl(url: string) { 994 if (url.match(/^oci:\/\/.*$/gi)) { 995 return true; 996 } else { 997 return false; 998 } 999 } 1000 1001 private stripProtocol(url: string) { 1002 return url.replace('https://', '').replace('oci://', ''); 1003 } 1004 1005 // only connections of git type which is not via GitHub App are updatable 1006 private isRepoUpdatable(repo: models.Repository) { 1007 return this.isHTTPOrHTTPSUrl(repo.repo) && repo.type === 'git' && !repo.githubAppId; 1008 } 1009 1010 // Forces a reload of configured repositories, circumventing the cache 1011 private async refreshRepoList(updatedRepo?: string) { 1012 // Refresh the credentials template list 1013 this.credsLoader.reload(); 1014 1015 try { 1016 await services.repos.listNoCache(); 1017 this.repoLoader.reload(); 1018 this.appContext.apis.notifications.show({ 1019 content: updatedRepo ? `Successfully updated ${updatedRepo} repository` : 'Successfully reloaded list of repositories', 1020 type: NotificationType.Success 1021 }); 1022 } catch (e) { 1023 this.appContext.apis.notifications.show({ 1024 content: <ErrorNotification title='Could not refresh list of repositories' e={e} />, 1025 type: NotificationType.Error 1026 }); 1027 } 1028 } 1029 1030 // Empty all fields in connect repository form 1031 private clearConnectRepoForm() { 1032 this.credsTemplate = false; 1033 this.formApi.resetAll(); 1034 } 1035 1036 // Connect a new repository or create a repository credentials for SSH repositories 1037 private async connectSSHRepo(params: NewSSHRepoParams) { 1038 if (this.credsTemplate) { 1039 this.createSSHCreds({url: params.url, sshPrivateKey: params.sshPrivateKey, write: params.write}); 1040 } else { 1041 this.setState({connecting: true}); 1042 try { 1043 if (params.write) { 1044 await services.repos.createSSHWrite(params); 1045 } else { 1046 await services.repos.createSSH(params); 1047 } 1048 this.repoLoader.reload(); 1049 this.showConnectRepo = false; 1050 } catch (e) { 1051 this.appContext.apis.notifications.show({ 1052 content: <ErrorNotification title='Unable to connect SSH repository' e={e} />, 1053 type: NotificationType.Error 1054 }); 1055 } finally { 1056 this.setState({connecting: false}); 1057 } 1058 } 1059 } 1060 1061 // Connect a new repository or create a repository credentials for HTTPS repositories 1062 private async connectHTTPSRepo(params: NewHTTPSRepoParams) { 1063 if (this.credsTemplate) { 1064 await this.createHTTPSCreds({ 1065 type: params.type, 1066 url: params.url, 1067 username: params.username, 1068 password: params.password, 1069 bearerToken: params.bearerToken, 1070 tlsClientCertData: params.tlsClientCertData, 1071 tlsClientCertKey: params.tlsClientCertKey, 1072 proxy: params.proxy, 1073 noProxy: params.noProxy, 1074 forceHttpBasicAuth: params.forceHttpBasicAuth, 1075 enableOCI: params.enableOCI, 1076 write: params.write, 1077 useAzureWorkloadIdentity: params.useAzureWorkloadIdentity, 1078 insecureOCIForceHttp: params.insecureOCIForceHttp 1079 }); 1080 } else { 1081 this.setState({connecting: true}); 1082 try { 1083 if (params.write) { 1084 await services.repos.createHTTPSWrite(params); 1085 } else { 1086 await services.repos.createHTTPS(params); 1087 } 1088 this.repoLoader.reload(); 1089 this.showConnectRepo = false; 1090 } catch (e) { 1091 this.appContext.apis.notifications.show({ 1092 content: <ErrorNotification title='Unable to connect HTTPS repository' e={e} />, 1093 type: NotificationType.Error 1094 }); 1095 } finally { 1096 this.setState({connecting: false}); 1097 } 1098 } 1099 } 1100 1101 // Update an existing repository for HTTPS repositories 1102 private async updateHTTPSRepo(params: NewHTTPSRepoParams) { 1103 try { 1104 if (params.write) { 1105 await services.repos.updateHTTPSWrite(params); 1106 } else { 1107 await services.repos.updateHTTPS(params); 1108 } 1109 this.repoLoader.reload(); 1110 this.setState({displayEditPanel: false}); 1111 this.refreshRepoList(params.url); 1112 } catch (e) { 1113 this.appContext.apis.notifications.show({ 1114 content: <ErrorNotification title='Unable to update HTTPS repository' e={e} />, 1115 type: NotificationType.Error 1116 }); 1117 } finally { 1118 this.setState({connecting: false}); 1119 } 1120 } 1121 1122 // Connect a new repository or create a repository credentials for GitHub App repositories 1123 private async connectGitHubAppRepo(params: NewGitHubAppRepoParams) { 1124 if (this.credsTemplate) { 1125 this.createGitHubAppCreds({ 1126 url: params.url, 1127 githubAppPrivateKey: params.githubAppPrivateKey, 1128 githubAppId: params.githubAppId, 1129 githubAppInstallationId: params.githubAppInstallationId, 1130 githubAppEnterpriseBaseURL: params.githubAppEnterpriseBaseURL, 1131 tlsClientCertData: params.tlsClientCertData, 1132 tlsClientCertKey: params.tlsClientCertKey, 1133 proxy: params.proxy, 1134 noProxy: params.noProxy, 1135 write: params.write 1136 }); 1137 } else { 1138 this.setState({connecting: true}); 1139 try { 1140 if (params.write) { 1141 await services.repos.createGitHubAppWrite(params); 1142 } else { 1143 await services.repos.createGitHubApp(params); 1144 } 1145 this.repoLoader.reload(); 1146 this.showConnectRepo = false; 1147 } catch (e) { 1148 this.appContext.apis.notifications.show({ 1149 content: <ErrorNotification title='Unable to connect GitHub App repository' e={e} />, 1150 type: NotificationType.Error 1151 }); 1152 } finally { 1153 this.setState({connecting: false}); 1154 } 1155 } 1156 } 1157 1158 // Connect a new repository or create a repository credentials for GitHub App repositories 1159 private async connectGoogleCloudSourceRepo(params: NewGoogleCloudSourceRepoParams) { 1160 if (this.credsTemplate) { 1161 this.createGoogleCloudSourceCreds({ 1162 url: params.url, 1163 gcpServiceAccountKey: params.gcpServiceAccountKey, 1164 write: params.write 1165 }); 1166 } else { 1167 this.setState({connecting: true}); 1168 try { 1169 if (params.write) { 1170 await services.repos.createGoogleCloudSourceWrite(params); 1171 } else { 1172 await services.repos.createGoogleCloudSource(params); 1173 } 1174 this.repoLoader.reload(); 1175 this.showConnectRepo = false; 1176 } catch (e) { 1177 this.appContext.apis.notifications.show({ 1178 content: <ErrorNotification title='Unable to connect Google Cloud Source repository' e={e} />, 1179 type: NotificationType.Error 1180 }); 1181 } finally { 1182 this.setState({connecting: false}); 1183 } 1184 } 1185 } 1186 1187 private async createHTTPSCreds(params: NewHTTPSRepoCredsParams) { 1188 try { 1189 if (params.write) { 1190 await services.repocreds.createHTTPSWrite(params); 1191 } else { 1192 await services.repocreds.createHTTPS(params); 1193 } 1194 this.credsLoader.reload(); 1195 this.showConnectRepo = false; 1196 } catch (e) { 1197 this.appContext.apis.notifications.show({ 1198 content: <ErrorNotification title='Unable to create HTTPS credentials' e={e} />, 1199 type: NotificationType.Error 1200 }); 1201 } 1202 } 1203 1204 private async createSSHCreds(params: NewSSHRepoCredsParams) { 1205 try { 1206 if (params.write) { 1207 await services.repocreds.createSSHWrite(params); 1208 } else { 1209 await services.repocreds.createSSH(params); 1210 } 1211 this.credsLoader.reload(); 1212 this.showConnectRepo = false; 1213 } catch (e) { 1214 this.appContext.apis.notifications.show({ 1215 content: <ErrorNotification title='Unable to create SSH credentials' e={e} />, 1216 type: NotificationType.Error 1217 }); 1218 } 1219 } 1220 1221 private async createGitHubAppCreds(params: NewGitHubAppRepoCredsParams) { 1222 try { 1223 if (params.write) { 1224 await services.repocreds.createGitHubAppWrite(params); 1225 } else { 1226 await services.repocreds.createGitHubApp(params); 1227 } 1228 this.credsLoader.reload(); 1229 this.showConnectRepo = false; 1230 } catch (e) { 1231 this.appContext.apis.notifications.show({ 1232 content: <ErrorNotification title='Unable to create GitHub App credentials' e={e} />, 1233 type: NotificationType.Error 1234 }); 1235 } 1236 } 1237 1238 private async createGoogleCloudSourceCreds(params: NewGoogleCloudSourceRepoCredsParams) { 1239 try { 1240 if (params.write) { 1241 await services.repocreds.createGoogleCloudSourceWrite(params); 1242 } else { 1243 await services.repocreds.createGoogleCloudSource(params); 1244 } 1245 this.credsLoader.reload(); 1246 this.showConnectRepo = false; 1247 } catch (e) { 1248 this.appContext.apis.notifications.show({ 1249 content: <ErrorNotification title='Unable to create Google Cloud Source credentials' e={e} />, 1250 type: NotificationType.Error 1251 }); 1252 } 1253 } 1254 1255 // Remove a repository from the configuration 1256 private async disconnectRepo(repo: string, project: string, write: boolean) { 1257 const confirmed = await this.appContext.apis.popup.confirm('Disconnect repository', `Are you sure you want to disconnect '${repo}'?`); 1258 if (confirmed) { 1259 try { 1260 if (write) { 1261 await services.repos.deleteWrite(repo, project || ''); 1262 } else { 1263 await services.repos.delete(repo, project || ''); 1264 } 1265 this.repoLoader.reload(); 1266 } catch (e) { 1267 this.appContext.apis.notifications.show({ 1268 content: <ErrorNotification title='Unable to disconnect repository' e={e} />, 1269 type: NotificationType.Error 1270 }); 1271 } 1272 } 1273 } 1274 1275 // Remove repository credentials from the configuration 1276 private async removeRepoCreds(url: string, write: boolean) { 1277 const confirmed = await this.appContext.apis.popup.confirm('Remove repository credentials', `Are you sure you want to remove credentials for URL prefix '${url}'?`); 1278 if (confirmed) { 1279 try { 1280 if (write) { 1281 await services.repocreds.deleteWrite(url); 1282 } else { 1283 await services.repocreds.delete(url); 1284 } 1285 this.credsLoader.reload(); 1286 } catch (e) { 1287 this.appContext.apis.notifications.show({ 1288 content: <ErrorNotification title='Unable to remove repository credentials' e={e} />, 1289 type: NotificationType.Error 1290 }); 1291 } 1292 } 1293 } 1294 1295 // filtering function 1296 private filteredRepos(repos: models.Repository[], type: string, project: string, status: string, name: string) { 1297 let newRepos = repos; 1298 1299 if (name && name.trim() !== '') { 1300 const response = this.filteredName(newRepos, name); 1301 newRepos = response; 1302 } 1303 1304 if (type !== 'all') { 1305 const response = this.filteredType(newRepos, type); 1306 newRepos = response; 1307 } 1308 1309 if (status !== 'all') { 1310 const response = this.filteredStatus(newRepos, status); 1311 newRepos = response; 1312 } 1313 1314 if (project !== 'all') { 1315 const response = this.filteredProject(newRepos, project); 1316 newRepos = response; 1317 } 1318 1319 return newRepos; 1320 } 1321 1322 private filteredName(repos: models.Repository[], name: string) { 1323 const trimmedName = name.trim(); 1324 if (trimmedName === '') { 1325 return repos; 1326 } 1327 const newRepos = repos.filter( 1328 repo => (repo.name && repo.name.toLowerCase().includes(trimmedName.toLowerCase())) || repo.repo.toLowerCase().includes(trimmedName.toLowerCase()) 1329 ); 1330 return newRepos; 1331 } 1332 1333 private filteredStatus(repos: models.Repository[], status: string) { 1334 const newRepos = repos.filter(repo => repo.connectionState.status.includes(status)); 1335 return newRepos; 1336 } 1337 1338 private filteredProject(repos: models.Repository[], project: string) { 1339 const newRepos = repos.filter(repo => repo.project && repo.project.includes(project)); 1340 return newRepos; 1341 } 1342 1343 private filteredType(repos: models.Repository[], type: string) { 1344 const newRepos = repos.filter(repo => repo.type.includes(type)); 1345 return newRepos; 1346 } 1347 1348 // Whether to show the new repository connection dialogue on the page 1349 private get showConnectRepo() { 1350 return new URLSearchParams(this.props.location.search).get('addRepo') === 'true'; 1351 } 1352 1353 private set showConnectRepo(val: boolean) { 1354 this.clearConnectRepoForm(); 1355 this.appContext.router.history.push(`${this.props.match.url}?addRepo=${val}`); 1356 } 1357 1358 private get appContext(): AppContext { 1359 return this.context as AppContext; 1360 } 1361 }