github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/settings/components/project-details/project-details.tsx (about)

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