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