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  }