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  }