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

     1  import {DropDownMenu, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
     2  import * as classNames from 'classnames';
     3  import * as PropTypes from 'prop-types';
     4  import * as React from 'react';
     5  import * as ReactDOM from 'react-dom';
     6  import * as models from '../../../shared/models';
     7  import {RouteComponentProps} from 'react-router';
     8  import {BehaviorSubject, combineLatest, from, merge, Observable} from 'rxjs';
     9  import {delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
    10  
    11  import {DataLoader, EmptyState, ErrorNotification, ObservableQuery, Page, Paginate, Revision, Timestamp, CheckboxField} from '../../../shared/components';
    12  import {AppContext, ContextApis} from '../../../shared/context';
    13  import * as appModels from '../../../shared/models';
    14  import {AppDetailsPreferences, AppsDetailsViewKey, AppsDetailsViewType, services} from '../../../shared/services';
    15  
    16  import {ApplicationConditions} from '../application-conditions/application-conditions';
    17  import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history';
    18  import {ApplicationOperationState} from '../application-operation-state/application-operation-state';
    19  import {PodGroupType, PodView} from '../application-pod-view/pod-view';
    20  import {ApplicationResourceTree, ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
    21  import {ApplicationStatusPanel} from '../application-status-panel/application-status-panel';
    22  import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
    23  import {ResourceDetails} from '../resource-details/resource-details';
    24  import * as AppUtils from '../utils';
    25  import {ApplicationResourceList} from './application-resource-list';
    26  import {Filters, FiltersProps} from './application-resource-filter';
    27  import {getAppDefaultSource, getAppCurrentVersion, urlPattern} from '../utils';
    28  import {ChartDetails, OCIMetadata, ResourceStatus} from '../../../shared/models';
    29  import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown';
    30  import {useSidebarTarget} from '../../../sidebar/sidebar';
    31  
    32  import './application-details.scss';
    33  import {TopBarActionMenuExt, AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service';
    34  import {ApplicationHydrateOperationState} from '../application-hydrate-operation-state/application-hydrate-operation-state';
    35  
    36  interface ApplicationDetailsState {
    37      page: number;
    38      revision?: string; // Which type of revision panelto show SYNC_STATUS_REVISION or OPERATION_STATE_REVISION
    39      groupedResources?: ResourceStatus[];
    40      slidingPanelPage?: number;
    41      filteredGraph?: any[];
    42      truncateNameOnRight?: boolean;
    43      showFullNodeName?: boolean;
    44      collapsedNodes?: string[];
    45      extensions?: AppViewExtension[];
    46      extensionsMap?: {[key: string]: AppViewExtension};
    47      statusExtensions?: StatusPanelExtension[];
    48      statusExtensionsMap?: {[key: string]: StatusPanelExtension};
    49      topBarActionMenuExts?: TopBarActionMenuExt[];
    50      topBarActionMenuExtsMap?: {[key: string]: TopBarActionMenuExt};
    51  }
    52  
    53  interface FilterInput {
    54      name: string[];
    55      kind: string[];
    56      health: string[];
    57      sync: string[];
    58      namespace: string[];
    59  }
    60  
    61  const ApplicationDetailsFilters = (props: FiltersProps) => {
    62      const sidebarTarget = useSidebarTarget();
    63      return ReactDOM.createPortal(<Filters {...props} />, sidebarTarget?.current);
    64  };
    65  
    66  export const NodeInfo = (node?: string): {key: string; container: number} => {
    67      const nodeContainer = {key: '', container: 0};
    68      if (node) {
    69          const parts = node.split('/');
    70          nodeContainer.key = parts.slice(0, 4).join('/');
    71          nodeContainer.container = parseInt(parts[4] || '0', 10);
    72      }
    73      return nodeContainer;
    74  };
    75  
    76  export const SelectNode = (fullName: string, containerIndex = 0, tab: string = null, appContext: ContextApis) => {
    77      const node = fullName ? `${fullName}/${containerIndex}` : null;
    78      appContext.navigation.goto('.', {node, tab}, {replace: true});
    79  };
    80  
    81  export class ApplicationDetails extends React.Component<RouteComponentProps<{appnamespace: string; name: string}>, ApplicationDetailsState> {
    82      public static contextTypes = {
    83          apis: PropTypes.object
    84      };
    85  
    86      private appChanged = new BehaviorSubject<appModels.Application>(null);
    87  
    88      constructor(props: RouteComponentProps<{appnamespace: string; name: string}>) {
    89          super(props);
    90          this.state = {
    91              page: 0,
    92              groupedResources: [],
    93              slidingPanelPage: 0,
    94              filteredGraph: [],
    95              truncateNameOnRight: false,
    96              showFullNodeName: false,
    97              collapsedNodes: [],
    98              ...this.getExtensionsState()
    99          };
   100      }
   101  
   102      public componentDidMount() {
   103          services.extensions.addEventListener('resource', this.onExtensionsUpdate);
   104          services.extensions.addEventListener('appView', this.onExtensionsUpdate);
   105          services.extensions.addEventListener('statusPanel', this.onExtensionsUpdate);
   106          services.extensions.addEventListener('topBar', this.onExtensionsUpdate);
   107      }
   108  
   109      public componentWillUnmount() {
   110          services.extensions.removeEventListener('resource', this.onExtensionsUpdate);
   111          services.extensions.removeEventListener('appView', this.onExtensionsUpdate);
   112          services.extensions.removeEventListener('statusPanel', this.onExtensionsUpdate);
   113          services.extensions.removeEventListener('topBar', this.onExtensionsUpdate);
   114      }
   115  
   116      private getAppNamespace() {
   117          if (typeof this.props.match.params.appnamespace === 'undefined') {
   118              return '';
   119          }
   120          return this.props.match.params.appnamespace;
   121      }
   122  
   123      private onExtensionsUpdate = () => {
   124          this.setState({...this.state, ...this.getExtensionsState()});
   125      };
   126  
   127      private getExtensionsState = () => {
   128          const extensions = services.extensions.getAppViewExtensions();
   129          const extensionsMap: {[key: string]: AppViewExtension} = {};
   130          extensions.forEach(ext => {
   131              extensionsMap[ext.title] = ext;
   132          });
   133          const statusExtensions = services.extensions.getStatusPanelExtensions();
   134          const statusExtensionsMap: {[key: string]: StatusPanelExtension} = {};
   135          statusExtensions.forEach(ext => {
   136              statusExtensionsMap[ext.id] = ext;
   137          });
   138          const topBarActionMenuExts = services.extensions.getActionMenuExtensions();
   139          const topBarActionMenuExtsMap: {[key: string]: TopBarActionMenuExt} = {};
   140          topBarActionMenuExts.forEach(ext => {
   141              topBarActionMenuExtsMap[ext.id] = ext;
   142          });
   143          return {extensions, extensionsMap, statusExtensions, statusExtensionsMap, topBarActionMenuExts, topBarActionMenuExtsMap};
   144      };
   145  
   146      private get showHydrateOperationState() {
   147          return new URLSearchParams(this.props.history.location.search).get('hydrateOperation') === 'true';
   148      }
   149  
   150      private get showOperationState() {
   151          return new URLSearchParams(this.props.history.location.search).get('operation') === 'true';
   152      }
   153  
   154      private setNodeExpansion(node: string, isExpanded: boolean) {
   155          const index = this.state.collapsedNodes.indexOf(node);
   156          if (isExpanded && index >= 0) {
   157              this.state.collapsedNodes.splice(index, 1);
   158              const updatedNodes = this.state.collapsedNodes.slice();
   159              this.setState({collapsedNodes: updatedNodes});
   160          } else if (!isExpanded && index < 0) {
   161              const updatedNodes = this.state.collapsedNodes.slice();
   162              updatedNodes.push(node);
   163              this.setState({collapsedNodes: updatedNodes});
   164          }
   165      }
   166  
   167      private getNodeExpansion(node: string): boolean {
   168          return this.state.collapsedNodes.indexOf(node) < 0;
   169      }
   170  
   171      private get showConditions() {
   172          return new URLSearchParams(this.props.history.location.search).get('conditions') === 'true';
   173      }
   174  
   175      private get selectedRollbackDeploymentIndex() {
   176          return parseInt(new URLSearchParams(this.props.history.location.search).get('rollback'), 10);
   177      }
   178  
   179      private get selectedNodeInfo() {
   180          return NodeInfo(new URLSearchParams(this.props.history.location.search).get('node'));
   181      }
   182  
   183      private get selectedNodeKey() {
   184          const nodeContainer = this.selectedNodeInfo;
   185          return nodeContainer.key;
   186      }
   187  
   188      private get selectedExtension() {
   189          return new URLSearchParams(this.props.history.location.search).get('extension');
   190      }
   191  
   192      private closeGroupedNodesPanel() {
   193          this.setState({groupedResources: []});
   194          this.setState({slidingPanelPage: 0});
   195      }
   196  
   197      private toggleCompactView(appName: string, pref: AppDetailsPreferences) {
   198          pref.userHelpTipMsgs = pref.userHelpTipMsgs.map(usrMsg => (usrMsg.appName === appName && usrMsg.msgKey === 'groupNodes' ? {...usrMsg, display: true} : usrMsg));
   199          services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: !pref.groupNodes}});
   200      }
   201  
   202      private getPageTitle(view: string) {
   203          const {Tree, Pods, Network, List} = AppsDetailsViewKey;
   204          switch (view) {
   205              case Tree:
   206                  return 'Application Details Tree';
   207              case Network:
   208                  return 'Application Details Network';
   209              case Pods:
   210                  return 'Application Details Pods';
   211              case List:
   212                  return 'Application Details List';
   213          }
   214          return '';
   215      }
   216  
   217      private getContent(application: models.Application, source: models.ApplicationSource, revisions: string[], revision: string) {
   218          const renderCommitMessage = (message: string) =>
   219              message.split(/\s/).map(part =>
   220                  urlPattern.test(part) ? (
   221                      <a href={part} target='_blank' rel='noopener noreferrer' style={{overflowWrap: 'anywhere', wordBreak: 'break-word'}}>
   222                          {part}{' '}
   223                      </a>
   224                  ) : (
   225                      part + ' '
   226                  )
   227              );
   228  
   229          const getContentForOci = (
   230              aRevision: string,
   231              aSourceIndex: number | null,
   232              aVersionId: number | null,
   233              indx: number,
   234              aSource: models.ApplicationSource,
   235              sourceHeader?: JSX.Element
   236          ) => {
   237              const showChartNonMetadataInfo = (aRevision: string, aRepoUrl: string) => {
   238                  return (
   239                      <>
   240                          <div className='row white-box__details-row'>
   241                              <div className='columns small-3'>Revision:</div>
   242                              <div className='columns small-9'>{aRevision}</div>
   243                          </div>
   244                          <div className='row white-box__details-row'>
   245                              <div className='columns small-3'>OCI Image:</div>
   246                              <div className='columns small-9'>{aRepoUrl}</div>
   247                          </div>
   248                      </>
   249                  );
   250              };
   251              return (
   252                  <DataLoader
   253                      key={indx}
   254                      input={application}
   255                      load={input => services.applications.ociMetadata(input.metadata.name, input.metadata.namespace, aRevision, aSourceIndex, aVersionId)}>
   256                      {(m: OCIMetadata) => {
   257                          return m ? (
   258                              <div className='white-box' style={{marginTop: '1.5em'}}>
   259                                  {sourceHeader && sourceHeader}
   260                                  <div className='white-box__details'>
   261                                      {showChartNonMetadataInfo(aRevision, aSource.repoURL)}
   262                                      {m.description && (
   263                                          <div className='row white-box__details-row'>
   264                                              <div className='columns small-3'>Description:</div>
   265                                              <div className='columns small-9'>{m.description}</div>
   266                                          </div>
   267                                      )}
   268                                      {m.authors && m.authors.length > 0 && (
   269                                          <div className='row white-box__details-row'>
   270                                              <div className='columns small-3'>Maintainers:</div>
   271                                              <div className='columns small-9'>{m.authors}</div>
   272                                          </div>
   273                                      )}
   274                                  </div>
   275                              </div>
   276                          ) : (
   277                              <div key={indx} className='white-box' style={{marginTop: '1.5em'}}>
   278                                  <div>Source {indx + 1}</div>
   279                                  <div className='white-box__details'>
   280                                      {showChartNonMetadataInfo(aRevision, aSource.repoURL)}
   281                                      <div className='row white-box__details-row'>
   282                                          <div className='columns small-3'>Helm Chart:</div>
   283                                          <div className='columns small-9'>
   284                                              {aSource.chart}&nbsp;
   285                                              {
   286                                                  <a
   287                                                      title={sources[indx].chart}
   288                                                      onClick={e => {
   289                                                          e.stopPropagation();
   290                                                          window.open(aSource.repoURL);
   291                                                      }}>
   292                                                      <i className='fa fa-external-link-alt' />
   293                                                  </a>
   294                                              }
   295                                          </div>
   296                                      </div>
   297                                  </div>
   298                              </div>
   299                          );
   300                      }}
   301                  </DataLoader>
   302              );
   303          };
   304  
   305          const getContentForChart = (
   306              aRevision: string,
   307              aSourceIndex: number | null,
   308              aVersionId: number | null,
   309              indx: number,
   310              aSource: models.ApplicationSource,
   311              sourceHeader?: JSX.Element
   312          ) => {
   313              const showChartNonMetadataInfo = (aRevision: string, aRepoUrl: string) => {
   314                  return (
   315                      <>
   316                          <div className='row white-box__details-row'>
   317                              <div className='columns small-3'>Revision:</div>
   318                              <div className='columns small-9'>{aRevision}</div>
   319                          </div>
   320                          <div className='row white-box__details-row'>
   321                              <div className='columns small-3'>Chart Source:</div>
   322                              <div className='columns small-9'>{aRepoUrl}</div>
   323                          </div>
   324                      </>
   325                  );
   326              };
   327              return (
   328                  <DataLoader
   329                      key={indx}
   330                      input={application}
   331                      load={input => services.applications.revisionChartDetails(input.metadata.name, input.metadata.namespace, aRevision, aSourceIndex, aVersionId)}>
   332                      {(m: ChartDetails) => {
   333                          return m ? (
   334                              <div className='white-box' style={{marginTop: '1.5em'}}>
   335                                  {sourceHeader && sourceHeader}
   336                                  <div className='white-box__details'>
   337                                      {showChartNonMetadataInfo(aRevision, aSource.repoURL)}
   338                                      <div className='row white-box__details-row'>
   339                                          <div className='columns small-3'>Helm Chart:</div>
   340                                          <div className='columns small-9'>
   341                                              {aSource.chart}&nbsp;
   342                                              {m.home && (
   343                                                  <a
   344                                                      title={m.home}
   345                                                      onClick={e => {
   346                                                          e.stopPropagation();
   347                                                          window.open(m.home);
   348                                                      }}>
   349                                                      <i className='fa fa-external-link-alt' />
   350                                                  </a>
   351                                              )}
   352                                          </div>
   353                                      </div>
   354                                      {m.description && (
   355                                          <div className='row white-box__details-row'>
   356                                              <div className='columns small-3'>Description:</div>
   357                                              <div className='columns small-9'>{m.description}</div>
   358                                          </div>
   359                                      )}
   360                                      {m.maintainers && m.maintainers.length > 0 && (
   361                                          <div className='row white-box__details-row'>
   362                                              <div className='columns small-3'>Maintainers:</div>
   363                                              <div className='columns small-9'>{m.maintainers.join(', ')}</div>
   364                                          </div>
   365                                      )}
   366                                  </div>
   367                              </div>
   368                          ) : (
   369                              <div key={indx} className='white-box' style={{marginTop: '1.5em'}}>
   370                                  <div>Source {indx + 1}</div>
   371                                  <div className='white-box__details'>
   372                                      {showChartNonMetadataInfo(aRevision, aSource.repoURL)}
   373                                      <div className='row white-box__details-row'>
   374                                          <div className='columns small-3'>Helm Chart:</div>
   375                                          <div className='columns small-9'>
   376                                              {aSource.chart}&nbsp;
   377                                              {
   378                                                  <a
   379                                                      title={sources[indx].chart}
   380                                                      onClick={e => {
   381                                                          e.stopPropagation();
   382                                                          window.open(aSource.repoURL);
   383                                                      }}>
   384                                                      <i className='fa fa-external-link-alt' />
   385                                                  </a>
   386                                              }
   387                                          </div>
   388                                      </div>
   389                                  </div>
   390                              </div>
   391                          );
   392                      }}
   393                  </DataLoader>
   394              );
   395          };
   396  
   397          const getContentForNonChart = (
   398              aRevision: string,
   399              aSourceIndex: number,
   400              aVersionId: number | null,
   401              indx: number,
   402              aSource: models.ApplicationSource,
   403              sourceHeader?: JSX.Element
   404          ) => {
   405              const showNonMetadataInfo = (aSource: models.ApplicationSource, aRevision: string) => {
   406                  return (
   407                      <>
   408                          <div className='white-box__details'>
   409                              <div className='row white-box__details-row'>
   410                                  <div className='columns small-3'>SHA:</div>
   411                                  <div className='columns small-9'>
   412                                      <Revision repoUrl={aSource.repoURL} revision={aRevision} />
   413                                  </div>
   414                              </div>
   415                          </div>
   416                          <div className='white-box__details'>
   417                              <div className='row white-box__details-row'>
   418                                  <div className='columns small-3'>Source:</div>
   419                                  <div className='columns small-9'>{aSource.repoURL}</div>
   420                              </div>
   421                          </div>
   422                      </>
   423                  );
   424              };
   425              return (
   426                  <DataLoader
   427                      key={indx}
   428                      load={() => services.applications.revisionMetadata(application.metadata.name, application.metadata.namespace, aRevision, aSourceIndex, aVersionId)}>
   429                      {metadata =>
   430                          metadata ? (
   431                              <div key={indx} className='white-box' style={{marginTop: '1.5em'}}>
   432                                  {sourceHeader && sourceHeader}
   433                                  {showNonMetadataInfo(aSource, aRevision)}
   434                                  <div className='white-box__details'>
   435                                      <div className='row white-box__details-row'>
   436                                          <div className='columns small-3'>Date:</div>
   437                                          <div className='columns small-9'>
   438                                              <Timestamp date={metadata.date} />
   439                                          </div>
   440                                      </div>
   441                                  </div>
   442                                  <div className='white-box__details'>
   443                                      <div className='row white-box__details-row'>
   444                                          <div className='columns small-3'>Tags:</div>
   445                                          <div className='columns small-9'>{((metadata.tags || []).length > 0 && metadata.tags.join(', ')) || 'No tags'}</div>
   446                                      </div>
   447                                  </div>
   448                                  <div className='white-box__details'>
   449                                      <div className='row white-box__details-row'>
   450                                          <div className='columns small-3'>Author:</div>
   451                                          <div className='columns small-9'>{metadata.author}</div>
   452                                      </div>
   453                                  </div>
   454                                  <div className='white-box__details'>
   455                                      <div className='row white-box__details-row'>
   456                                          <div className='columns small-3'>Message:</div>
   457                                          <div className='columns small-9' style={{display: 'flex', alignItems: 'center'}}>
   458                                              <div className='application-details__commit-message'>{renderCommitMessage(metadata.message)}</div>
   459                                          </div>
   460                                      </div>
   461                                  </div>
   462                              </div>
   463                          ) : (
   464                              <div key={indx} className='white-box' style={{marginTop: '1.5em'}}>
   465                                  <div>Source {indx + 1}</div>
   466                                  {showNonMetadataInfo(aSource, aRevision)}
   467                              </div>
   468                          )
   469                      }
   470                  </DataLoader>
   471              );
   472          };
   473          const cont: JSX.Element[] = [];
   474          const sources: models.ApplicationSource[] = application.spec.sources;
   475          if (sources?.length > 0 && revisions) {
   476              revisions.forEach((rev, indx) => {
   477                  if (sources[indx].repoURL.startsWith('oci://')) {
   478                      cont.push(getContentForOci(rev, indx, getAppCurrentVersion(application), indx, sources[indx], <div>Source {indx + 1}</div>));
   479                  } else if (sources[indx].chart) {
   480                      cont.push(getContentForChart(rev, indx, getAppCurrentVersion(application), indx, sources[indx], <div>Source {indx + 1}</div>));
   481                  } else {
   482                      cont.push(getContentForNonChart(rev, indx, getAppCurrentVersion(application), indx, sources[indx], <div>Source {indx + 1}</div>));
   483                  }
   484              });
   485              return <>{cont}</>;
   486          } else if (application.spec.source) {
   487              if (source.repoURL.startsWith('oci://')) {
   488                  cont.push(getContentForOci(revision, null, getAppCurrentVersion(application), 0, source));
   489              } else if (source.chart) {
   490                  cont.push(getContentForChart(revision, null, null, 0, source));
   491              } else {
   492                  cont.push(getContentForNonChart(revision, null, getAppCurrentVersion(application), 0, source));
   493              }
   494              return <>{cont}</>;
   495          } else {
   496              return (
   497                  <div className='white-box' style={{marginTop: '1.5em'}}>
   498                      <div className='white-box__details'>
   499                          <div className='row white-box__details-row'>
   500                              <div className='columns small-9'>No other information available</div>
   501                          </div>
   502                      </div>
   503                  </div>
   504              );
   505          }
   506      }
   507  
   508      public render() {
   509          return (
   510              <ObservableQuery>
   511                  {q => (
   512                      <DataLoader
   513                          errorRenderer={error => <Page title='Application Details'>{error}</Page>}
   514                          loadingRenderer={() => <Page title='Application Details'>Loading...</Page>}
   515                          input={this.props.match.params.name}
   516                          load={name =>
   517                              combineLatest([this.loadAppInfo(name, this.getAppNamespace()), services.viewPreferences.getPreferences(), q]).pipe(
   518                                  map(items => {
   519                                      const application = items[0].application;
   520                                      const pref = items[1].appDetails;
   521                                      const params = items[2];
   522                                      if (params.get('resource') != null) {
   523                                          pref.resourceFilter = params
   524                                              .get('resource')
   525                                              .split(',')
   526                                              .filter(item => !!item);
   527                                      }
   528                                      if (params.get('view') != null) {
   529                                          pref.view = params.get('view') as AppsDetailsViewType;
   530                                      } else {
   531                                          const appDefaultView = (application.metadata &&
   532                                              application.metadata.annotations &&
   533                                              application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType;
   534                                          if (appDefaultView != null) {
   535                                              pref.view = appDefaultView;
   536                                          }
   537                                      }
   538                                      if (params.get('orphaned') != null) {
   539                                          pref.orphanedResources = params.get('orphaned') === 'true';
   540                                      }
   541                                      if (params.get('podSortMode') != null) {
   542                                          pref.podView.sortMode = params.get('podSortMode') as PodGroupType;
   543                                      } else {
   544                                          const appDefaultPodSort = (application.metadata &&
   545                                              application.metadata.annotations &&
   546                                              application.metadata.annotations[appModels.AnnotationDefaultPodSort]) as PodGroupType;
   547                                          if (appDefaultPodSort != null) {
   548                                              pref.podView.sortMode = appDefaultPodSort;
   549                                          }
   550                                      }
   551                                      return {...items[0], pref};
   552                                  })
   553                              )
   554                          }>
   555                          {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => {
   556                              tree.nodes = tree.nodes || [];
   557                              const treeFilter = this.getTreeFilter(pref.resourceFilter);
   558                              const setFilter = (items: string[]) => {
   559                                  this.appContext.apis.navigation.goto('.', {resource: items.join(',')}, {replace: true});
   560                                  services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
   561                              };
   562                              const clearFilter = () => setFilter([]);
   563                              const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
   564                              const appNodesByName = this.groupAppNodesByKey(application, tree);
   565                              const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null;
   566                              const isAppSelected = selectedItem === application;
   567                              const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode);
   568                              const operationState = application.status.operationState;
   569                              const hydrateOperationState = application.status.sourceHydrator.currentOperation;
   570                              const conditions = application.status.conditions || [];
   571                              const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy');
   572                              const tab = new URLSearchParams(this.props.history.location.search).get('tab');
   573                              const source = getAppDefaultSource(application);
   574                              const showToolTip = pref?.userHelpTipMsgs.find(usrMsg => usrMsg.appName === application.metadata.name);
   575                              const resourceNodes = (): any[] => {
   576                                  const statusByKey = new Map<string, models.ResourceStatus>();
   577                                  application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res));
   578                                  const resources = new Map<string, any>();
   579                                  tree.nodes
   580                                      .map(node => ({...node, orphaned: false}))
   581                                      .concat(((pref.orphanedResources && tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
   582                                      .forEach(node => {
   583                                          const resource: any = {...node};
   584                                          resource.uid = node.uid;
   585                                          const status = statusByKey.get(AppUtils.nodeKey(node));
   586                                          if (status) {
   587                                              resource.health = status.health;
   588                                              resource.status = status.status;
   589                                              resource.hook = status.hook;
   590                                              resource.syncWave = status.syncWave;
   591                                              resource.requiresPruning = status.requiresPruning;
   592                                          }
   593                                          resources.set(node.uid || AppUtils.nodeKey(node), resource);
   594                                      });
   595                                  const resourcesRef = Array.from(resources.values());
   596                                  return resourcesRef;
   597                              };
   598  
   599                              const filteredRes = resourceNodes().filter(res => {
   600                                  const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''};
   601                                  resNode.root = resNode;
   602                                  return this.filterTreeNode(resNode, treeFilter);
   603                              });
   604                              const openGroupNodeDetails = (groupdedNodeIds: string[]) => {
   605                                  const resources = resourceNodes();
   606                                  this.setState({
   607                                      groupedResources: groupdedNodeIds
   608                                          ? resources.filter(res => groupdedNodeIds.includes(res.uid) || groupdedNodeIds.includes(AppUtils.nodeKey(res)))
   609                                          : []
   610                                  });
   611                              };
   612                              const {Tree, Pods, Network, List} = AppsDetailsViewKey;
   613                              const zoomNum = (pref.zoom * 100).toFixed(0);
   614                              const setZoom = (s: number) => {
   615                                  let targetZoom: number = pref.zoom + s;
   616                                  if (targetZoom <= 0.05) {
   617                                      targetZoom = 0.1;
   618                                  } else if (targetZoom > 2.0) {
   619                                      targetZoom = 2.0;
   620                                  }
   621                                  services.viewPreferences.updatePreferences({appDetails: {...pref, zoom: targetZoom}});
   622                              };
   623                              const setFilterGraph = (filterGraph: any[]) => {
   624                                  this.setState({filteredGraph: filterGraph});
   625                              };
   626                              const setShowCompactNodes = (showCompactView: boolean) => {
   627                                  services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: showCompactView}});
   628                              };
   629                              const updateHelpTipState = (usrHelpTip: models.UserMessages) => {
   630                                  const existingIndex = pref.userHelpTipMsgs.findIndex(msg => msg.appName === usrHelpTip.appName && msg.msgKey === usrHelpTip.msgKey);
   631                                  if (existingIndex !== -1) {
   632                                      pref.userHelpTipMsgs[existingIndex] = usrHelpTip;
   633                                  } else {
   634                                      (pref.userHelpTipMsgs || []).push(usrHelpTip);
   635                                  }
   636                              };
   637                              const toggleNodeName = () => {
   638                                  this.setState({showFullNodeName: !this.state.showFullNodeName});
   639                              };
   640                              const toggleNameDirection = () => {
   641                                  this.setState({truncateNameOnRight: !this.state.truncateNameOnRight});
   642                              };
   643                              const expandAll = () => {
   644                                  this.setState({collapsedNodes: []});
   645                              };
   646                              const collapseAll = () => {
   647                                  const nodes = new Array<ResourceTreeNode>();
   648                                  tree.nodes
   649                                      .map(node => ({...node, orphaned: false}))
   650                                      .concat((tree.orphanedNodes || []).map(node => ({...node, orphaned: true})))
   651                                      .forEach(node => {
   652                                          const resourceNode: ResourceTreeNode = {...node};
   653                                          nodes.push(resourceNode);
   654                                      });
   655                                  const collapsedNodesList = this.state.collapsedNodes.slice();
   656                                  if (pref.view === 'network') {
   657                                      const networkNodes = nodes.filter(node => node.networkingInfo);
   658                                      networkNodes.forEach(parent => {
   659                                          const parentId = parent.uid;
   660                                          if (collapsedNodesList.indexOf(parentId) < 0) {
   661                                              collapsedNodesList.push(parentId);
   662                                          }
   663                                      });
   664                                      this.setState({collapsedNodes: collapsedNodesList});
   665                                  } else {
   666                                      const managedKeys = new Set(application.status.resources.map(AppUtils.nodeKey));
   667                                      nodes.forEach(node => {
   668                                          if (!((node.parentRefs || []).length === 0 || managedKeys.has(AppUtils.nodeKey(node)))) {
   669                                              node.parentRefs.forEach(parent => {
   670                                                  const parentId = parent.uid;
   671                                                  if (collapsedNodesList.indexOf(parentId) < 0) {
   672                                                      collapsedNodesList.push(parentId);
   673                                                  }
   674                                              });
   675                                          }
   676                                      });
   677                                      collapsedNodesList.push(application.kind + '-' + application.metadata.namespace + '-' + application.metadata.name);
   678                                      this.setState({collapsedNodes: collapsedNodesList});
   679                                  }
   680                              };
   681                              const appFullName = AppUtils.nodeKey({
   682                                  group: 'argoproj.io',
   683                                  kind: application.kind,
   684                                  name: application.metadata.name,
   685                                  namespace: application.metadata.namespace
   686                              });
   687  
   688                              const activeStatusExt = this.state.statusExtensionsMap[this.selectedExtension];
   689                              const activeTopBarActionMenuExt = this.state.topBarActionMenuExtsMap[this.selectedExtension];
   690  
   691                              return (
   692                                  <div className={`application-details ${this.props.match.params.name}`}>
   693                                      <Page
   694                                          title={this.props.match.params.name + ' - ' + this.getPageTitle(pref.view)}
   695                                          useTitleOnly={true}
   696                                          topBarTitle={this.getPageTitle(pref.view)}
   697                                          toolbar={{
   698                                              breadcrumbs: [
   699                                                  {title: 'Applications', path: '/applications'},
   700                                                  {title: <ApplicationsDetailsAppDropdown appName={this.props.match.params.name} />}
   701                                              ],
   702                                              actionMenu: {
   703                                                  items: [
   704                                                      ...this.getApplicationActionMenu(application, true),
   705                                                      ...(this.state.topBarActionMenuExts
   706                                                          ?.filter(ext => ext.shouldDisplay?.(application))
   707                                                          .map(ext => this.renderActionMenuItem(ext, tree, application, this.setExtensionPanelVisible)) || [])
   708                                                  ]
   709                                              },
   710                                              tools: (
   711                                                  <React.Fragment key='app-list-tools'>
   712                                                      <div className='application-details__view-type'>
   713                                                          <i
   714                                                              className={classNames('fa fa-sitemap', {selected: pref.view === Tree})}
   715                                                              title='Tree'
   716                                                              onClick={() => {
   717                                                                  this.appContext.apis.navigation.goto('.', {view: Tree});
   718                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Tree}});
   719                                                              }}
   720                                                          />
   721                                                          <i
   722                                                              className={classNames('fa fa-th', {selected: pref.view === Pods})}
   723                                                              title='Pods'
   724                                                              onClick={() => {
   725                                                                  this.appContext.apis.navigation.goto('.', {view: Pods});
   726                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}});
   727                                                              }}
   728                                                          />
   729                                                          <i
   730                                                              className={classNames('fa fa-network-wired', {selected: pref.view === Network})}
   731                                                              title='Network'
   732                                                              onClick={() => {
   733                                                                  this.appContext.apis.navigation.goto('.', {view: Network});
   734                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}});
   735                                                              }}
   736                                                          />
   737                                                          <i
   738                                                              className={classNames('fa fa-th-list', {selected: pref.view === List})}
   739                                                              title='List'
   740                                                              onClick={() => {
   741                                                                  this.appContext.apis.navigation.goto('.', {view: List});
   742                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: List}});
   743                                                              }}
   744                                                          />
   745                                                          {this.state.extensions &&
   746                                                              (this.state.extensions || []).map(ext => (
   747                                                                  <i
   748                                                                      key={ext.title}
   749                                                                      className={classNames(`fa ${ext.icon}`, {selected: pref.view === ext.title})}
   750                                                                      title={ext.title}
   751                                                                      onClick={() => {
   752                                                                          this.appContext.apis.navigation.goto('.', {view: ext.title});
   753                                                                          services.viewPreferences.updatePreferences({appDetails: {...pref, view: ext.title}});
   754                                                                      }}
   755                                                                  />
   756                                                              ))}
   757                                                      </div>
   758                                                  </React.Fragment>
   759                                              )
   760                                          }}>
   761                                          <div className='application-details__wrapper'>
   762                                              <div className='application-details__status-panel'>
   763                                                  <ApplicationStatusPanel
   764                                                      application={application}
   765                                                      showDiff={() => this.selectNode(appFullName, 0, 'diff')}
   766                                                      showOperation={() => this.setOperationStatusVisible(true)}
   767                                                      showHydrateOperation={() => this.setHydrateOperationStatusVisible(true)}
   768                                                      showConditions={() => this.setConditionsStatusVisible(true)}
   769                                                      showExtension={id => this.setExtensionPanelVisible(id)}
   770                                                      showMetadataInfo={revision => this.setState({...this.state, revision})}
   771                                                  />
   772                                              </div>
   773                                              <div className='application-details__tree'>
   774                                                  {refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
   775                                                  {((pref.view === 'tree' || pref.view === 'network') && (
   776                                                      <>
   777                                                          <DataLoader load={() => services.viewPreferences.getPreferences()}>
   778                                                              {viewPref => (
   779                                                                  <ApplicationDetailsFilters
   780                                                                      pref={pref}
   781                                                                      tree={tree}
   782                                                                      onSetFilter={setFilter}
   783                                                                      onClearFilter={clearFilter}
   784                                                                      collapsed={viewPref.hideSidebar}
   785                                                                      resourceNodes={this.state.filteredGraph}
   786                                                                  />
   787                                                              )}
   788                                                          </DataLoader>
   789                                                          <div className='graph-options-panel'>
   790                                                              <a
   791                                                                  className={`group-nodes-button`}
   792                                                                  onClick={() => {
   793                                                                      toggleNameDirection();
   794                                                                  }}
   795                                                                  title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
   796                                                                  <i
   797                                                                      className={classNames({
   798                                                                          'fa fa-align-right': this.state.truncateNameOnRight,
   799                                                                          'fa fa-align-left': !this.state.truncateNameOnRight
   800                                                                      })}
   801                                                                  />
   802                                                              </a>
   803                                                              <a
   804                                                                  className={`group-nodes-button`}
   805                                                                  onClick={() => {
   806                                                                      toggleNodeName();
   807                                                                  }}
   808                                                                  title={this.state.showFullNodeName ? 'Show wrapped resource name' : 'Show full resource name'}>
   809                                                                  <i
   810                                                                      className={classNames({
   811                                                                          'fa fa-expand': this.state.showFullNodeName,
   812                                                                          'fa fa-compress': !this.state.showFullNodeName
   813                                                                      })}
   814                                                                  />
   815                                                              </a>
   816                                                              {(pref.view === 'tree' || pref.view === 'network') && (
   817                                                                  <Tooltip
   818                                                                      content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
   819                                                                      visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
   820                                                                      duration={showToolTip?.duration}
   821                                                                      zIndex={1}>
   822                                                                      <a
   823                                                                          className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
   824                                                                          title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
   825                                                                          onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
   826                                                                          <i className={classNames('fa fa-object-group fa-fw')} />
   827                                                                      </a>
   828                                                                  </Tooltip>
   829                                                              )}
   830                                                              <span className={`separator`} />
   831                                                              <a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
   832                                                                  <i className='fa fa-plus fa-fw' />
   833                                                              </a>
   834                                                              <a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
   835                                                                  <i className='fa fa-minus fa-fw' />
   836                                                              </a>
   837                                                              <span className={`separator`} />
   838                                                              <span>
   839                                                                  <a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
   840                                                                      <i className='fa fa-search-plus fa-fw' />
   841                                                                  </a>
   842                                                                  <a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
   843                                                                      <i className='fa fa-search-minus fa-fw' />
   844                                                                  </a>
   845                                                                  <div className={`zoom-value`}>{zoomNum}%</div>
   846                                                              </span>
   847                                                          </div>
   848                                                          <ApplicationResourceTree
   849                                                              nodeFilter={node => this.filterTreeNode(node, treeFilter)}
   850                                                              selectedNodeFullName={this.selectedNodeKey}
   851                                                              onNodeClick={fullName => this.selectNode(fullName)}
   852                                                              nodeMenu={node =>
   853                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   854                                                                      this.getApplicationActionMenu(application, false)
   855                                                                  )
   856                                                              }
   857                                                              showCompactNodes={pref.groupNodes}
   858                                                              userMsgs={pref.userHelpTipMsgs}
   859                                                              tree={tree}
   860                                                              app={application}
   861                                                              showOrphanedResources={pref.orphanedResources}
   862                                                              useNetworkingHierarchy={pref.view === 'network'}
   863                                                              onClearFilter={clearFilter}
   864                                                              onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
   865                                                              zoom={pref.zoom}
   866                                                              podGroupCount={pref.podGroupCount}
   867                                                              appContext={this.appContext}
   868                                                              nameDirection={this.state.truncateNameOnRight}
   869                                                              nameWrap={this.state.showFullNodeName}
   870                                                              filters={pref.resourceFilter}
   871                                                              setTreeFilterGraph={setFilterGraph}
   872                                                              updateUsrHelpTipMsgs={updateHelpTipState}
   873                                                              setShowCompactNodes={setShowCompactNodes}
   874                                                              setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
   875                                                              getNodeExpansion={node => this.getNodeExpansion(node)}
   876                                                          />
   877                                                      </>
   878                                                  )) ||
   879                                                      (pref.view === 'pods' && (
   880                                                          <PodView
   881                                                              tree={tree}
   882                                                              app={application}
   883                                                              onItemClick={fullName => this.selectNode(fullName)}
   884                                                              nodeMenu={node =>
   885                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   886                                                                      this.getApplicationActionMenu(application, false)
   887                                                                  )
   888                                                              }
   889                                                              quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
   890                                                          />
   891                                                      )) ||
   892                                                      (this.state.extensionsMap[pref.view] != null && (
   893                                                          <ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
   894                                                      )) || (
   895                                                          <div>
   896                                                              <DataLoader load={() => services.viewPreferences.getPreferences()}>
   897                                                                  {viewPref => (
   898                                                                      <ApplicationDetailsFilters
   899                                                                          pref={pref}
   900                                                                          tree={tree}
   901                                                                          onSetFilter={setFilter}
   902                                                                          onClearFilter={clearFilter}
   903                                                                          collapsed={viewPref.hideSidebar}
   904                                                                          resourceNodes={filteredRes}
   905                                                                      />
   906                                                                  )}
   907                                                              </DataLoader>
   908                                                              {(filteredRes.length > 0 && (
   909                                                                  <Paginate
   910                                                                      page={this.state.page}
   911                                                                      data={filteredRes}
   912                                                                      onPageChange={page => this.setState({page})}
   913                                                                      preferencesKey='application-details'>
   914                                                                      {data => (
   915                                                                          <ApplicationResourceList
   916                                                                              pref={pref}
   917                                                                              onNodeClick={fullName => this.selectNode(fullName)}
   918                                                                              resources={data}
   919                                                                              nodeMenu={node =>
   920                                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   921                                                                                      this.getApplicationActionMenu(application, false)
   922                                                                                  )
   923                                                                              }
   924                                                                              tree={tree}
   925                                                                          />
   926                                                                      )}
   927                                                                  </Paginate>
   928                                                              )) || (
   929                                                                  <EmptyState icon='fa fa-search'>
   930                                                                      <h4>No resources found</h4>
   931                                                                      <h5>Try to change filter criteria</h5>
   932                                                                  </EmptyState>
   933                                                              )}
   934                                                          </div>
   935                                                      )}
   936                                              </div>
   937                                          </div>
   938                                          <SlidingPanel isShown={this.state.groupedResources.length > 0} onClose={() => this.closeGroupedNodesPanel()}>
   939                                              <div className='application-details__sliding-panel-pagination-wrap'>
   940                                                  <Paginate
   941                                                      page={this.state.slidingPanelPage}
   942                                                      data={this.state.groupedResources}
   943                                                      onPageChange={page => this.setState({slidingPanelPage: page})}
   944                                                      preferencesKey='grouped-nodes-details'>
   945                                                      {data => (
   946                                                          <ApplicationResourceList
   947                                                              pref={pref}
   948                                                              onNodeClick={fullName => this.selectNode(fullName)}
   949                                                              resources={data}
   950                                                              nodeMenu={node =>
   951                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   952                                                                      this.getApplicationActionMenu(application, false)
   953                                                                  )
   954                                                              }
   955                                                              tree={tree}
   956                                                          />
   957                                                      )}
   958                                                  </Paginate>
   959                                              </div>
   960                                          </SlidingPanel>
   961                                          <SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => this.selectNode('')}>
   962                                              <ResourceDetails
   963                                                  tree={tree}
   964                                                  application={application}
   965                                                  isAppSelected={isAppSelected}
   966                                                  updateApp={(app: models.Application, query: {validate?: boolean}) => this.updateApp(app, query)}
   967                                                  selectedNode={selectedNode}
   968                                                  appCxt={this.context}
   969                                                  tab={tab}
   970                                              />
   971                                          </SlidingPanel>
   972                                          <ApplicationSyncPanel
   973                                              application={application}
   974                                              hide={() => AppUtils.showDeploy(null, null, this.appContext.apis)}
   975                                              selectedResource={syncResourceKey}
   976                                          />
   977                                          <SlidingPanel isShown={this.selectedRollbackDeploymentIndex > -1} onClose={() => this.setRollbackPanelVisible(-1)}>
   978                                              {this.selectedRollbackDeploymentIndex > -1 && (
   979                                                  <ApplicationDeploymentHistory
   980                                                      app={application}
   981                                                      rollbackApp={info => this.rollbackApplication(info, application)}
   982                                                      selectDeployment={i => this.setRollbackPanelVisible(i)}
   983                                                  />
   984                                              )}
   985                                          </SlidingPanel>
   986                                          <SlidingPanel isShown={this.showOperationState && !!operationState} onClose={() => this.setOperationStatusVisible(false)}>
   987                                              {operationState && <ApplicationOperationState application={application} operationState={operationState} />}
   988                                          </SlidingPanel>
   989                                          <SlidingPanel
   990                                              isShown={this.showHydrateOperationState && !!hydrateOperationState}
   991                                              onClose={() => this.setHydrateOperationStatusVisible(false)}>
   992                                              {hydrateOperationState && <ApplicationHydrateOperationState hydrateOperationState={hydrateOperationState} />}
   993                                          </SlidingPanel>
   994                                          <SlidingPanel isShown={this.showConditions && !!conditions} onClose={() => this.setConditionsStatusVisible(false)}>
   995                                              {conditions && <ApplicationConditions conditions={conditions} />}
   996                                          </SlidingPanel>
   997                                          <SlidingPanel
   998                                              isShown={this.state.revision === 'SYNC_STATUS_REVISION' || this.state.revision === 'OPERATION_STATE_REVISION'}
   999                                              isMiddle={true}
  1000                                              onClose={() => this.setState({revision: null})}>
  1001                                              {this.state.revision === 'SYNC_STATUS_REVISION' &&
  1002                                                  (application.status.sync.revisions || application.status.sync.revision) &&
  1003                                                  this.getContent(application, source, application.status.sync.revisions, application.status.sync.revision)}
  1004                                              {this.state.revision === 'OPERATION_STATE_REVISION' &&
  1005                                                  (application.status.operationState.syncResult.revisions || application.status.operationState.syncResult.revision) &&
  1006                                                  this.getContent(
  1007                                                      application,
  1008                                                      source,
  1009                                                      application.status.operationState.syncResult.revisions,
  1010                                                      application.status.operationState.syncResult.revision
  1011                                                  )}
  1012                                          </SlidingPanel>
  1013                                          <SlidingPanel
  1014                                              isShown={this.selectedExtension !== '' && activeStatusExt != null && activeStatusExt.flyout != null}
  1015                                              onClose={() => this.setExtensionPanelVisible('')}>
  1016                                              {this.selectedExtension !== '' && activeStatusExt?.flyout && <activeStatusExt.flyout application={application} tree={tree} />}
  1017                                          </SlidingPanel>
  1018                                          <SlidingPanel
  1019                                              isMiddle={activeTopBarActionMenuExt?.isMiddle ?? true}
  1020                                              isShown={this.selectedExtension !== '' && activeTopBarActionMenuExt != null && activeTopBarActionMenuExt.flyout != null}
  1021                                              onClose={() => this.setExtensionPanelVisible('')}>
  1022                                              {this.selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && (
  1023                                                  <activeTopBarActionMenuExt.flyout application={application} tree={tree} />
  1024                                              )}
  1025                                          </SlidingPanel>
  1026                                      </Page>
  1027                                  </div>
  1028                              );
  1029                          }}
  1030                      </DataLoader>
  1031                  )}
  1032              </ObservableQuery>
  1033          );
  1034      }
  1035      private renderActionMenuItem(ext: TopBarActionMenuExt, tree: appModels.ApplicationTree, application: appModels.Application, showExtension?: (id: string) => any): any {
  1036          return {
  1037              action: () => this.setExtensionPanelVisible(ext.id),
  1038              title: <ext.component application={application} tree={tree} openFlyout={() => showExtension && showExtension(ext.id)} />,
  1039              iconClassName: ext.iconClassName
  1040          };
  1041      }
  1042      private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) {
  1043          const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey];
  1044          const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
  1045          const ActionMenuItem = (prop: {actionLabel: string}) => <span className={needOverlapLabelOnNarrowScreen ? 'show-for-large' : ''}>{prop.actionLabel}</span>;
  1046          return [
  1047              {
  1048                  iconClassName: 'fa fa-info-circle',
  1049                  title: <ActionMenuItem actionLabel='Details' />,
  1050                  action: () => this.selectNode(fullName),
  1051                  disabled: !app.spec.source && (!app.spec.sources || app.spec.sources.length === 0) && !app.spec.sourceHydrator
  1052              },
  1053              {
  1054                  iconClassName: 'fa fa-file-medical',
  1055                  title: <ActionMenuItem actionLabel='Diff' />,
  1056                  action: () => this.selectNode(fullName, 0, 'diff'),
  1057                  disabled:
  1058                      app.status.sync.status === appModels.SyncStatuses.Synced ||
  1059                      (!app.spec.source && (!app.spec.sources || app.spec.sources.length === 0) && !app.spec.sourceHydrator)
  1060              },
  1061              {
  1062                  iconClassName: 'fa fa-sync',
  1063                  title: <ActionMenuItem actionLabel='Sync' />,
  1064                  action: () => AppUtils.showDeploy('all', null, this.appContext.apis),
  1065                  disabled: !app.spec.source && (!app.spec.sources || app.spec.sources.length === 0) && !app.spec.sourceHydrator
  1066              },
  1067              ...(app.status?.operationState?.phase === 'Running' && app.status.resources.find(r => r.requiresDeletionConfirmation)
  1068                  ? [
  1069                        {
  1070                            iconClassName: 'fa fa-check',
  1071                            title: <ActionMenuItem actionLabel='Confirm Pruning' />,
  1072                            action: () => this.confirmDeletion(app, 'Confirm Prunning', 'Are you sure you want to confirm resources pruning?')
  1073                        }
  1074                    ]
  1075                  : []),
  1076              {
  1077                  iconClassName: 'fa fa-info-circle',
  1078                  title: <ActionMenuItem actionLabel='Sync Status' />,
  1079                  action: () => this.setOperationStatusVisible(true),
  1080                  disabled: !app.status.operationState
  1081              },
  1082              {
  1083                  iconClassName: 'fa fa-history',
  1084                  title: <ActionMenuItem actionLabel='History and rollback' />,
  1085                  action: () => {
  1086                      this.setRollbackPanelVisible(0);
  1087                  },
  1088                  disabled: !app.status.operationState
  1089              },
  1090              app.metadata.deletionTimestamp &&
  1091              app.status.resources.find(r => r.requiresDeletionConfirmation) &&
  1092              !((app.metadata.annotations || {})[appModels.AppDeletionConfirmedAnnotation] == 'true')
  1093                  ? {
  1094                        iconClassName: 'fa fa-check',
  1095                        title: <ActionMenuItem actionLabel='Confirm Deletion' />,
  1096                        action: () => this.confirmDeletion(app, 'Confirm Deletion', 'Are you sure you want to delete this application?')
  1097                    }
  1098                  : {
  1099                        iconClassName: 'fa fa-times-circle',
  1100                        title: <ActionMenuItem actionLabel='Delete' />,
  1101                        action: () => this.deleteApplication(),
  1102                        disabled: !!app.metadata.deletionTimestamp
  1103                    },
  1104              {
  1105                  iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}),
  1106                  title: (
  1107                      <React.Fragment>
  1108                          <ActionMenuItem actionLabel='Refresh' />{' '}
  1109                          <DropDownMenu
  1110                              items={[
  1111                                  {
  1112                                      title: 'Hard Refresh',
  1113                                      action: () => !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, 'hard')
  1114                                  }
  1115                              ]}
  1116                              anchor={() => <i className='fa fa-caret-down' />}
  1117                          />
  1118                      </React.Fragment>
  1119                  ),
  1120                  disabled: !!refreshing,
  1121                  action: () => {
  1122                      if (!refreshing) {
  1123                          services.applications.get(app.metadata.name, app.metadata.namespace, 'normal');
  1124                          AppUtils.setAppRefreshing(app);
  1125                          this.appChanged.next(app);
  1126                      }
  1127                  }
  1128              }
  1129          ];
  1130      }
  1131  
  1132      private filterTreeNode(node: ResourceTreeNode, filterInput: FilterInput): boolean {
  1133          const syncStatuses = filterInput.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []);
  1134  
  1135          const root = node.root || ({} as ResourceTreeNode);
  1136          const hook = root && root.hook;
  1137          if (
  1138              (filterInput.name.length === 0 || this.nodeNameMatchesWildcardFilters(node.name, filterInput.name)) &&
  1139              (filterInput.kind.length === 0 || filterInput.kind.indexOf(node.kind) > -1) &&
  1140              // include if node's root sync matches filter
  1141              (syncStatuses.length === 0 || hook || (root.status && syncStatuses.indexOf(root.status) > -1)) &&
  1142              // include if node or node's root health matches filter
  1143              (filterInput.health.length === 0 ||
  1144                  hook ||
  1145                  (root.health && filterInput.health.indexOf(root.health.status) > -1) ||
  1146                  (node.health && filterInput.health.indexOf(node.health.status) > -1)) &&
  1147              (filterInput.namespace.length === 0 || filterInput.namespace.includes(node.namespace))
  1148          ) {
  1149              return true;
  1150          }
  1151  
  1152          return false;
  1153      }
  1154  
  1155      private nodeNameMatchesWildcardFilters(nodeName: string, filterInputNames: string[]): boolean {
  1156          const regularExpression = new RegExp(
  1157              filterInputNames
  1158                  // Escape any regex input to ensure only * can be used
  1159                  .map(pattern => '^' + this.escapeRegex(pattern) + '$')
  1160                  // Replace any escaped * with proper regex
  1161                  .map(pattern => pattern.replace(/\\\*/g, '.*'))
  1162                  // Join all filterInputs to a single regular expression
  1163                  .join('|'),
  1164              'gi'
  1165          );
  1166          return regularExpression.test(nodeName);
  1167      }
  1168  
  1169      private escapeRegex(input: string): string {
  1170          return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1171      }
  1172  
  1173      private loadAppInfo(name: string, appNamespace: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> {
  1174          return from(services.applications.get(name, appNamespace))
  1175              .pipe(
  1176                  mergeMap(app => {
  1177                      const fallbackTree = {
  1178                          nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})),
  1179                          orphanedNodes: [],
  1180                          hosts: []
  1181                      } as appModels.ApplicationTree;
  1182                      return combineLatest(
  1183                          merge(
  1184                              from([app]),
  1185                              this.appChanged.pipe(filter(item => !!item)),
  1186                              AppUtils.handlePageVisibility(() =>
  1187                                  services.applications
  1188                                      .watch({name, appNamespace})
  1189                                      .pipe(
  1190                                          map(watchEvent => {
  1191                                              if (watchEvent.type === 'DELETED') {
  1192                                                  this.onAppDeleted();
  1193                                              }
  1194                                              return watchEvent.application;
  1195                                          })
  1196                                      )
  1197                                      .pipe(repeat())
  1198                                      .pipe(retryWhen(errors => errors.pipe(delay(500))))
  1199                              )
  1200                          ),
  1201                          merge(
  1202                              from([fallbackTree]),
  1203                              services.applications.resourceTree(name, appNamespace).catch(() => fallbackTree),
  1204                              AppUtils.handlePageVisibility(() =>
  1205                                  services.applications
  1206                                      .watchResourceTree(name, appNamespace)
  1207                                      .pipe(repeat())
  1208                                      .pipe(retryWhen(errors => errors.pipe(delay(500))))
  1209                              )
  1210                          )
  1211                      );
  1212                  })
  1213              )
  1214              .pipe(filter(([application, tree]) => !!application && !!tree))
  1215              .pipe(map(([application, tree]) => ({application, tree})));
  1216      }
  1217  
  1218      private onAppDeleted() {
  1219          this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`});
  1220          this.appContext.apis.navigation.goto('/applications');
  1221      }
  1222  
  1223      private async updateApp(app: appModels.Application, query: {validate?: boolean}) {
  1224          const latestApp = await services.applications.get(app.metadata.name, app.metadata.namespace);
  1225          latestApp.metadata.labels = app.metadata.labels;
  1226          latestApp.metadata.annotations = app.metadata.annotations;
  1227          latestApp.spec = app.spec;
  1228          const updatedApp = await services.applications.update(latestApp, query);
  1229          this.appChanged.next(updatedApp);
  1230      }
  1231  
  1232      private groupAppNodesByKey(application: appModels.Application, tree: appModels.ApplicationTree) {
  1233          const nodeByKey = new Map<string, appModels.ResourceDiff | appModels.ResourceNode | appModels.Application>();
  1234          tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node));
  1235          nodeByKey.set(AppUtils.nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application);
  1236          return nodeByKey;
  1237      }
  1238  
  1239      private getTreeFilter(filterInput: string[]): FilterInput {
  1240          const name = new Array<string>();
  1241          const kind = new Array<string>();
  1242          const health = new Array<string>();
  1243          const sync = new Array<string>();
  1244          const namespace = new Array<string>();
  1245          for (const item of filterInput || []) {
  1246              const [type, val] = item.split(':');
  1247              switch (type) {
  1248                  case 'name':
  1249                      name.push(val);
  1250                      break;
  1251                  case 'kind':
  1252                      kind.push(val);
  1253                      break;
  1254                  case 'health':
  1255                      health.push(val);
  1256                      break;
  1257                  case 'sync':
  1258                      sync.push(val);
  1259                      break;
  1260                  case 'namespace':
  1261                      namespace.push(val);
  1262                      break;
  1263              }
  1264          }
  1265          return {kind, health, sync, namespace, name};
  1266      }
  1267  
  1268      private setOperationStatusVisible(isVisible: boolean) {
  1269          this.appContext.apis.navigation.goto('.', {operation: isVisible}, {replace: true});
  1270      }
  1271  
  1272      private setHydrateOperationStatusVisible(isVisible: boolean) {
  1273          this.appContext.apis.navigation.goto('.', {hydrateOperation: isVisible}, {replace: true});
  1274      }
  1275  
  1276      private setConditionsStatusVisible(isVisible: boolean) {
  1277          this.appContext.apis.navigation.goto('.', {conditions: isVisible}, {replace: true});
  1278      }
  1279  
  1280      private setRollbackPanelVisible(selectedDeploymentIndex = 0) {
  1281          this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex}, {replace: true});
  1282      }
  1283  
  1284      private setExtensionPanelVisible(selectedExtension = '') {
  1285          this.appContext.apis.navigation.goto('.', {extension: selectedExtension}, {replace: true});
  1286      }
  1287  
  1288      private selectNode(fullName: string, containerIndex = 0, tab: string = null) {
  1289          SelectNode(fullName, containerIndex, tab, this.appContext.apis);
  1290      }
  1291  
  1292      private async rollbackApplication(revisionHistory: appModels.RevisionHistory, application: appModels.Application) {
  1293          try {
  1294              const needDisableRollback = application.spec.syncPolicy && application.spec.syncPolicy.automated;
  1295              let confirmationMessage = `Are you sure you want to rollback application '${this.props.match.params.name}'?`;
  1296              if (needDisableRollback) {
  1297                  confirmationMessage = `Auto-Sync needs to be disabled in order for rollback to occur.
  1298  Are you sure you want to disable auto-sync and rollback application '${this.props.match.params.name}'?`;
  1299              }
  1300  
  1301              await this.appContext.apis.popup.prompt(
  1302                  'Rollback application',
  1303                  api => (
  1304                      <div>
  1305                          <p>{confirmationMessage}</p>
  1306                          <div className='argo-form-row'>
  1307                              <CheckboxField id='rollback-prune' field='prune' formApi={api} />
  1308                              <label htmlFor='rollback-prune'>PRUNE</label>
  1309                          </div>
  1310                      </div>
  1311                  ),
  1312                  {
  1313                      submit: async (vals, _, close) => {
  1314                          try {
  1315                              if (needDisableRollback) {
  1316                                  const update = JSON.parse(JSON.stringify(application)) as appModels.Application;
  1317                                  update.spec.syncPolicy.automated = null;
  1318                                  await services.applications.update(update, {validate: false});
  1319                              }
  1320                              await services.applications.rollback(this.props.match.params.name, this.getAppNamespace(), revisionHistory.id, vals.prune);
  1321                              this.appChanged.next(await services.applications.get(this.props.match.params.name, this.getAppNamespace()));
  1322                              this.setRollbackPanelVisible(-1);
  1323                              close();
  1324                          } catch (e) {
  1325                              this.appContext.apis.notifications.show({
  1326                                  content: <ErrorNotification title='Unable to rollback application' e={e} />,
  1327                                  type: NotificationType.Error
  1328                              });
  1329                          }
  1330                      }
  1331                  }
  1332              );
  1333          } catch (e) {
  1334              this.appContext.apis.notifications.show({
  1335                  content: <ErrorNotification title='Unable to rollback application' e={e} />,
  1336                  type: NotificationType.Error
  1337              });
  1338          }
  1339      }
  1340  
  1341      private get appContext(): AppContext {
  1342          return this.context as AppContext;
  1343      }
  1344  
  1345      private async deleteApplication() {
  1346          await AppUtils.deleteApplication(this.props.match.params.name, this.getAppNamespace(), this.appContext.apis);
  1347      }
  1348  
  1349      private async confirmDeletion(app: appModels.Application, title: string, message: string) {
  1350          const confirmed = await this.appContext.apis.popup.confirm(title, message);
  1351          if (confirmed) {
  1352              if (!app.metadata.annotations) {
  1353                  app.metadata.annotations = {};
  1354              }
  1355              app.metadata.annotations[appModels.AppDeletionConfirmedAnnotation] = new Date().toISOString();
  1356              await services.applications.update(app);
  1357          }
  1358      }
  1359  }
  1360  
  1361  const ExtensionView = (props: {extension: AppViewExtension; application: models.Application; tree: models.ApplicationTree}) => {
  1362      const {extension, application, tree} = props;
  1363      return <extension.component application={application} tree={tree} />;
  1364  };