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