github.com/argoproj/argo-cd@v1.8.7/ui/src/app/applications/components/application-details/application-details.tsx (about)

     1  import {Checkbox as ArgoCheckbox, DropDownMenu, MenuItem, NotificationType, SlidingPanel, Tab, Tabs, TopBarFilter} from 'argo-ui';
     2  import * as classNames from 'classnames';
     3  import * as PropTypes from 'prop-types';
     4  import * as React from 'react';
     5  import {Checkbox} from 'react-form';
     6  import {RouteComponentProps} from 'react-router';
     7  import {BehaviorSubject, Observable} from 'rxjs';
     8  import {DataLoader, EmptyState, ErrorNotification, EventsList, ObservableQuery, Page, Paginate, YamlEditor} from '../../../shared/components';
     9  import {AppContext} from '../../../shared/context';
    10  import * as appModels from '../../../shared/models';
    11  import {AppDetailsPreferences, AppsDetailsViewType, services} from '../../../shared/services';
    12  
    13  import {SyncStatuses} from '../../../shared/models';
    14  import {ApplicationConditions} from '../application-conditions/application-conditions';
    15  import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history';
    16  import {ApplicationNodeInfo} from '../application-node-info/application-node-info';
    17  import {ApplicationOperationState} from '../application-operation-state/application-operation-state';
    18  import {ApplicationParameters} from '../application-parameters/application-parameters';
    19  import {ApplicationResourceEvents} from '../application-resource-events/application-resource-events';
    20  import {ApplicationResourceTree, ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
    21  import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff';
    22  import {ApplicationStatusPanel} from '../application-status-panel/application-status-panel';
    23  import {ApplicationSummary} from '../application-summary/application-summary';
    24  import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
    25  import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer';
    26  import * as AppUtils from '../utils';
    27  import {isSameNode, nodeKey} from '../utils';
    28  import {ApplicationResourceList} from './application-resource-list';
    29  
    30  const jsonMergePatch = require('json-merge-patch');
    31  
    32  require('./application-details.scss');
    33  
    34  type ActionMenuItem = MenuItem & {disabled?: boolean};
    35  
    36  export class ApplicationDetails extends React.Component<RouteComponentProps<{name: string}>, {page: number}> {
    37      public static contextTypes = {
    38          apis: PropTypes.object
    39      };
    40  
    41      private appChanged = new BehaviorSubject<appModels.Application>(null);
    42  
    43      constructor(props: RouteComponentProps<{name: string}>) {
    44          super(props);
    45          this.state = {page: 0};
    46      }
    47  
    48      private get showOperationState() {
    49          return new URLSearchParams(this.props.history.location.search).get('operation') === 'true';
    50      }
    51  
    52      private get showConditions() {
    53          return new URLSearchParams(this.props.history.location.search).get('conditions') === 'true';
    54      }
    55  
    56      private get selectedRollbackDeploymentIndex() {
    57          return parseInt(new URLSearchParams(this.props.history.location.search).get('rollback'), 10);
    58      }
    59  
    60      private get selectedNodeInfo() {
    61          const nodeContainer = {key: '', container: 0};
    62          const node = new URLSearchParams(this.props.location.search).get('node');
    63          if (node) {
    64              const parts = node.split('/');
    65              nodeContainer.key = parts.slice(0, 4).join('/');
    66              nodeContainer.container = parseInt(parts[4] || '0', 10);
    67          }
    68          return nodeContainer;
    69      }
    70  
    71      private get selectedNodeKey() {
    72          const nodeContainer = this.selectedNodeInfo;
    73          return nodeContainer.key;
    74      }
    75  
    76      public render() {
    77          return (
    78              <ObservableQuery>
    79                  {q => (
    80                      <DataLoader
    81                          errorRenderer={error => <Page title='Application Details'>{error}</Page>}
    82                          loadingRenderer={() => <Page title='Application Details'>Loading...</Page>}
    83                          input={this.props.match.params.name}
    84                          load={name =>
    85                              Observable.combineLatest(this.loadAppInfo(name), services.viewPreferences.getPreferences(), q).map(items => {
    86                                  const pref = items[1].appDetails;
    87                                  const params = items[2];
    88                                  if (params.get('resource') != null) {
    89                                      pref.resourceFilter = params
    90                                          .get('resource')
    91                                          .split(',')
    92                                          .filter(item => !!item);
    93                                  }
    94                                  if (params.get('view') != null) {
    95                                      pref.view = params.get('view') as AppsDetailsViewType;
    96                                  }
    97                                  if (params.get('orphaned') != null) {
    98                                      pref.orphanedResources = params.get('orphaned') === 'true';
    99                                  }
   100                                  return {...items[0], pref};
   101                              })
   102                          }>
   103                          {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => {
   104                              tree.nodes = tree.nodes || [];
   105                              const kindsSet = new Set<string>(tree.nodes.map(item => item.kind));
   106                              const treeFilter = this.getTreeFilter(pref.resourceFilter);
   107                              treeFilter.kind.forEach(kind => {
   108                                  kindsSet.add(kind);
   109                              });
   110                              const kinds = Array.from(kindsSet);
   111                              const noKindsFilter = pref.resourceFilter.filter(item => item.indexOf('kind:') !== 0);
   112                              const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
   113  
   114                              const filter: TopBarFilter<string> = {
   115                                  items: [
   116                                      {content: () => <span>Sync</span>},
   117                                      {value: 'sync:Synced', label: 'Synced'},
   118                                      // Unhealthy includes 'Unknown' and 'OutOfSync'
   119                                      {value: 'sync:OutOfSync', label: 'OutOfSync'},
   120                                      {content: () => <span>Health</span>},
   121                                      {value: 'health:Healthy', label: 'Healthy'},
   122                                      {value: 'health:Progressing', label: 'Progressing'},
   123                                      {value: 'health:Degraded', label: 'Degraded'},
   124                                      {value: 'health:Missing', label: 'Missing'},
   125                                      {value: 'health:Unknown', label: 'Unknown'},
   126                                      {
   127                                          content: setSelection => (
   128                                              <div>
   129                                                  Kinds <a onClick={() => setSelection(noKindsFilter.concat(kinds.map(kind => `kind:${kind}`)))}>all</a> /{' '}
   130                                                  <a onClick={() => setSelection(noKindsFilter)}>none</a>
   131                                              </div>
   132                                          )
   133                                      },
   134                                      ...kinds.sort().map(kind => ({value: `kind:${kind}`, label: kind}))
   135                                  ],
   136                                  selectedValues: pref.resourceFilter,
   137                                  selectionChanged: items => {
   138                                      this.appContext.apis.navigation.goto('.', {resource: `${items.join(',')}`});
   139                                      services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
   140                                  }
   141                              };
   142  
   143                              const appNodesByName = this.groupAppNodesByKey(application, tree);
   144                              const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null;
   145                              const isAppSelected = selectedItem === application;
   146                              const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode);
   147                              const operationState = application.status.operationState;
   148                              const conditions = application.status.conditions || [];
   149                              const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy');
   150                              const tab = new URLSearchParams(this.props.history.location.search).get('tab');
   151                              const filteredRes = application.status.resources.filter(res => {
   152                                  const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''};
   153                                  resNode.root = resNode;
   154                                  return this.filterTreeNode(resNode, treeFilter);
   155                              });
   156                              return (
   157                                  <div className='application-details'>
   158                                      <Page
   159                                          title='Application Details'
   160                                          toolbar={{
   161                                              filter,
   162                                              breadcrumbs: [{title: 'Applications', path: '/applications'}, {title: this.props.match.params.name}],
   163                                              actionMenu: {items: this.getApplicationActionMenu(application)},
   164                                              tools: (
   165                                                  <React.Fragment key='app-list-tools'>
   166                                                      <div className='application-details__view-type'>
   167                                                          <i
   168                                                              className={classNames('fa fa-sitemap', {selected: pref.view === 'tree'})}
   169                                                              title='Tree'
   170                                                              onClick={() => {
   171                                                                  this.appContext.apis.navigation.goto('.', {view: 'tree'});
   172                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'tree'}});
   173                                                              }}
   174                                                          />
   175                                                          <i
   176                                                              className={classNames('fa fa-network-wired', {selected: pref.view === 'network'})}
   177                                                              title='Network'
   178                                                              onClick={() => {
   179                                                                  this.appContext.apis.navigation.goto('.', {view: 'network'});
   180                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'network'}});
   181                                                              }}
   182                                                          />
   183                                                          <i
   184                                                              className={classNames('fa fa-th-list', {selected: pref.view === 'list'})}
   185                                                              title='List'
   186                                                              onClick={() => {
   187                                                                  this.appContext.apis.navigation.goto('.', {view: 'list'});
   188                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'list'}});
   189                                                              }}
   190                                                          />
   191                                                      </div>
   192                                                  </React.Fragment>
   193                                              )
   194                                          }}>
   195                                          <div className='application-details__status-panel'>
   196                                              <ApplicationStatusPanel
   197                                                  application={application}
   198                                                  showOperation={() => this.setOperationStatusVisible(true)}
   199                                                  showConditions={() => this.setConditionsStatusVisible(true)}
   200                                              />
   201                                          </div>
   202                                          <div className='application-details__tree'>
   203                                              {refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
   204                                              {(tree.orphanedNodes || []).length > 0 && (
   205                                                  <div className='application-details__orphaned-filter'>
   206                                                      <ArgoCheckbox
   207                                                          checked={!!pref.orphanedResources}
   208                                                          id='orphanedFilter'
   209                                                          onChange={val => {
   210                                                              this.appContext.apis.navigation.goto('.', {orphaned: val});
   211                                                              services.viewPreferences.updatePreferences({appDetails: {...pref, orphanedResources: val}});
   212                                                          }}
   213                                                      />{' '}
   214                                                      <label htmlFor='orphanedFilter'>SHOW ORPHANED</label>
   215                                                  </div>
   216                                              )}
   217                                              {((pref.view === 'tree' || pref.view === 'network') && (
   218                                                  <ApplicationResourceTree
   219                                                      nodeFilter={node => this.filterTreeNode(node, treeFilter)}
   220                                                      selectedNodeFullName={this.selectedNodeKey}
   221                                                      onNodeClick={fullName => this.selectNode(fullName)}
   222                                                      nodeMenu={node => this.renderResourceMenu(node, application)}
   223                                                      tree={tree}
   224                                                      app={application}
   225                                                      showOrphanedResources={pref.orphanedResources}
   226                                                      useNetworkingHierarchy={pref.view === 'network'}
   227                                                      onClearFilter={() => {
   228                                                          this.appContext.apis.navigation.goto('.', {resource: ''});
   229                                                          services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: []}});
   230                                                      }}
   231                                                  />
   232                                              )) || (
   233                                                  <div>
   234                                                      {(filteredRes.length > 0 && (
   235                                                          <Paginate
   236                                                              page={this.state.page}
   237                                                              data={filteredRes}
   238                                                              onPageChange={page => this.setState({page})}
   239                                                              preferencesKey='application-details'>
   240                                                              {data => (
   241                                                                  <ApplicationResourceList
   242                                                                      onNodeClick={fullName => this.selectNode(fullName)}
   243                                                                      resources={data}
   244                                                                      nodeMenu={node => this.renderResourceMenu({...node, root: node}, application)}
   245                                                                  />
   246                                                              )}
   247                                                          </Paginate>
   248                                                      )) || (
   249                                                          <EmptyState icon='fa fa-search'>
   250                                                              <h4>No resources found</h4>
   251                                                              <h5>Try to change filter criteria</h5>
   252                                                          </EmptyState>
   253                                                      )}
   254                                                  </div>
   255                                              )}
   256                                          </div>
   257                                          <SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => this.selectNode('')}>
   258                                              <div>
   259                                                  {selectedNode && (
   260                                                      <DataLoader
   261                                                          noLoaderOnInputChange={true}
   262                                                          input={selectedNode.resourceVersion}
   263                                                          load={async () => {
   264                                                              const managedResources = await services.applications.managedResources(application.metadata.name, {
   265                                                                  id: {name: selectedNode.name, namespace: selectedNode.namespace, kind: selectedNode.kind, group: selectedNode.group}
   266                                                              });
   267                                                              const controlled = managedResources.find(item => isSameNode(selectedNode, item));
   268                                                              const summary = application.status.resources.find(item => isSameNode(selectedNode, item));
   269                                                              const controlledState = (controlled && summary && {summary, state: controlled}) || null;
   270                                                              const resQuery = {...selectedNode};
   271                                                              if (controlled && controlled.targetState) {
   272                                                                  resQuery.version = AppUtils.parseApiVersion(controlled.targetState.apiVersion).version;
   273                                                              }
   274                                                              const liveState = await services.applications.getResource(application.metadata.name, resQuery).catch(() => null);
   275                                                              const events =
   276                                                                  (liveState &&
   277                                                                      (await services.applications.resourceEvents(application.metadata.name, {
   278                                                                          name: liveState.metadata.name,
   279                                                                          namespace: liveState.metadata.namespace,
   280                                                                          uid: liveState.metadata.uid
   281                                                                      }))) ||
   282                                                                  [];
   283  
   284                                                              return {controlledState, liveState, events};
   285                                                          }}>
   286                                                          {data => (
   287                                                              <Tabs
   288                                                                  navTransparent={true}
   289                                                                  tabs={this.getResourceTabs(application, selectedNode, data.liveState, data.events, [
   290                                                                      {
   291                                                                          title: 'SUMMARY',
   292                                                                          key: 'summary',
   293                                                                          content: (
   294                                                                              <ApplicationNodeInfo
   295                                                                                  application={application}
   296                                                                                  live={data.liveState}
   297                                                                                  controlled={data.controlledState}
   298                                                                                  node={selectedNode}
   299                                                                              />
   300                                                                          )
   301                                                                      }
   302                                                                  ])}
   303                                                                  selectedTabKey={tab}
   304                                                                  onTabSelected={selected => this.appContext.apis.navigation.goto('.', {tab: selected})}
   305                                                              />
   306                                                          )}
   307                                                      </DataLoader>
   308                                                  )}
   309                                                  {isAppSelected && (
   310                                                      <Tabs
   311                                                          navTransparent={true}
   312                                                          tabs={[
   313                                                              {
   314                                                                  title: 'SUMMARY',
   315                                                                  key: 'summary',
   316                                                                  content: <ApplicationSummary app={application} updateApp={app => this.updateApp(app)} />
   317                                                              },
   318                                                              {
   319                                                                  title: 'PARAMETERS',
   320                                                                  key: 'parameters',
   321                                                                  content: (
   322                                                                      <DataLoader
   323                                                                          key='appDetails'
   324                                                                          input={application.spec.source}
   325                                                                          load={src =>
   326                                                                              services.repos
   327                                                                                  .appDetails(src)
   328                                                                                  .catch(() => ({type: 'Directory' as appModels.AppSourceType, path: application.spec.source.path}))
   329                                                                          }>
   330                                                                          {(details: appModels.RepoAppDetails) => (
   331                                                                              <ApplicationParameters save={app => this.updateApp(app)} application={application} details={details} />
   332                                                                          )}
   333                                                                      </DataLoader>
   334                                                                  )
   335                                                              },
   336                                                              {
   337                                                                  title: 'MANIFEST',
   338                                                                  key: 'manifest',
   339                                                                  content: (
   340                                                                      <YamlEditor
   341                                                                          minHeight={800}
   342                                                                          input={application.spec}
   343                                                                          onSave={async patch => {
   344                                                                              const spec = JSON.parse(JSON.stringify(application.spec));
   345                                                                              return services.applications.updateSpec(
   346                                                                                  application.metadata.name,
   347                                                                                  jsonMergePatch.apply(spec, JSON.parse(patch))
   348                                                                              );
   349                                                                          }}
   350                                                                      />
   351                                                                  )
   352                                                              },
   353                                                              {
   354                                                                  icon: 'fa fa-file-medical',
   355                                                                  title: 'DIFF',
   356                                                                  key: 'diff',
   357                                                                  content: (
   358                                                                      <DataLoader
   359                                                                          key='diff'
   360                                                                          load={async () =>
   361                                                                              await services.applications.managedResources(application.metadata.name, {
   362                                                                                  fields: [
   363                                                                                      'items.normalizedLiveState',
   364                                                                                      'items.predictedLiveState',
   365                                                                                      'items.group',
   366                                                                                      'items.kind',
   367                                                                                      'items.namespace',
   368                                                                                      'items.name'
   369                                                                                  ]
   370                                                                              })
   371                                                                          }>
   372                                                                          {managedResources => <ApplicationResourcesDiff states={managedResources} />}
   373                                                                      </DataLoader>
   374                                                                  )
   375                                                              },
   376                                                              {
   377                                                                  title: 'EVENTS',
   378                                                                  key: 'event',
   379                                                                  content: <ApplicationResourceEvents applicationName={application.metadata.name} />
   380                                                              }
   381                                                          ]}
   382                                                          selectedTabKey={tab}
   383                                                          onTabSelected={selected => this.appContext.apis.navigation.goto('.', {tab: selected})}
   384                                                      />
   385                                                  )}
   386                                              </div>
   387                                          </SlidingPanel>
   388                                          <ApplicationSyncPanel application={application} hide={() => this.showDeploy(null)} selectedResource={syncResourceKey} />
   389                                          <SlidingPanel isShown={this.selectedRollbackDeploymentIndex > -1} onClose={() => this.setRollbackPanelVisible(-1)}>
   390                                              {this.selectedRollbackDeploymentIndex > -1 && (
   391                                                  <ApplicationDeploymentHistory
   392                                                      app={application}
   393                                                      selectedRollbackDeploymentIndex={this.selectedRollbackDeploymentIndex}
   394                                                      rollbackApp={info => this.rollbackApplication(info, application)}
   395                                                      selectDeployment={i => this.setRollbackPanelVisible(i)}
   396                                                  />
   397                                              )}
   398                                          </SlidingPanel>
   399                                          <SlidingPanel isShown={this.showOperationState && !!operationState} onClose={() => this.setOperationStatusVisible(false)}>
   400                                              {operationState && <ApplicationOperationState application={application} operationState={operationState} />}
   401                                          </SlidingPanel>
   402                                          <SlidingPanel isShown={this.showConditions && !!conditions} onClose={() => this.setConditionsStatusVisible(false)}>
   403                                              {conditions && <ApplicationConditions conditions={conditions} />}
   404                                          </SlidingPanel>
   405                                      </Page>
   406                                  </div>
   407                              );
   408                          }}
   409                      </DataLoader>
   410                  )}
   411              </ObservableQuery>
   412          );
   413      }
   414  
   415      private getApplicationActionMenu(app: appModels.Application) {
   416          const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey];
   417          const fullName = nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
   418          return [
   419              {
   420                  iconClassName: 'fa fa-info-circle',
   421                  title: <span className='show-for-medium'>App Details</span>,
   422                  action: () => this.selectNode(fullName)
   423              },
   424              {
   425                  iconClassName: 'fa fa-file-medical',
   426                  title: <span className='show-for-medium'>App Diff</span>,
   427                  action: () => this.selectNode(fullName, 0, 'diff'),
   428                  disabled: app.status.sync.status === SyncStatuses.Synced
   429              },
   430              {
   431                  iconClassName: 'fa fa-sync',
   432                  title: <span className='show-for-medium'>Sync</span>,
   433                  action: () => this.showDeploy('all')
   434              },
   435              {
   436                  iconClassName: 'fa fa-info-circle',
   437                  title: <span className='show-for-medium'>Sync Status</span>,
   438                  action: () => this.setOperationStatusVisible(true),
   439                  disabled: !app.status.operationState
   440              },
   441              {
   442                  iconClassName: 'fa fa-history',
   443                  title: <span className='show-for-medium'>History and rollback</span>,
   444                  action: () => this.setRollbackPanelVisible(0),
   445                  disabled: !app.status.operationState
   446              },
   447              {
   448                  iconClassName: 'fa fa-times-circle',
   449                  title: <span className='show-for-medium'>Delete</span>,
   450                  action: () => this.deleteApplication()
   451              },
   452              {
   453                  iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}),
   454                  title: (
   455                      <React.Fragment>
   456                          <span className='show-for-medium'>Refresh</span>{' '}
   457                          <DropDownMenu
   458                              items={[
   459                                  {
   460                                      title: 'Hard Refresh',
   461                                      action: () => !refreshing && services.applications.get(app.metadata.name, 'hard')
   462                                  }
   463                              ]}
   464                              anchor={() => <i className='fa fa-caret-down' />}
   465                          />
   466                      </React.Fragment>
   467                  ),
   468                  disabled: !!refreshing,
   469                  action: () => {
   470                      if (!refreshing) {
   471                          services.applications.get(app.metadata.name, 'normal');
   472                          AppUtils.setAppRefreshing(app);
   473                          this.appChanged.next(app);
   474                      }
   475                  }
   476              }
   477          ];
   478      }
   479  
   480      private filterTreeNode(node: ResourceTreeNode, filter: {kind: string[]; health: string[]; sync: string[]}): boolean {
   481          const syncStatuses = filter.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []);
   482  
   483          return (
   484              (filter.kind.length === 0 || filter.kind.indexOf(node.kind) > -1) &&
   485              (syncStatuses.length === 0 || node.root.hook || (node.root.status && syncStatuses.indexOf(node.root.status) > -1)) &&
   486              (filter.health.length === 0 || node.root.hook || (node.root.health && filter.health.indexOf(node.root.health.status) > -1))
   487          );
   488      }
   489  
   490      private loadAppInfo(name: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> {
   491          return Observable.fromPromise(services.applications.get(name))
   492              .flatMap(app => {
   493                  const fallbackTree = {
   494                      nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})),
   495                      orphanedNodes: []
   496                  } as appModels.ApplicationTree;
   497                  return Observable.combineLatest(
   498                      Observable.merge(
   499                          Observable.from([app]),
   500                          this.appChanged.filter(item => !!item),
   501                          AppUtils.handlePageVisibility(() =>
   502                              services.applications
   503                                  .watch({name})
   504                                  .map(watchEvent => {
   505                                      if (watchEvent.type === 'DELETED') {
   506                                          this.onAppDeleted();
   507                                      }
   508                                      return watchEvent.application;
   509                                  })
   510                                  .repeat()
   511                                  .retryWhen(errors => errors.delay(500))
   512                          )
   513                      ),
   514                      Observable.merge(
   515                          Observable.from([fallbackTree]),
   516                          services.applications.resourceTree(name).catch(() => fallbackTree),
   517                          AppUtils.handlePageVisibility(() =>
   518                              services.applications
   519                                  .watchResourceTree(name)
   520                                  .repeat()
   521                                  .retryWhen(errors => errors.delay(500))
   522                          )
   523                      )
   524                  );
   525              })
   526              .filter(([application, tree]) => !!application && !!tree)
   527              .map(([application, tree]) => ({application, tree}));
   528      }
   529  
   530      private onAppDeleted() {
   531          this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`});
   532          this.appContext.apis.navigation.goto('/applications');
   533      }
   534  
   535      private async updateApp(app: appModels.Application) {
   536          const latestApp = await services.applications.get(app.metadata.name);
   537          latestApp.metadata.labels = app.metadata.labels;
   538          latestApp.metadata.annotations = app.metadata.annotations;
   539          latestApp.spec = app.spec;
   540          const updatedApp = await services.applications.update(latestApp);
   541          this.appChanged.next(updatedApp);
   542      }
   543  
   544      private groupAppNodesByKey(application: appModels.Application, tree: appModels.ApplicationTree) {
   545          const nodeByKey = new Map<string, appModels.ResourceDiff | appModels.ResourceNode | appModels.Application>();
   546          tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(nodeKey(node), node));
   547          nodeByKey.set(nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application);
   548          return nodeByKey;
   549      }
   550  
   551      private getTreeFilter(filter: string[]): {kind: string[]; health: string[]; sync: string[]} {
   552          const kind = new Array<string>();
   553          const health = new Array<string>();
   554          const sync = new Array<string>();
   555          for (const item of filter) {
   556              const [type, val] = item.split(':');
   557              switch (type) {
   558                  case 'kind':
   559                      kind.push(val);
   560                      break;
   561                  case 'health':
   562                      health.push(val);
   563                      break;
   564                  case 'sync':
   565                      sync.push(val);
   566                      break;
   567              }
   568          }
   569          return {kind, health, sync};
   570      }
   571  
   572      private showDeploy(resource: string) {
   573          this.appContext.apis.navigation.goto('.', {deploy: resource});
   574      }
   575  
   576      private setOperationStatusVisible(isVisible: boolean) {
   577          this.appContext.apis.navigation.goto('.', {operation: isVisible});
   578      }
   579  
   580      private setConditionsStatusVisible(isVisible: boolean) {
   581          this.appContext.apis.navigation.goto('.', {conditions: isVisible});
   582      }
   583  
   584      private setRollbackPanelVisible(selectedDeploymentIndex = 0) {
   585          this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex});
   586      }
   587  
   588      private selectNode(fullName: string, containerIndex = 0, tab: string = null) {
   589          const node = fullName ? `${fullName}/${containerIndex}` : null;
   590          this.appContext.apis.navigation.goto('.', {node, tab});
   591      }
   592  
   593      private async rollbackApplication(revisionHistory: appModels.RevisionHistory, application: appModels.Application) {
   594          try {
   595              const needDisableRollback = application.spec.syncPolicy && application.spec.syncPolicy.automated;
   596              let confirmationMessage = `Are you sure you want to rollback application '${this.props.match.params.name}'?`;
   597              if (needDisableRollback) {
   598                  confirmationMessage = `Auto-Sync needs to be disabled in order for rollback to occur.
   599  Are you sure you want to disable auto-sync and rollback application '${this.props.match.params.name}'?`;
   600              }
   601  
   602              const confirmed = await this.appContext.apis.popup.confirm('Rollback application', confirmationMessage);
   603              if (confirmed) {
   604                  if (needDisableRollback) {
   605                      const update = JSON.parse(JSON.stringify(application)) as appModels.Application;
   606                      update.spec.syncPolicy = {automated: null};
   607                      await services.applications.update(update);
   608                  }
   609                  await services.applications.rollback(this.props.match.params.name, revisionHistory.id);
   610                  this.appChanged.next(await services.applications.get(this.props.match.params.name));
   611                  this.setRollbackPanelVisible(-1);
   612              }
   613          } catch (e) {
   614              this.appContext.apis.notifications.show({
   615                  content: <ErrorNotification title='Unable to rollback application' e={e} />,
   616                  type: NotificationType.Error
   617              });
   618          }
   619      }
   620  
   621      private get appContext(): AppContext {
   622          return this.context as AppContext;
   623      }
   624  
   625      private renderResourceMenu(resource: ResourceTreeNode, application: appModels.Application): React.ReactNode {
   626          let menuItems: Observable<ActionMenuItem[]>;
   627          if (AppUtils.isAppNode(resource) && resource.name === application.metadata.name) {
   628              menuItems = Observable.from([this.getApplicationActionMenu(application)]);
   629          } else {
   630              const isRoot = resource.root && AppUtils.nodeKey(resource.root) === AppUtils.nodeKey(resource);
   631              const items: MenuItem[] = [
   632                  ...((isRoot && [
   633                      {
   634                          title: 'Sync',
   635                          action: () => this.showDeploy(nodeKey(resource))
   636                      }
   637                  ]) ||
   638                      []),
   639                  {
   640                      title: 'Delete',
   641                      action: async () => {
   642                          this.appContext.apis.popup.prompt(
   643                              'Delete resource',
   644                              () => (
   645                                  <div>
   646                                      <p>
   647                                          Are your sure you want to delete {resource.kind} '{resource.name}'?
   648                                      </p>
   649                                      <div className='argo-form-row' style={{paddingLeft: '30px'}}>
   650                                          <Checkbox id='force-delete-checkbox' field='force' /> <label htmlFor='force-delete-checkbox'>Force delete</label>
   651                                      </div>
   652                                  </div>
   653                              ),
   654                              {
   655                                  submit: async (vals, _, close) => {
   656                                      try {
   657                                          await services.applications.deleteResource(this.props.match.params.name, resource, !!vals.force);
   658                                          this.appChanged.next(await services.applications.get(this.props.match.params.name));
   659                                          close();
   660                                      } catch (e) {
   661                                          this.appContext.apis.notifications.show({
   662                                              content: <ErrorNotification title='Unable to delete resource' e={e} />,
   663                                              type: NotificationType.Error
   664                                          });
   665                                      }
   666                                  }
   667                              }
   668                          );
   669                      }
   670                  }
   671              ];
   672              const resourceActions = services.applications
   673                  .getResourceActions(application.metadata.name, resource)
   674                  .then(actions =>
   675                      items.concat(
   676                          actions.map(action => ({
   677                              title: action.name,
   678                              disabled: !!action.disabled,
   679                              action: async () => {
   680                                  try {
   681                                      const confirmed = await this.appContext.apis.popup.confirm(
   682                                          `Execute '${action.name}' action?`,
   683                                          `Are you sure you want to execute '${action.name}' action?`
   684                                      );
   685                                      if (confirmed) {
   686                                          await services.applications.runResourceAction(application.metadata.name, resource, action.name);
   687                                      }
   688                                  } catch (e) {
   689                                      this.appContext.apis.notifications.show({
   690                                          content: <ErrorNotification title='Unable to execute resource action' e={e} />,
   691                                          type: NotificationType.Error
   692                                      });
   693                                  }
   694                              }
   695                          }))
   696                      )
   697                  )
   698                  .catch(() => items);
   699              menuItems = Observable.merge(Observable.from([items]), Observable.fromPromise(resourceActions));
   700          }
   701          return (
   702              <DataLoader load={() => menuItems}>
   703                  {items => (
   704                      <ul>
   705                          {items.map((item, i) => (
   706                              <li
   707                                  className={classNames('application-details__action-menu', {disabled: item.disabled})}
   708                                  key={i}
   709                                  onClick={e => {
   710                                      e.stopPropagation();
   711                                      if (!item.disabled) {
   712                                          item.action();
   713                                          document.body.click();
   714                                      }
   715                                  }}>
   716                                  {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   717                              </li>
   718                          ))}
   719                      </ul>
   720                  )}
   721              </DataLoader>
   722          );
   723      }
   724  
   725      private async deleteApplication() {
   726          await AppUtils.deleteApplication(this.props.match.params.name, this.appContext.apis);
   727      }
   728  
   729      private getResourceTabs(application: appModels.Application, node: appModels.ResourceNode, state: appModels.State, events: appModels.Event[], tabs: Tab[]) {
   730          if (state) {
   731              const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0);
   732              tabs.push({
   733                  title: 'EVENTS',
   734                  badge: (numErrors > 0 && numErrors) || null,
   735                  key: 'events',
   736                  content: (
   737                      <div className='application-resource-events'>
   738                          <EventsList events={events} />
   739                      </div>
   740                  )
   741              });
   742          }
   743          if (node.kind === 'Pod' && state) {
   744              const containerGroups = [
   745                  {
   746                      offset: 0,
   747                      title: 'INIT CONTAINERS',
   748                      containers: state.spec.initContainers || []
   749                  },
   750                  {
   751                      offset: (state.spec.initContainers || []).length,
   752                      title: 'CONTAINERS',
   753                      containers: state.spec.containers || []
   754                  }
   755              ];
   756              tabs = tabs.concat([
   757                  {
   758                      key: 'logs',
   759                      title: 'LOGS',
   760                      content: (
   761                          <div className='application-details__tab-content-full-height'>
   762                              <div className='row'>
   763                                  <div className='columns small-3 medium-2'>
   764                                      {containerGroups.map(group => (
   765                                          <div key={group.title} style={{marginBottom: '1em'}}>
   766                                              {group.containers.length > 0 && <p>{group.title}:</p>}
   767                                              {group.containers.map((container: any, i: number) => (
   768                                                  <div
   769                                                      className='application-details__container'
   770                                                      key={container.name}
   771                                                      onClick={() => this.selectNode(this.selectedNodeKey, group.offset + i, 'logs')}>
   772                                                      {group.offset + i === this.selectedNodeInfo.container && <i className='fa fa-angle-right' />}
   773                                                      <span title={container.name}>{container.name}</span>
   774                                                  </div>
   775                                              ))}
   776                                          </div>
   777                                      ))}
   778                                  </div>
   779                                  <div className='columns small-9 medium-10'>
   780                                      <PodsLogsViewer pod={state} applicationName={application.metadata.name} containerIndex={this.selectedNodeInfo.container} />
   781                                  </div>
   782                              </div>
   783                          </div>
   784                      )
   785                  }
   786              ]);
   787          }
   788          return tabs;
   789      }
   790  }