github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/settings/components/project-details/project-details.tsx (about) 1 import {AutocompleteField, FormField, HelpIcon, NotificationsApi, NotificationType, SlidingPanel, Tabs, Tooltip} from 'argo-ui'; 2 import classNames from 'classnames'; 3 import * as PropTypes from 'prop-types'; 4 import * as React from 'react'; 5 import {FormApi, Text} from 'react-form'; 6 import {RouteComponentProps} from 'react-router'; 7 import {Link} from 'react-router-dom'; 8 9 import {BadgePanel, CheckboxField, DataLoader, EditablePanel, ErrorNotification, MapInputField, Page, Query} from '../../../shared/components'; 10 import {AppContext, Consumer, AuthSettingsCtx} from '../../../shared/context'; 11 import {GroupKind, Groups, Project, DetailedProjectsResponse, ProjectSpec, ResourceKinds} from '../../../shared/models'; 12 import {CreateJWTTokenParams, DeleteJWTTokenParams, ProjectRoleParams, services} from '../../../shared/services'; 13 14 import {SyncWindowStatusIcon} from '../../../applications/components/utils'; 15 import {ProjectSyncWindowsParams} from '../../../shared/services/projects-service'; 16 import {ProjectEvents} from '../project-events/project-events'; 17 import {ProjectRoleEditPanel} from '../project-role-edit-panel/project-role-edit-panel'; 18 import {ProjectSyncWindowsEditPanel} from '../project-sync-windows-edit-panel/project-sync-windows-edit-panel'; 19 import {ResourceListsPanel} from './resource-lists-panel'; 20 import {DeepLinks} from '../../../shared/components/deep-links'; 21 22 require('./project-details.scss'); 23 24 interface ProjectDetailsState { 25 token: string; 26 } 27 28 function removeEl(items: any[], index: number) { 29 return items.slice(0, index).concat(items.slice(index + 1)); 30 } 31 32 function helpTip(text: string) { 33 return ( 34 <Tooltip content={text}> 35 <span style={{fontSize: 'smaller'}}> 36 {' '} 37 <i className='fas fa-info-circle' /> 38 </span> 39 </Tooltip> 40 ); 41 } 42 43 function emptyMessage(title: string) { 44 return <p>Project has no {title}</p>; 45 } 46 47 function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} { 48 return (projs || []).reduce( 49 (merged, proj) => { 50 merged.clusterResourceBlacklist = merged.clusterResourceBlacklist.concat(proj.spec.clusterResourceBlacklist || []); 51 merged.clusterResourceWhitelist = merged.clusterResourceWhitelist.concat(proj.spec.clusterResourceWhitelist || []); 52 merged.namespaceResourceBlacklist = merged.namespaceResourceBlacklist.concat(proj.spec.namespaceResourceBlacklist || []); 53 merged.namespaceResourceWhitelist = merged.namespaceResourceWhitelist.concat(proj.spec.namespaceResourceWhitelist || []); 54 merged.sourceRepos = merged.sourceRepos.concat(proj.spec.sourceRepos || []); 55 merged.destinations = merged.destinations.concat(proj.spec.destinations || []); 56 merged.sourceNamespaces = merged.sourceNamespaces.concat(proj.spec.sourceNamespaces || []); 57 58 merged.sourceRepos = merged.sourceRepos.filter((item, index) => { 59 return ( 60 index === 61 merged.sourceRepos.findIndex(obj => { 62 return obj === item; 63 }) 64 ); 65 }); 66 67 merged.destinationServiceAccounts = merged.destinationServiceAccounts.filter((item, index) => { 68 return ( 69 index === 70 merged.destinationServiceAccounts.findIndex(obj => { 71 return obj.server === item.server && obj.namespace === item.namespace && obj.defaultServiceAccount === item.defaultServiceAccount; 72 }) 73 ); 74 }); 75 76 merged.destinations = merged.destinations.filter((item, index) => { 77 return ( 78 index === 79 merged.destinations.findIndex(obj => { 80 return obj.server === item.server && obj.namespace === item.namespace; 81 }) 82 ); 83 }); 84 85 merged.clusterResourceBlacklist = merged.clusterResourceBlacklist.filter((item, index) => { 86 return ( 87 index === 88 merged.clusterResourceBlacklist.findIndex(obj => { 89 return obj.kind === item.kind && obj.group === item.group; 90 }) 91 ); 92 }); 93 94 merged.clusterResourceWhitelist = merged.clusterResourceWhitelist.filter((item, index) => { 95 return ( 96 index === 97 merged.clusterResourceWhitelist.findIndex(obj => { 98 return obj.kind === item.kind && obj.group === item.group; 99 }) 100 ); 101 }); 102 103 merged.namespaceResourceBlacklist = merged.namespaceResourceBlacklist.filter((item, index) => { 104 return ( 105 index === 106 merged.namespaceResourceBlacklist.findIndex(obj => { 107 return obj.kind === item.kind && obj.group === item.group; 108 }) 109 ); 110 }); 111 112 merged.namespaceResourceWhitelist = merged.namespaceResourceWhitelist.filter((item, index) => { 113 return ( 114 index === 115 merged.namespaceResourceWhitelist.findIndex(obj => { 116 return obj.kind === item.kind && obj.group === item.group; 117 }) 118 ); 119 }); 120 121 merged.sourceNamespaces = merged.sourceNamespaces.filter((item, index) => { 122 return ( 123 index === 124 merged.sourceNamespaces.findIndex(obj => { 125 return obj === item; 126 }) 127 ); 128 }); 129 merged.count += 1; 130 131 return merged; 132 }, 133 { 134 clusterResourceBlacklist: new Array<GroupKind>(), 135 namespaceResourceBlacklist: new Array<GroupKind>(), 136 namespaceResourceWhitelist: new Array<GroupKind>(), 137 clusterResourceWhitelist: new Array<GroupKind>(), 138 sourceRepos: [], 139 sourceNamespaces: [], 140 signatureKeys: [], 141 destinations: [], 142 description: '', 143 destinationServiceAccounts: [], 144 roles: [], 145 count: 0 146 } 147 ); 148 } 149 150 export class ProjectDetails extends React.Component<RouteComponentProps<{name: string}>, ProjectDetailsState> { 151 public static contextTypes = { 152 apis: PropTypes.object 153 }; 154 private projectRoleFormApi: FormApi; 155 private projectSyncWindowsFormApi: FormApi; 156 private loader: DataLoader; 157 158 constructor(props: RouteComponentProps<{name: string}>) { 159 super(props); 160 this.state = {token: ''}; 161 } 162 163 public render() { 164 return ( 165 <Consumer> 166 {ctx => ( 167 <Page 168 title='Projects' 169 toolbar={{ 170 breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Projects', path: '/settings/projects'}, {title: this.props.match.params.name}], 171 actionMenu: { 172 items: [ 173 {title: 'Add Role', iconClassName: 'fa fa-plus', action: () => ctx.navigation.goto('.', {newRole: true}, {replace: true})}, 174 {title: 'Add Sync Window', iconClassName: 'fa fa-plus', action: () => ctx.navigation.goto('.', {newWindow: true}, {replace: true})}, 175 { 176 title: 'Delete', 177 iconClassName: 'fa fa-times-circle', 178 action: async () => { 179 const confirmed = await ctx.popup.confirm('Delete project', 'Are you sure you want to delete project?'); 180 if (confirmed) { 181 try { 182 await services.projects.delete(this.props.match.params.name); 183 ctx.navigation.goto('/settings/projects', {replace: true}); 184 } catch (e) { 185 ctx.notifications.show({ 186 content: <ErrorNotification title='Unable to delete project' e={e} />, 187 type: NotificationType.Error 188 }); 189 } 190 } 191 } 192 } 193 ] 194 } 195 }}> 196 <DataLoader 197 load={() => { 198 return services.projects.getDetailed(this.props.match.params.name); 199 }} 200 ref={loader => (this.loader = loader)}> 201 {scopedProj => ( 202 <Query> 203 {params => { 204 const {project: proj, globalProjects: globalProj} = scopedProj; 205 return ( 206 <div className='project-details'> 207 <Tabs 208 selectedTabKey={params.get('tab') || 'summary'} 209 onTabSelected={tab => ctx.navigation.goto('.', {tab}, {replace: true})} 210 navCenter={true} 211 tabs={[ 212 { 213 key: 'summary', 214 title: 'Summary', 215 content: this.summaryTab(proj, reduceGlobal(globalProj), scopedProj) 216 }, 217 { 218 key: 'roles', 219 title: 'Roles', 220 content: this.rolesTab(proj, ctx) 221 }, 222 { 223 key: 'windows', 224 title: 'Sync Windows', 225 content: this.SyncWindowsTab(proj, ctx) 226 }, 227 { 228 key: 'events', 229 title: 'Events', 230 content: this.eventsTab(proj) 231 } 232 ].map(tab => ({...tab, isOnlyContentScrollable: true, extraVerticalScrollPadding: 160}))} 233 /> 234 <SlidingPanel 235 isMiddle={true} 236 isShown={params.get('editRole') !== null || params.get('newRole') !== null} 237 onClose={() => { 238 this.setState({token: ''}); 239 ctx.navigation.goto('.', {editRole: null, newRole: null}, {replace: true}); 240 }} 241 header={ 242 <div> 243 <button onClick={() => this.projectRoleFormApi.submitForm(null)} className='argo-button argo-button--base'> 244 {params.get('newRole') != null ? 'Create' : 'Update'} 245 </button>{' '} 246 <button 247 onClick={() => { 248 this.setState({token: ''}); 249 ctx.navigation.goto('.', {editRole: null, newRole: null}, {replace: true}); 250 }} 251 className='argo-button argo-button--base-o'> 252 Cancel 253 </button>{' '} 254 {params.get('newRole') === null ? ( 255 <button 256 onClick={async () => { 257 const confirmed = await ctx.popup.confirm( 258 'Delete project role', 259 'Are you sure you want to delete project role?' 260 ); 261 if (confirmed) { 262 try { 263 this.projectRoleFormApi.setValue('deleteRole', true); 264 this.projectRoleFormApi.submitForm(null); 265 ctx.navigation.goto('.', {editRole: null}, {replace: true}); 266 } catch (e) { 267 ctx.notifications.show({ 268 content: <ErrorNotification title='Unable to delete project role' e={e} />, 269 type: NotificationType.Error 270 }); 271 } 272 } 273 }} 274 className='argo-button argo-button--base'> 275 Delete 276 </button> 277 ) : null} 278 </div> 279 }> 280 {(params.get('editRole') !== null || params.get('newRole') === 'true') && ( 281 <ProjectRoleEditPanel 282 nameReadonly={params.get('newRole') === null ? true : false} 283 defaultParams={{ 284 newRole: params.get('newRole') === null ? false : true, 285 deleteRole: false, 286 projName: proj.metadata.name, 287 role: 288 params.get('newRole') === null && proj.spec.roles !== undefined 289 ? proj.spec.roles.find(x => params.get('editRole') === x.name) 290 : undefined, 291 jwtTokens: 292 params.get('newRole') === null && proj.spec.roles !== undefined && proj.status.jwtTokensByRole !== undefined 293 ? proj.status.jwtTokensByRole[params.get('editRole')].items 294 : undefined 295 }} 296 getApi={(api: FormApi) => (this.projectRoleFormApi = api)} 297 submit={async (projRoleParams: ProjectRoleParams) => { 298 try { 299 await services.projects.updateRole(projRoleParams); 300 ctx.navigation.goto('.', {editRole: null, newRole: null}, {replace: true}); 301 this.loader.reload(); 302 } catch (e) { 303 ctx.notifications.show({ 304 content: <ErrorNotification title='Unable to edit project' e={e} />, 305 type: NotificationType.Error 306 }); 307 } 308 }} 309 token={this.state.token} 310 createJWTToken={async (jwtTokenParams: CreateJWTTokenParams) => this.createJWTToken(jwtTokenParams, ctx.notifications)} 311 deleteJWTToken={async (jwtTokenParams: DeleteJWTTokenParams) => this.deleteJWTToken(jwtTokenParams, ctx.notifications)} 312 hideJWTToken={() => this.setState({token: ''})} 313 /> 314 )} 315 </SlidingPanel> 316 <SlidingPanel 317 isNarrow={false} 318 isMiddle={false} 319 isShown={params.get('editWindow') !== null || params.get('newWindow') !== null} 320 onClose={() => { 321 this.setState({token: ''}); 322 ctx.navigation.goto('.', {editWindow: null, newWindow: null}, {replace: true}); 323 }} 324 header={ 325 <div> 326 <button 327 onClick={() => { 328 if (params.get('newWindow') === null) { 329 this.projectSyncWindowsFormApi.setValue('id', Number(params.get('editWindow'))); 330 } 331 this.projectSyncWindowsFormApi.submitForm(null); 332 }} 333 className='argo-button argo-button--base'> 334 {params.get('newWindow') != null ? 'Create' : 'Update'} 335 </button>{' '} 336 <button 337 onClick={() => { 338 this.setState({token: ''}); 339 ctx.navigation.goto('.', {editWindow: null, newWindow: null}, {replace: true}); 340 }} 341 className='argo-button argo-button--base-o'> 342 Cancel 343 </button>{' '} 344 {params.get('newWindow') === null ? ( 345 <button 346 onClick={async () => { 347 const confirmed = await ctx.popup.confirm( 348 'Delete sync window', 349 'Are you sure you want to delete sync window?' 350 ); 351 if (confirmed) { 352 try { 353 this.projectSyncWindowsFormApi.setValue('id', Number(params.get('editWindow'))); 354 this.projectSyncWindowsFormApi.setValue('deleteWindow', true); 355 this.projectSyncWindowsFormApi.submitForm(null); 356 ctx.navigation.goto('.', {editWindow: null}, {replace: true}); 357 } catch (e) { 358 ctx.notifications.show({ 359 content: <ErrorNotification title='Unable to delete sync window' e={e} />, 360 type: NotificationType.Error 361 }); 362 } 363 } 364 }} 365 className='argo-button argo-button--base'> 366 Delete 367 </button> 368 ) : null} 369 </div> 370 }> 371 {(params.get('editWindow') !== null || params.get('newWindow') === 'true') && ( 372 <ProjectSyncWindowsEditPanel 373 defaultParams={{ 374 newWindow: params.get('newWindow') === null ? false : true, 375 projName: proj.metadata.name, 376 window: 377 params.get('newWindow') === null && proj.spec.syncWindows !== undefined 378 ? proj.spec.syncWindows[Number(params.get('editWindow'))] 379 : undefined, 380 id: 381 params.get('newWindow') === null && proj.spec.syncWindows !== undefined 382 ? Number(params.get('editWindow')) 383 : undefined 384 }} 385 getApi={(api: FormApi) => (this.projectSyncWindowsFormApi = api)} 386 submit={async (projectSyncWindowsParams: ProjectSyncWindowsParams) => { 387 try { 388 await services.projects.updateWindow(projectSyncWindowsParams); 389 ctx.navigation.goto('.', {editWindow: null, newWindow: null}, {replace: true}); 390 this.loader.reload(); 391 } catch (e) { 392 ctx.notifications.show({ 393 content: <ErrorNotification title='Unable to edit project' e={e} />, 394 type: NotificationType.Error 395 }); 396 } 397 }} 398 /> 399 )} 400 </SlidingPanel> 401 </div> 402 ); 403 }} 404 </Query> 405 )} 406 </DataLoader> 407 </Page> 408 )} 409 </Consumer> 410 ); 411 } 412 413 private async deleteJWTToken(params: DeleteJWTTokenParams, notifications: NotificationsApi) { 414 try { 415 await services.projects.deleteJWTToken(params); 416 const info = await services.projects.getDetailed(this.props.match.params.name); 417 this.loader.setData(info); 418 } catch (e) { 419 notifications.show({ 420 content: <ErrorNotification title='Unable to delete JWT token' e={e} />, 421 type: NotificationType.Error 422 }); 423 } 424 } 425 426 private async createJWTToken(params: CreateJWTTokenParams, notifications: NotificationsApi) { 427 try { 428 const jwtToken = await services.projects.createJWTToken(params); 429 const info = await services.projects.getDetailed(this.props.match.params.name); 430 this.loader.setData(info); 431 this.setState({token: jwtToken.token}); 432 } catch (e) { 433 notifications.show({ 434 content: <ErrorNotification title='Unable to create JWT token' e={e} />, 435 type: NotificationType.Error 436 }); 437 } 438 } 439 440 private eventsTab(proj: Project) { 441 return ( 442 <div className='argo-container'> 443 <ProjectEvents projectName={proj.metadata.name} /> 444 </div> 445 ); 446 } 447 448 private rolesTab(proj: Project, ctx: any) { 449 return ( 450 <div className='argo-container'> 451 {((proj.spec.roles || []).length > 0 && ( 452 <div className='argo-table-list argo-table-list--clickable'> 453 <div className='argo-table-list__head'> 454 <div className='row'> 455 <div className='columns small-3'>NAME</div> 456 <div className='columns small-6'>DESCRIPTION</div> 457 </div> 458 </div> 459 {(proj.spec.roles || []).map(role => ( 460 <div className='argo-table-list__row' key={`${role.name}`} onClick={() => ctx.navigation.goto(`.`, {editRole: role.name})}> 461 <div className='row'> 462 <div className='columns small-3'>{role.name}</div> 463 <div className='columns small-6'>{role.description}</div> 464 </div> 465 </div> 466 ))} 467 </div> 468 )) || ( 469 <div className='white-box'> 470 <p>Project has no roles</p> 471 </div> 472 )} 473 </div> 474 ); 475 } 476 477 private SyncWindowsTab(proj: Project, ctx: any) { 478 return ( 479 <div className='argo-container'> 480 {((proj.spec.syncWindows || []).length > 0 && ( 481 <DataLoader 482 noLoaderOnInputChange={true} 483 input={proj.spec.syncWindows} 484 load={async () => { 485 return await services.projects.getSyncWindows(proj.metadata.name); 486 }}> 487 {data => ( 488 <div className='argo-table-list argo-table-list--clickable'> 489 <div className='argo-table-list__head'> 490 <div className='row'> 491 <div className='columns small-8-elements'> 492 STATUS 493 {helpTip( 494 'If a window is active or inactive and what the current ' + 495 'effect would be if it was assigned to an application, namespace or cluster. ' + 496 'Red: no syncs allowed. ' + 497 'Yellow: manual syncs allowed. ' + 498 'Green: all syncs allowed' 499 )} 500 </div> 501 <div className='columns small-8-elements'> 502 WINDOW 503 {helpTip('The kind, start time and duration of the window')} 504 </div> 505 <div className='columns small-8-elements'> 506 APPLICATIONS 507 {helpTip('The applications assigned to the window, wildcards are supported')} 508 </div> 509 <div className='columns small-8-elements'> 510 NAMESPACES 511 {helpTip('The namespaces assigned to the window, wildcards are supported')} 512 </div> 513 <div className='columns small-8-elements'> 514 CLUSTERS 515 {helpTip('The clusters assigned to the window, wildcards are supported')} 516 </div> 517 <div className='columns small-8-elements'> 518 MANUALSYNC 519 {helpTip('If the window allows manual syncs')} 520 </div> 521 <div className='columns small-8-elements'> 522 USE AND OPERATOR 523 {helpTip('Use AND operator while selecting the apps that match the configured selectors')} 524 </div> 525 <div className='columns small-8-elements'> 526 DESCRIPTION 527 {helpTip('Add a description to your sync window (eg "Ticket 12345")')} 528 </div> 529 </div> 530 </div> 531 {(proj.spec.syncWindows || []).map((window, i) => ( 532 <div className='argo-table-list__row' key={`${i}`} onClick={() => ctx.navigation.goto(`.`, {editWindow: `${i}`})}> 533 <div className='row'> 534 <div className='columns small-8-elements'> 535 <span> 536 <SyncWindowStatusIcon state={data} window={window} /> 537 </span> 538 </div> 539 <div className='columns small-8-elements'> 540 {window.kind}:{window.schedule}:{window.duration}:{window.timeZone} 541 </div> 542 <div className='columns small-8-elements'>{(window.applications || ['-']).join(',')}</div> 543 <div className='columns small-8-elements'>{(window.namespaces || ['-']).join(',')}</div> 544 <div className='columns small-8-elements'>{(window.clusters || ['-']).join(',')}</div> 545 <div className='columns small-8-elements'>{window.manualSync ? 'Enabled' : 'Disabled'}</div> 546 <div className='columns small-8-elements'>{window.andOperator ? 'Enabled' : 'Disabled'}</div> 547 <div className='columns small-8-elements'>{window.description || ''}</div> 548 </div> 549 </div> 550 ))} 551 </div> 552 )} 553 </DataLoader> 554 )) || ( 555 <div className='white-box'> 556 <p>Project has no sync windows</p> 557 </div> 558 )} 559 </div> 560 ); 561 } 562 563 private get appContext(): AppContext { 564 return this.context as AppContext; 565 } 566 567 private async saveProject(updatedProj: Project) { 568 try { 569 const proj = await services.projects.get(updatedProj.metadata.name); 570 proj.metadata.labels = updatedProj.metadata.labels; 571 proj.spec = updatedProj.spec; 572 573 await services.projects.update(proj); 574 const scopedProj = await services.projects.getDetailed(this.props.match.params.name); 575 this.loader.setData(scopedProj); 576 } catch (e) { 577 this.appContext.apis.notifications.show({ 578 content: <ErrorNotification title='Unable to update project' e={e} />, 579 type: NotificationType.Error 580 }); 581 } 582 } 583 584 private summaryTab(proj: Project, globalProj: ProjectSpec & {count: number}, scopedProj: DetailedProjectsResponse) { 585 return ( 586 <div className='argo-container'> 587 <EditablePanel 588 save={item => this.saveProject(item)} 589 validate={input => ({ 590 'metadata.name': !input.metadata.name && 'Project name is required' 591 })} 592 values={proj} 593 title='GENERAL' 594 items={[ 595 { 596 title: 'NAME', 597 view: proj.metadata.name, 598 edit: () => proj.metadata.name 599 }, 600 { 601 title: 'DESCRIPTION', 602 view: proj.spec.description, 603 edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.description' component={Text} /> 604 }, 605 { 606 title: 'LABELS', 607 view: Object.keys(proj.metadata.labels || {}) 608 .map(label => `${label}=${proj.metadata.labels[label]}`) 609 .join(' '), 610 edit: (formApi: FormApi) => <FormField formApi={formApi} field='metadata.labels' component={MapInputField} /> 611 }, 612 { 613 title: 'LINKS', 614 view: ( 615 <div style={{margin: '8px 0'}}> 616 <DataLoader load={() => services.projects.getLinks(proj.metadata.name)}>{links => <DeepLinks links={links.items} />}</DataLoader> 617 </div> 618 ) 619 }, 620 { 621 title: 'APPLICATIONS', 622 view: ( 623 <div> 624 <DataLoader load={() => services.applications.list([proj.metadata.name])}> 625 {apps => <Link to={'/applications?proj=' + proj.metadata.name}>{apps.items.length}</Link>} 626 </DataLoader> 627 </div> 628 ) 629 } 630 ]} 631 /> 632 633 <EditablePanel 634 save={item => this.saveProject(item)} 635 values={proj} 636 title={<React.Fragment>SOURCE REPOSITORIES {helpTip('Git repositories where application manifests are permitted to be retrieved from')}</React.Fragment>} 637 view={ 638 <React.Fragment> 639 {proj.spec.sourceRepos 640 ? proj.spec.sourceRepos.map((repo, i) => ( 641 <div className='row white-box__details-row' key={i}> 642 <div className='columns small-12'>{repo}</div> 643 </div> 644 )) 645 : emptyMessage('source repositories')} 646 </React.Fragment> 647 } 648 edit={formApi => ( 649 <DataLoader load={() => services.repos.list()}> 650 {repos => ( 651 <React.Fragment> 652 {(formApi.values.spec.sourceRepos || []).map((_: Project, i: number) => ( 653 <div className='row white-box__details-row' key={i}> 654 <div className='columns small-12'> 655 <FormField 656 formApi={formApi} 657 field={`spec.sourceRepos[${i}]`} 658 component={AutocompleteField} 659 componentProps={{items: repos.map(repo => repo.repo)}} 660 /> 661 <i className='fa fa-times' onClick={() => formApi.setValue('spec.sourceRepos', removeEl(formApi.values.spec.sourceRepos, i))} /> 662 </div> 663 </div> 664 ))} 665 <button 666 className='argo-button argo-button--short' 667 onClick={() => formApi.setValue('spec.sourceRepos', (formApi.values.spec.sourceRepos || []).concat('*'))}> 668 ADD SOURCE 669 </button> 670 </React.Fragment> 671 )} 672 </DataLoader> 673 )} 674 items={[]} 675 /> 676 677 <EditablePanel 678 values={scopedProj} 679 title={<React.Fragment>SCOPED REPOSITORIES{helpTip('Git repositories where application manifests are permitted to be retrieved from')}</React.Fragment>} 680 view={ 681 <React.Fragment> 682 {scopedProj.repositories && scopedProj.repositories.length 683 ? scopedProj.repositories.map((repo, i) => ( 684 <div className='row white-box__details-row' key={i}> 685 <div className='columns small-12'>{repo.repo}</div> 686 </div> 687 )) 688 : emptyMessage('source repositories')} 689 </React.Fragment> 690 } 691 items={[]} 692 /> 693 <AuthSettingsCtx.Consumer> 694 {authCtx => 695 authCtx?.appsInAnyNamespaceEnabled && ( 696 <EditablePanel 697 save={item => this.saveProject(item)} 698 values={proj} 699 title={ 700 <React.Fragment>SOURCE NAMESPACES {helpTip('Kubernetes namespaces where application resources are allowed to be created in')}</React.Fragment> 701 } 702 view={ 703 <React.Fragment> 704 {proj.spec.sourceNamespaces 705 ? proj.spec.sourceNamespaces.map((namespace, i) => ( 706 <div className='row white-box__details-row' key={i}> 707 <div className='columns small-12'>{namespace}</div> 708 </div> 709 )) 710 : emptyMessage('source namespaces')} 711 </React.Fragment> 712 } 713 edit={formApi => ( 714 <React.Fragment> 715 {(formApi.values.spec.sourceNamespaces || []).map((_: Project, i: number) => ( 716 <div className='row white-box__details-row' key={i}> 717 <div className='columns small-12'> 718 <FormField formApi={formApi} field={`spec.sourceNamespaces[${i}]`} component={AutocompleteField} /> 719 <i 720 className='fa fa-times' 721 onClick={() => formApi.setValue('spec.sourceNamespaces', removeEl(formApi.values.spec.sourceNamespaces, i))} 722 /> 723 </div> 724 </div> 725 ))} 726 <button 727 className='argo-button argo-button--short' 728 onClick={() => formApi.setValue('spec.sourceNamespaces', (formApi.values.spec.sourceNamespaces || []).concat('*'))}> 729 ADD SOURCE 730 </button> 731 </React.Fragment> 732 )} 733 items={[]} 734 /> 735 ) 736 } 737 </AuthSettingsCtx.Consumer> 738 <EditablePanel 739 save={item => this.saveProject(item)} 740 values={proj} 741 title={<React.Fragment>DESTINATIONS {helpTip('Cluster and namespaces where applications are permitted to be deployed to')}</React.Fragment>} 742 view={ 743 <React.Fragment> 744 {proj.spec.destinations ? ( 745 <React.Fragment> 746 <div className='row white-box__details-row'> 747 <div className='columns small-4'>Server</div> 748 <div className='columns small-3'>Name</div> 749 <div className='columns small-5'>Namespace</div> 750 </div> 751 {proj.spec.destinations.map((dest, i) => ( 752 <div className='row white-box__details-row' key={i}> 753 <div className='columns small-4'>{dest.server}</div> 754 <div className='columns small-3'>{dest.name}</div> 755 <div className='columns small-5'>{dest.namespace}</div> 756 </div> 757 ))} 758 </React.Fragment> 759 ) : ( 760 emptyMessage('destinations') 761 )} 762 </React.Fragment> 763 } 764 edit={formApi => ( 765 <DataLoader load={() => services.clusters.list()}> 766 {clusters => ( 767 <React.Fragment> 768 <div className='row white-box__details-row'> 769 <div className='columns small-4'>Server</div> 770 <div className='columns small-3'>Name</div> 771 <div className='columns small-5'>Namespace</div> 772 </div> 773 {(formApi.values.spec.destinations || []).map((_: Project, i: number) => ( 774 <div className='row white-box__details-row' key={i}> 775 <div className='columns small-4'> 776 <FormField 777 formApi={formApi} 778 field={`spec.destinations[${i}].server`} 779 component={AutocompleteField} 780 componentProps={{items: clusters.map(cluster => cluster.server)}} 781 /> 782 </div> 783 <div className='columns small-3'> 784 <FormField 785 formApi={formApi} 786 field={`spec.destinations[${i}].name`} 787 component={AutocompleteField} 788 componentProps={{items: clusters.map(cluster => cluster.name)}} 789 /> 790 </div> 791 <div className='columns small-5'> 792 <FormField formApi={formApi} field={`spec.destinations[${i}].namespace`} component={AutocompleteField} /> 793 </div> 794 <i className='fa fa-times' onClick={() => formApi.setValue('spec.destinations', removeEl(formApi.values.spec.destinations, i))} /> 795 </div> 796 ))} 797 <button 798 className='argo-button argo-button--short' 799 onClick={() => 800 formApi.setValue( 801 'spec.destinations', 802 (formApi.values.spec.destinations || []).concat({ 803 server: '*', 804 namespace: '*', 805 name: '*' 806 }) 807 ) 808 }> 809 ADD DESTINATION 810 </button> 811 </React.Fragment> 812 )} 813 </DataLoader> 814 )} 815 items={[]} 816 /> 817 818 <EditablePanel 819 values={scopedProj} 820 title={<React.Fragment>SCOPED CLUSTERS{helpTip('Cluster and namespaces where applications are permitted to be deployed to')}</React.Fragment>} 821 view={ 822 <React.Fragment> 823 {scopedProj.clusters && scopedProj.clusters.length 824 ? scopedProj.clusters.map((cluster, i) => ( 825 <div className='row white-box__details-row' key={i}> 826 <div className='columns small-12'>{cluster.server}</div> 827 </div> 828 )) 829 : emptyMessage('destinations')} 830 </React.Fragment> 831 } 832 items={[]} 833 /> 834 835 <EditablePanel 836 save={item => this.saveProject(item)} 837 values={proj} 838 title={ 839 <React.Fragment> 840 DESTINATION SERVICE ACCOUNTS{' '} 841 {helpTip( 842 'Destination Service Accounts holds information about the service accounts to be impersonated for the application sync operation for each destination.' 843 )} 844 </React.Fragment> 845 } 846 view={ 847 <React.Fragment> 848 {proj.spec.destinationServiceAccounts ? ( 849 <React.Fragment> 850 <div className='row white-box__details-row'> 851 <div className='columns small-4'>Server</div> 852 <div className='columns small-3'>Namespace</div> 853 <div className='columns small-5'>DefaultServiceAccount</div> 854 </div> 855 {proj.spec.destinationServiceAccounts.map((dest, i) => ( 856 <div className='row white-box__details-row' key={i}> 857 <div className='columns small-4'>{dest.server}</div> 858 <div className='columns small-3'>{dest.namespace}</div> 859 <div className='columns small-5'>{dest.defaultServiceAccount}</div> 860 </div> 861 ))} 862 </React.Fragment> 863 ) : ( 864 emptyMessage('destinationServiceAccount') 865 )} 866 </React.Fragment> 867 } 868 edit={formApi => ( 869 <DataLoader load={() => services.clusters.list()}> 870 {clusters => ( 871 <React.Fragment> 872 <div className='row white-box__details-row'> 873 <div className='columns small-4'>Server</div> 874 <div className='columns small-3'>Namespace</div> 875 <div className='columns small-5'>DefaultServiceAccount</div> 876 </div> 877 {(formApi.values.spec.destinationServiceAccounts || []).map((_: Project, i: number) => ( 878 <div className='row white-box__details-row' key={i}> 879 <div className='columns small-4'> 880 <FormField 881 formApi={formApi} 882 field={`spec.destinationServiceAccounts[${i}].server`} 883 component={AutocompleteField} 884 componentProps={{items: clusters.map(cluster => cluster.server)}} 885 /> 886 </div> 887 <div className='columns small-3'> 888 <FormField formApi={formApi} field={`spec.destinationServiceAccounts[${i}].namespace`} component={AutocompleteField} /> 889 </div> 890 <div className='columns small-5'> 891 <FormField 892 formApi={formApi} 893 field={`spec.destinationServiceAccounts[${i}].defaultServiceAccount`} 894 component={AutocompleteField} 895 componentProps={{items: clusters.map(cluster => cluster.name)}} 896 /> 897 </div> 898 <i 899 className='fa fa-times' 900 onClick={() => formApi.setValue('spec.destinationServiceAccounts', removeEl(formApi.values.spec.destinationServiceAccounts, i))} 901 /> 902 </div> 903 ))} 904 905 <button 906 className='argo-button argo-button--short' 907 onClick={() => 908 formApi.setValue( 909 'spec.destinationServiceAccounts', 910 (formApi.values.spec.destinationServiceAccounts || []).concat({ 911 server: '*', 912 namespace: '*', 913 defaultServiceAccount: '*' 914 }) 915 ) 916 }> 917 ADD DESTINATION SERVICE ACCOUNTS 918 </button> 919 </React.Fragment> 920 )} 921 </DataLoader> 922 )} 923 items={[]} 924 /> 925 926 <ResourceListsPanel proj={proj} saveProject={item => this.saveProject(item)} /> 927 {globalProj.count > 0 && ( 928 <ResourceListsPanel 929 title={<p>INHERITED FROM GLOBAL PROJECTS {helpTip('Global projects provide configurations that other projects can inherit from.')}</p>} 930 proj={{metadata: null, spec: globalProj, status: null}} 931 /> 932 )} 933 934 <EditablePanel 935 save={item => this.saveProject(item)} 936 values={proj} 937 title={<React.Fragment>GPG SIGNATURE KEYS {helpTip('IDs of GnuPG keys that commits must be signed with in order to be allowed to sync to')}</React.Fragment>} 938 view={ 939 <React.Fragment> 940 {proj.spec.signatureKeys 941 ? proj.spec.signatureKeys.map((key, i) => ( 942 <div className='row white-box__details-row' key={i}> 943 <div className='columns small-12'>{key.keyID}</div> 944 </div> 945 )) 946 : emptyMessage('signature keys')} 947 </React.Fragment> 948 } 949 edit={formApi => ( 950 <DataLoader load={() => services.gpgkeys.list()}> 951 {keys => ( 952 <React.Fragment> 953 {(formApi.values.spec.signatureKeys || []).map((_: Project, i: number) => ( 954 <div className='row white-box__details-row' key={i}> 955 <div className='columns small-12'> 956 <FormField 957 formApi={formApi} 958 field={`spec.signatureKeys[${i}].keyID`} 959 component={AutocompleteField} 960 componentProps={{items: keys.map(key => key.keyID)}} 961 /> 962 </div> 963 <i className='fa fa-times' onClick={() => formApi.setValue('spec.signatureKeys', removeEl(formApi.values.spec.signatureKeys, i))} /> 964 </div> 965 ))} 966 <button 967 className='argo-button argo-button--short' 968 onClick={() => 969 formApi.setValue( 970 'spec.signatureKeys', 971 (formApi.values.spec.signatureKeys || []).concat({ 972 keyID: '' 973 }) 974 ) 975 }> 976 ADD KEY 977 </button> 978 </React.Fragment> 979 )} 980 </DataLoader> 981 )} 982 items={[]} 983 /> 984 985 <EditablePanel 986 save={item => this.saveProject(item)} 987 values={proj} 988 title={<React.Fragment>RESOURCE MONITORING {helpTip('Enables monitoring of top level resources in the application target namespace')}</React.Fragment>} 989 view={ 990 proj.spec.orphanedResources ? ( 991 <React.Fragment> 992 <p> 993 <i className={'fa fa-toggle-on'} /> Enabled 994 </p> 995 <p> 996 <i 997 className={classNames('fa', { 998 'fa-toggle-off': !proj.spec.orphanedResources.warn, 999 'fa-toggle-on': proj.spec.orphanedResources.warn 1000 })} 1001 />{' '} 1002 Application warning conditions are {proj.spec.orphanedResources.warn ? 'enabled' : 'disabled'}. 1003 </p> 1004 {(proj.spec.orphanedResources.ignore || []).length > 0 ? ( 1005 <React.Fragment> 1006 <p>Resources Ignore List</p> 1007 <div className='row white-box__details-row'> 1008 <div className='columns small-4'>Group</div> 1009 <div className='columns small-4'>Kind</div> 1010 <div className='columns small-4'>Name</div> 1011 </div> 1012 {(proj.spec.orphanedResources.ignore || []).map((resource, i) => ( 1013 <div className='row white-box__details-row' key={i}> 1014 <div className='columns small-4'>{resource.group}</div> 1015 <div className='columns small-4'>{resource.kind}</div> 1016 <div className='columns small-4'>{resource.name}</div> 1017 </div> 1018 ))} 1019 </React.Fragment> 1020 ) : ( 1021 <p>The resource ignore list is empty</p> 1022 )} 1023 </React.Fragment> 1024 ) : ( 1025 <p> 1026 <i className={'fa fa-toggle-off'} /> Disabled 1027 </p> 1028 ) 1029 } 1030 edit={formApi => 1031 formApi.values.spec.orphanedResources ? ( 1032 <React.Fragment> 1033 <button className='argo-button argo-button--base' onClick={() => formApi.setValue('spec.orphanedResources', null)}> 1034 DISABLE 1035 </button> 1036 <div className='row white-box__details-row'> 1037 <div className='columns small-4'> 1038 Enable application warning conditions? 1039 <HelpIcon title='If checked, Application will have a warning condition when orphaned resources detected' /> 1040 </div> 1041 <div className='columns small-8'> 1042 <FormField formApi={formApi} field='spec.orphanedResources.warn' component={CheckboxField} /> 1043 </div> 1044 </div> 1045 1046 <div> 1047 Resources Ignore List 1048 <HelpIcon title='Define resources that ArgoCD should not report as orphaned' /> 1049 </div> 1050 <div className='row white-box__details-row'> 1051 <div className='columns small-4'>Group</div> 1052 <div className='columns small-4'>Kind</div> 1053 <div className='columns small-4'>Name</div> 1054 </div> 1055 {((formApi.values.spec.orphanedResources.ignore || []).length === 0 && <div>Ignore list is empty</div>) || 1056 formApi.values.spec.orphanedResources.ignore.map((_: Project, i: number) => ( 1057 <div className='row white-box__details-row' key={i}> 1058 <div className='columns small-4'> 1059 <FormField 1060 formApi={formApi} 1061 field={`spec.orphanedResources.ignore[${i}].group`} 1062 component={AutocompleteField} 1063 componentProps={{items: Groups, filterSuggestions: true}} 1064 /> 1065 </div> 1066 <div className='columns small-4'> 1067 <FormField 1068 formApi={formApi} 1069 field={`spec.orphanedResources.ignore[${i}].kind`} 1070 component={AutocompleteField} 1071 componentProps={{items: ResourceKinds, filterSuggestions: true}} 1072 /> 1073 </div> 1074 <div className='columns small-4'> 1075 <FormField formApi={formApi} field={`spec.orphanedResources.ignore[${i}].name`} component={AutocompleteField} /> 1076 </div> 1077 <i 1078 className='fa fa-times' 1079 onClick={() => formApi.setValue('spec.orphanedResources.ignore', removeEl(formApi.values.spec.orphanedResources.ignore, i))} 1080 /> 1081 </div> 1082 ))} 1083 <br /> 1084 <button 1085 className='argo-button argo-button--base' 1086 onClick={() => 1087 formApi.setValue( 1088 'spec.orphanedResources.ignore', 1089 (formApi.values.spec.orphanedResources ? formApi.values.spec.orphanedResources.ignore || [] : []).concat({ 1090 keyID: '' 1091 }) 1092 ) 1093 }> 1094 ADD RESOURCE 1095 </button> 1096 </React.Fragment> 1097 ) : ( 1098 <button className='argo-button argo-button--base' onClick={() => formApi.setValue('spec.orphanedResources.ignore', [])}> 1099 ENABLE 1100 </button> 1101 ) 1102 } 1103 items={[]} 1104 /> 1105 1106 <BadgePanel project={proj.metadata.name} /> 1107 </div> 1108 ); 1109 } 1110 }