github.com/argoproj/argo-cd/v2@v2.10.9/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} 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, urlPattern, helpTip} from '../utils';
    28  import {ChartDetails, 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 {AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service';
    34  
    35  interface ApplicationDetailsState {
    36      page: number;
    37      revision?: string;
    38      groupedResources?: ResourceStatus[];
    39      slidingPanelPage?: number;
    40      filteredGraph?: any[];
    41      truncateNameOnRight?: boolean;
    42      collapsedNodes?: string[];
    43      extensions?: AppViewExtension[];
    44      extensionsMap?: {[key: string]: AppViewExtension};
    45      statusExtensions?: StatusPanelExtension[];
    46      statusExtensionsMap?: {[key: string]: StatusPanelExtension};
    47  }
    48  
    49  interface FilterInput {
    50      name: string[];
    51      kind: string[];
    52      health: string[];
    53      sync: string[];
    54      namespace: string[];
    55  }
    56  
    57  const ApplicationDetailsFilters = (props: FiltersProps) => {
    58      const sidebarTarget = useSidebarTarget();
    59      return ReactDOM.createPortal(<Filters {...props} />, sidebarTarget?.current);
    60  };
    61  
    62  export const NodeInfo = (node?: string): {key: string; container: number} => {
    63      const nodeContainer = {key: '', container: 0};
    64      if (node) {
    65          const parts = node.split('/');
    66          nodeContainer.key = parts.slice(0, 4).join('/');
    67          nodeContainer.container = parseInt(parts[4] || '0', 10);
    68      }
    69      return nodeContainer;
    70  };
    71  
    72  export const SelectNode = (fullName: string, containerIndex = 0, tab: string = null, appContext: ContextApis) => {
    73      const node = fullName ? `${fullName}/${containerIndex}` : null;
    74      appContext.navigation.goto('.', {node, tab}, {replace: true});
    75  };
    76  
    77  export class ApplicationDetails extends React.Component<RouteComponentProps<{appnamespace: string; name: string}>, ApplicationDetailsState> {
    78      public static contextTypes = {
    79          apis: PropTypes.object
    80      };
    81  
    82      private appChanged = new BehaviorSubject<appModels.Application>(null);
    83      private appNamespace: string;
    84  
    85      constructor(props: RouteComponentProps<{appnamespace: string; name: string}>) {
    86          super(props);
    87          const extensions = services.extensions.getAppViewExtensions();
    88          const extensionsMap: {[key: string]: AppViewExtension} = {};
    89          extensions.forEach(ext => {
    90              extensionsMap[ext.title] = ext;
    91          });
    92          const statusExtensions = services.extensions.getStatusPanelExtensions();
    93          const statusExtensionsMap: {[key: string]: StatusPanelExtension} = {};
    94          statusExtensions.forEach(ext => {
    95              statusExtensionsMap[ext.id] = ext;
    96          });
    97          this.state = {
    98              page: 0,
    99              groupedResources: [],
   100              slidingPanelPage: 0,
   101              filteredGraph: [],
   102              truncateNameOnRight: false,
   103              collapsedNodes: [],
   104              extensions,
   105              extensionsMap,
   106              statusExtensions,
   107              statusExtensionsMap
   108          };
   109          if (typeof this.props.match.params.appnamespace === 'undefined') {
   110              this.appNamespace = '';
   111          } else {
   112              this.appNamespace = this.props.match.params.appnamespace;
   113          }
   114      }
   115  
   116      private get showOperationState() {
   117          return new URLSearchParams(this.props.history.location.search).get('operation') === 'true';
   118      }
   119  
   120      private setNodeExpansion(node: string, isExpanded: boolean) {
   121          const index = this.state.collapsedNodes.indexOf(node);
   122          if (isExpanded && index >= 0) {
   123              this.state.collapsedNodes.splice(index, 1);
   124              const updatedNodes = this.state.collapsedNodes.slice();
   125              this.setState({collapsedNodes: updatedNodes});
   126          } else if (!isExpanded && index < 0) {
   127              const updatedNodes = this.state.collapsedNodes.slice();
   128              updatedNodes.push(node);
   129              this.setState({collapsedNodes: updatedNodes});
   130          }
   131      }
   132  
   133      private getNodeExpansion(node: string): boolean {
   134          return this.state.collapsedNodes.indexOf(node) < 0;
   135      }
   136  
   137      private get showConditions() {
   138          return new URLSearchParams(this.props.history.location.search).get('conditions') === 'true';
   139      }
   140  
   141      private get selectedRollbackDeploymentIndex() {
   142          return parseInt(new URLSearchParams(this.props.history.location.search).get('rollback'), 10);
   143      }
   144  
   145      private get selectedNodeInfo() {
   146          return NodeInfo(new URLSearchParams(this.props.history.location.search).get('node'));
   147      }
   148  
   149      private get selectedNodeKey() {
   150          const nodeContainer = this.selectedNodeInfo;
   151          return nodeContainer.key;
   152      }
   153  
   154      private get selectedExtension() {
   155          return new URLSearchParams(this.props.history.location.search).get('extension');
   156      }
   157  
   158      private closeGroupedNodesPanel() {
   159          this.setState({groupedResources: []});
   160          this.setState({slidingPanelPage: 0});
   161      }
   162  
   163      private toggleCompactView(appName: string, pref: AppDetailsPreferences) {
   164          pref.userHelpTipMsgs = pref.userHelpTipMsgs.map(usrMsg => (usrMsg.appName === appName && usrMsg.msgKey === 'groupNodes' ? {...usrMsg, display: true} : usrMsg));
   165          services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: !pref.groupNodes}});
   166      }
   167  
   168      private getPageTitle(view: string) {
   169          const {Tree, Pods, Network, List} = AppsDetailsViewKey;
   170          switch (view) {
   171              case Tree:
   172                  return 'Application Details Tree';
   173              case Network:
   174                  return 'Application Details Network';
   175              case Pods:
   176                  return 'Application Details Pods';
   177              case List:
   178                  return 'Application Details List';
   179          }
   180          return '';
   181      }
   182  
   183      public render() {
   184          return (
   185              <ObservableQuery>
   186                  {q => (
   187                      <DataLoader
   188                          errorRenderer={error => <Page title='Application Details'>{error}</Page>}
   189                          loadingRenderer={() => <Page title='Application Details'>Loading...</Page>}
   190                          input={this.props.match.params.name}
   191                          load={name =>
   192                              combineLatest([this.loadAppInfo(name, this.appNamespace), services.viewPreferences.getPreferences(), q]).pipe(
   193                                  map(items => {
   194                                      const application = items[0].application;
   195                                      const pref = items[1].appDetails;
   196                                      const params = items[2];
   197                                      if (params.get('resource') != null) {
   198                                          pref.resourceFilter = params
   199                                              .get('resource')
   200                                              .split(',')
   201                                              .filter(item => !!item);
   202                                      }
   203                                      if (params.get('view') != null) {
   204                                          pref.view = params.get('view') as AppsDetailsViewType;
   205                                      } else {
   206                                          const appDefaultView = (application.metadata &&
   207                                              application.metadata.annotations &&
   208                                              application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType;
   209                                          if (appDefaultView != null) {
   210                                              pref.view = appDefaultView;
   211                                          }
   212                                      }
   213                                      if (params.get('orphaned') != null) {
   214                                          pref.orphanedResources = params.get('orphaned') === 'true';
   215                                      }
   216                                      if (params.get('podSortMode') != null) {
   217                                          pref.podView.sortMode = params.get('podSortMode') as PodGroupType;
   218                                      } else {
   219                                          const appDefaultPodSort = (application.metadata &&
   220                                              application.metadata.annotations &&
   221                                              application.metadata.annotations[appModels.AnnotationDefaultPodSort]) as PodGroupType;
   222                                          if (appDefaultPodSort != null) {
   223                                              pref.podView.sortMode = appDefaultPodSort;
   224                                          }
   225                                      }
   226                                      return {...items[0], pref};
   227                                  })
   228                              )
   229                          }>
   230                          {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => {
   231                              tree.nodes = tree.nodes || [];
   232                              const treeFilter = this.getTreeFilter(pref.resourceFilter);
   233                              const setFilter = (items: string[]) => {
   234                                  this.appContext.apis.navigation.goto('.', {resource: items.join(',')}, {replace: true});
   235                                  services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
   236                              };
   237                              const clearFilter = () => setFilter([]);
   238                              const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
   239                              const appNodesByName = this.groupAppNodesByKey(application, tree);
   240                              const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null;
   241                              const isAppSelected = selectedItem === application;
   242                              const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode);
   243                              const operationState = application.status.operationState;
   244                              const conditions = application.status.conditions || [];
   245                              const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy');
   246                              const tab = new URLSearchParams(this.props.history.location.search).get('tab');
   247                              const source = getAppDefaultSource(application);
   248                              const showToolTip = pref?.userHelpTipMsgs.find(usrMsg => usrMsg.appName === application.metadata.name);
   249                              const resourceNodes = (): any[] => {
   250                                  const statusByKey = new Map<string, models.ResourceStatus>();
   251                                  application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res));
   252                                  const resources = new Map<string, any>();
   253                                  tree.nodes
   254                                      .map(node => ({...node, orphaned: false}))
   255                                      .concat(((pref.orphanedResources && tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
   256                                      .forEach(node => {
   257                                          const resource: any = {...node};
   258                                          resource.uid = node.uid;
   259                                          const status = statusByKey.get(AppUtils.nodeKey(node));
   260                                          if (status) {
   261                                              resource.health = status.health;
   262                                              resource.status = status.status;
   263                                              resource.hook = status.hook;
   264                                              resource.syncWave = status.syncWave;
   265                                              resource.requiresPruning = status.requiresPruning;
   266                                          }
   267                                          resources.set(node.uid || AppUtils.nodeKey(node), resource);
   268                                      });
   269                                  const resourcesRef = Array.from(resources.values());
   270                                  return resourcesRef;
   271                              };
   272  
   273                              const filteredRes = resourceNodes().filter(res => {
   274                                  const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''};
   275                                  resNode.root = resNode;
   276                                  return this.filterTreeNode(resNode, treeFilter);
   277                              });
   278                              const openGroupNodeDetails = (groupdedNodeIds: string[]) => {
   279                                  const resources = resourceNodes();
   280                                  this.setState({
   281                                      groupedResources: groupdedNodeIds
   282                                          ? resources.filter(res => groupdedNodeIds.includes(res.uid) || groupdedNodeIds.includes(AppUtils.nodeKey(res)))
   283                                          : []
   284                                  });
   285                              };
   286  
   287                              const renderCommitMessage = (message: string) =>
   288                                  message.split(/\s/).map(part =>
   289                                      urlPattern.test(part) ? (
   290                                          <a href={part} target='_blank' rel='noopener noreferrer' style={{overflowWrap: 'anywhere', wordBreak: 'break-word'}}>
   291                                              {part}{' '}
   292                                          </a>
   293                                      ) : (
   294                                          part + ' '
   295                                      )
   296                                  );
   297                              const {Tree, Pods, Network, List} = AppsDetailsViewKey;
   298                              const zoomNum = (pref.zoom * 100).toFixed(0);
   299                              const setZoom = (s: number) => {
   300                                  let targetZoom: number = pref.zoom + s;
   301                                  if (targetZoom <= 0.05) {
   302                                      targetZoom = 0.1;
   303                                  } else if (targetZoom > 2.0) {
   304                                      targetZoom = 2.0;
   305                                  }
   306                                  services.viewPreferences.updatePreferences({appDetails: {...pref, zoom: targetZoom}});
   307                              };
   308                              const setFilterGraph = (filterGraph: any[]) => {
   309                                  this.setState({filteredGraph: filterGraph});
   310                              };
   311                              const setShowCompactNodes = (showCompactView: boolean) => {
   312                                  services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: showCompactView}});
   313                              };
   314                              const updateHelpTipState = (usrHelpTip: models.UserMessages) => {
   315                                  const existingIndex = pref.userHelpTipMsgs.findIndex(msg => msg.appName === usrHelpTip.appName && msg.msgKey === usrHelpTip.msgKey);
   316                                  if (existingIndex !== -1) {
   317                                      pref.userHelpTipMsgs[existingIndex] = usrHelpTip;
   318                                  } else {
   319                                      (pref.userHelpTipMsgs || []).push(usrHelpTip);
   320                                  }
   321                              };
   322                              const toggleNameDirection = () => {
   323                                  this.setState({truncateNameOnRight: !this.state.truncateNameOnRight});
   324                              };
   325                              const expandAll = () => {
   326                                  this.setState({collapsedNodes: []});
   327                              };
   328                              const collapseAll = () => {
   329                                  const nodes = new Array<ResourceTreeNode>();
   330                                  tree.nodes
   331                                      .map(node => ({...node, orphaned: false}))
   332                                      .concat((tree.orphanedNodes || []).map(node => ({...node, orphaned: true})))
   333                                      .forEach(node => {
   334                                          const resourceNode: ResourceTreeNode = {...node};
   335                                          nodes.push(resourceNode);
   336                                      });
   337                                  const collapsedNodesList = this.state.collapsedNodes.slice();
   338                                  if (pref.view === 'network') {
   339                                      const networkNodes = nodes.filter(node => node.networkingInfo);
   340                                      networkNodes.forEach(parent => {
   341                                          const parentId = parent.uid;
   342                                          if (collapsedNodesList.indexOf(parentId) < 0) {
   343                                              collapsedNodesList.push(parentId);
   344                                          }
   345                                      });
   346                                      this.setState({collapsedNodes: collapsedNodesList});
   347                                  } else {
   348                                      const managedKeys = new Set(application.status.resources.map(AppUtils.nodeKey));
   349                                      nodes.forEach(node => {
   350                                          if (!((node.parentRefs || []).length === 0 || managedKeys.has(AppUtils.nodeKey(node)))) {
   351                                              node.parentRefs.forEach(parent => {
   352                                                  const parentId = parent.uid;
   353                                                  if (collapsedNodesList.indexOf(parentId) < 0) {
   354                                                      collapsedNodesList.push(parentId);
   355                                                  }
   356                                              });
   357                                          }
   358                                      });
   359                                      collapsedNodesList.push(application.kind + '-' + application.metadata.namespace + '-' + application.metadata.name);
   360                                      this.setState({collapsedNodes: collapsedNodesList});
   361                                  }
   362                              };
   363                              const appFullName = AppUtils.nodeKey({
   364                                  group: 'argoproj.io',
   365                                  kind: application.kind,
   366                                  name: application.metadata.name,
   367                                  namespace: application.metadata.namespace
   368                              });
   369  
   370                              const activeExtension = this.state.statusExtensionsMap[this.selectedExtension];
   371  
   372                              return (
   373                                  <div className={`application-details ${this.props.match.params.name}`}>
   374                                      <Page
   375                                          title={this.props.match.params.name + ' - ' + this.getPageTitle(pref.view)}
   376                                          useTitleOnly={true}
   377                                          topBarTitle={this.getPageTitle(pref.view)}
   378                                          toolbar={{
   379                                              breadcrumbs: [
   380                                                  {title: 'Applications', path: '/applications'},
   381                                                  {title: <ApplicationsDetailsAppDropdown appName={this.props.match.params.name} />}
   382                                              ],
   383                                              actionMenu: {items: this.getApplicationActionMenu(application, true)},
   384                                              tools: (
   385                                                  <React.Fragment key='app-list-tools'>
   386                                                      <div className='application-details__view-type'>
   387                                                          <i
   388                                                              className={classNames('fa fa-sitemap', {selected: pref.view === Tree})}
   389                                                              title='Tree'
   390                                                              onClick={() => {
   391                                                                  this.appContext.apis.navigation.goto('.', {view: Tree});
   392                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Tree}});
   393                                                              }}
   394                                                          />
   395                                                          <i
   396                                                              className={classNames('fa fa-th', {selected: pref.view === Pods})}
   397                                                              title='Pods'
   398                                                              onClick={() => {
   399                                                                  this.appContext.apis.navigation.goto('.', {view: Pods});
   400                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}});
   401                                                              }}
   402                                                          />
   403                                                          <i
   404                                                              className={classNames('fa fa-network-wired', {selected: pref.view === Network})}
   405                                                              title='Network'
   406                                                              onClick={() => {
   407                                                                  this.appContext.apis.navigation.goto('.', {view: Network});
   408                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}});
   409                                                              }}
   410                                                          />
   411                                                          <i
   412                                                              className={classNames('fa fa-th-list', {selected: pref.view === List})}
   413                                                              title='List'
   414                                                              onClick={() => {
   415                                                                  this.appContext.apis.navigation.goto('.', {view: List});
   416                                                                  services.viewPreferences.updatePreferences({appDetails: {...pref, view: List}});
   417                                                              }}
   418                                                          />
   419                                                          {this.state.extensions &&
   420                                                              (this.state.extensions || []).map(ext => (
   421                                                                  <i
   422                                                                      key={ext.title}
   423                                                                      className={classNames(`fa ${ext.icon}`, {selected: pref.view === ext.title})}
   424                                                                      title={ext.title}
   425                                                                      onClick={() => {
   426                                                                          this.appContext.apis.navigation.goto('.', {view: ext.title});
   427                                                                          services.viewPreferences.updatePreferences({appDetails: {...pref, view: ext.title}});
   428                                                                      }}
   429                                                                  />
   430                                                              ))}
   431                                                      </div>
   432                                                  </React.Fragment>
   433                                              )
   434                                          }}>
   435                                          <div className='application-details__wrapper'>
   436                                              <div className='application-details__status-panel'>
   437                                                  <ApplicationStatusPanel
   438                                                      application={application}
   439                                                      showDiff={() => this.selectNode(appFullName, 0, 'diff')}
   440                                                      showOperation={() => this.setOperationStatusVisible(true)}
   441                                                      showConditions={() => this.setConditionsStatusVisible(true)}
   442                                                      showExtension={id => this.setExtensionPanelVisible(id)}
   443                                                      showMetadataInfo={revision => this.setState({...this.state, revision})}
   444                                                  />
   445                                              </div>
   446                                              <div className='application-details__tree'>
   447                                                  {refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
   448                                                  {((pref.view === 'tree' || pref.view === 'network') && (
   449                                                      <>
   450                                                          <DataLoader load={() => services.viewPreferences.getPreferences()}>
   451                                                              {viewPref => (
   452                                                                  <ApplicationDetailsFilters
   453                                                                      pref={pref}
   454                                                                      tree={tree}
   455                                                                      onSetFilter={setFilter}
   456                                                                      onClearFilter={clearFilter}
   457                                                                      collapsed={viewPref.hideSidebar}
   458                                                                      resourceNodes={this.state.filteredGraph}
   459                                                                  />
   460                                                              )}
   461                                                          </DataLoader>
   462                                                          <div className='graph-options-panel'>
   463                                                              <a
   464                                                                  className={`group-nodes-button`}
   465                                                                  onClick={() => {
   466                                                                      toggleNameDirection();
   467                                                                  }}
   468                                                                  title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
   469                                                                  <i
   470                                                                      className={classNames({
   471                                                                          'fa fa-align-right': this.state.truncateNameOnRight,
   472                                                                          'fa fa-align-left': !this.state.truncateNameOnRight
   473                                                                      })}
   474                                                                  />
   475                                                              </a>
   476                                                              {(pref.view === 'tree' || pref.view === 'network') && (
   477                                                                  <Tooltip
   478                                                                      content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
   479                                                                      visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
   480                                                                      duration={showToolTip?.duration}
   481                                                                      zIndex={1}>
   482                                                                      <a
   483                                                                          className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
   484                                                                          title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
   485                                                                          onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
   486                                                                          <i className={classNames('fa fa-object-group fa-fw')} />
   487                                                                      </a>
   488                                                                  </Tooltip>
   489                                                              )}
   490  
   491                                                              <span className={`separator`} />
   492                                                              <a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
   493                                                                  <i className='fa fa-plus fa-fw' />
   494                                                              </a>
   495                                                              <a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
   496                                                                  <i className='fa fa-minus fa-fw' />
   497                                                              </a>
   498                                                              <span className={`separator`} />
   499                                                              <span>
   500                                                                  <a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
   501                                                                      <i className='fa fa-search-plus fa-fw' />
   502                                                                  </a>
   503                                                                  <a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
   504                                                                      <i className='fa fa-search-minus fa-fw' />
   505                                                                  </a>
   506                                                                  <div className={`zoom-value`}>{zoomNum}%</div>
   507                                                              </span>
   508                                                          </div>
   509                                                          <ApplicationResourceTree
   510                                                              nodeFilter={node => this.filterTreeNode(node, treeFilter)}
   511                                                              selectedNodeFullName={this.selectedNodeKey}
   512                                                              onNodeClick={fullName => this.selectNode(fullName)}
   513                                                              nodeMenu={node =>
   514                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   515                                                                      this.getApplicationActionMenu(application, false)
   516                                                                  )
   517                                                              }
   518                                                              showCompactNodes={pref.groupNodes}
   519                                                              userMsgs={pref.userHelpTipMsgs}
   520                                                              tree={tree}
   521                                                              app={application}
   522                                                              showOrphanedResources={pref.orphanedResources}
   523                                                              useNetworkingHierarchy={pref.view === 'network'}
   524                                                              onClearFilter={clearFilter}
   525                                                              onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
   526                                                              zoom={pref.zoom}
   527                                                              podGroupCount={pref.podGroupCount}
   528                                                              appContext={this.appContext}
   529                                                              nameDirection={this.state.truncateNameOnRight}
   530                                                              filters={pref.resourceFilter}
   531                                                              setTreeFilterGraph={setFilterGraph}
   532                                                              updateUsrHelpTipMsgs={updateHelpTipState}
   533                                                              setShowCompactNodes={setShowCompactNodes}
   534                                                              setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
   535                                                              getNodeExpansion={node => this.getNodeExpansion(node)}
   536                                                          />
   537                                                      </>
   538                                                  )) ||
   539                                                      (pref.view === 'pods' && (
   540                                                          <PodView
   541                                                              tree={tree}
   542                                                              app={application}
   543                                                              onItemClick={fullName => this.selectNode(fullName)}
   544                                                              nodeMenu={node =>
   545                                                                  AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
   546                                                                      this.getApplicationActionMenu(application, false)
   547                                                                  )
   548                                                              }
   549                                                              quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
   550                                                          />
   551                                                      )) ||
   552                                                      (this.state.extensionsMap[pref.view] != null && (
   553                                                          <ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
   554                                                      )) || (
   555                                                          <div>
   556                                                              <DataLoader load={() => services.viewPreferences.getPreferences()}>
   557                                                                  {viewPref => (
   558                                                                      <ApplicationDetailsFilters
   559                                                                          pref={pref}
   560                                                                          tree={tree}
   561                                                                          onSetFilter={setFilter}
   562                                                                          onClearFilter={clearFilter}
   563                                                                          collapsed={viewPref.hideSidebar}
   564                                                                          resourceNodes={filteredRes}
   565                                                                      />
   566                                                                  )}
   567                                                              </DataLoader>
   568                                                              {(filteredRes.length > 0 && (
   569                                                                  <Paginate
   570                                                                      page={this.state.page}
   571                                                                      data={filteredRes}
   572                                                                      onPageChange={page => this.setState({page})}
   573                                                                      preferencesKey='application-details'>
   574                                                                      {data => (
   575                                                                          <ApplicationResourceList
   576                                                                              onNodeClick={fullName => this.selectNode(fullName)}
   577                                                                              resources={data}
   578                                                                              nodeMenu={node =>
   579                                                                                  AppUtils.renderResourceMenu(
   580                                                                                      {...node, root: node},
   581                                                                                      application,
   582                                                                                      tree,
   583                                                                                      this.appContext.apis,
   584                                                                                      this.appChanged,
   585                                                                                      () => this.getApplicationActionMenu(application, false)
   586                                                                                  )
   587                                                                              }
   588                                                                              tree={tree}
   589                                                                          />
   590                                                                      )}
   591                                                                  </Paginate>
   592                                                              )) || (
   593                                                                  <EmptyState icon='fa fa-search'>
   594                                                                      <h4>No resources found</h4>
   595                                                                      <h5>Try to change filter criteria</h5>
   596                                                                  </EmptyState>
   597                                                              )}
   598                                                          </div>
   599                                                      )}
   600                                              </div>
   601                                          </div>
   602                                          <SlidingPanel isShown={this.state.groupedResources.length > 0} onClose={() => this.closeGroupedNodesPanel()}>
   603                                              <div className='application-details__sliding-panel-pagination-wrap'>
   604                                                  <Paginate
   605                                                      page={this.state.slidingPanelPage}
   606                                                      data={this.state.groupedResources}
   607                                                      onPageChange={page => this.setState({slidingPanelPage: page})}
   608                                                      preferencesKey='grouped-nodes-details'>
   609                                                      {data => (
   610                                                          <ApplicationResourceList
   611                                                              onNodeClick={fullName => this.selectNode(fullName)}
   612                                                              resources={data}
   613                                                              nodeMenu={node =>
   614                                                                  AppUtils.renderResourceMenu({...node, root: node}, application, tree, this.appContext.apis, this.appChanged, () =>
   615                                                                      this.getApplicationActionMenu(application, false)
   616                                                                  )
   617                                                              }
   618                                                              tree={tree}
   619                                                          />
   620                                                      )}
   621                                                  </Paginate>
   622                                              </div>
   623                                          </SlidingPanel>
   624                                          <SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => this.selectNode('')}>
   625                                              <ResourceDetails
   626                                                  tree={tree}
   627                                                  application={application}
   628                                                  isAppSelected={isAppSelected}
   629                                                  updateApp={(app: models.Application, query: {validate?: boolean}) => this.updateApp(app, query)}
   630                                                  selectedNode={selectedNode}
   631                                                  tab={tab}
   632                                              />
   633                                          </SlidingPanel>
   634                                          <ApplicationSyncPanel
   635                                              application={application}
   636                                              hide={() => AppUtils.showDeploy(null, null, this.appContext.apis)}
   637                                              selectedResource={syncResourceKey}
   638                                          />
   639                                          <SlidingPanel isShown={this.selectedRollbackDeploymentIndex > -1} onClose={() => this.setRollbackPanelVisible(-1)}>
   640                                              {this.selectedRollbackDeploymentIndex > -1 && (
   641                                                  <ApplicationDeploymentHistory
   642                                                      app={application}
   643                                                      selectedRollbackDeploymentIndex={this.selectedRollbackDeploymentIndex}
   644                                                      rollbackApp={info => this.rollbackApplication(info, application)}
   645                                                      selectDeployment={i => this.setRollbackPanelVisible(i)}
   646                                                  />
   647                                              )}
   648                                          </SlidingPanel>
   649                                          <SlidingPanel isShown={this.showOperationState && !!operationState} onClose={() => this.setOperationStatusVisible(false)}>
   650                                              {operationState && <ApplicationOperationState application={application} operationState={operationState} />}
   651                                          </SlidingPanel>
   652                                          <SlidingPanel isShown={this.showConditions && !!conditions} onClose={() => this.setConditionsStatusVisible(false)}>
   653                                              {conditions && <ApplicationConditions conditions={conditions} />}
   654                                          </SlidingPanel>
   655                                          <SlidingPanel isShown={!!this.state.revision} isMiddle={true} onClose={() => this.setState({revision: null})}>
   656                                              {this.state.revision &&
   657                                                  (source.chart ? (
   658                                                      <DataLoader
   659                                                          input={application}
   660                                                          load={input =>
   661                                                              services.applications.revisionChartDetails(input.metadata.name, input.metadata.namespace, this.state.revision)
   662                                                          }>
   663                                                          {(m: ChartDetails) => (
   664                                                              <div className='white-box' style={{marginTop: '1.5em'}}>
   665                                                                  <div className='white-box__details'>
   666                                                                      <div className='row white-box__details-row'>
   667                                                                          <div className='columns small-3'>Revision:</div>
   668                                                                          <div className='columns small-9'>{this.state.revision}</div>
   669                                                                      </div>
   670                                                                      <div className='row white-box__details-row'>
   671                                                                          <div className='columns small-3'>Helm Chart:</div>
   672                                                                          <div className='columns small-9'>
   673                                                                              {source.chart}&nbsp;
   674                                                                              {m.home && (
   675                                                                                  <a
   676                                                                                      title={m.home}
   677                                                                                      onClick={e => {
   678                                                                                          e.stopPropagation();
   679                                                                                          window.open(m.home);
   680                                                                                      }}>
   681                                                                                      <i className='fa fa-external-link-alt' />
   682                                                                                  </a>
   683                                                                              )}
   684                                                                          </div>
   685                                                                      </div>
   686                                                                      {m.description && (
   687                                                                          <div className='row white-box__details-row'>
   688                                                                              <div className='columns small-3'>Description:</div>
   689                                                                              <div className='columns small-9'>{m.description}</div>
   690                                                                          </div>
   691                                                                      )}
   692                                                                      {m.maintainers && m.maintainers.length > 0 && (
   693                                                                          <div className='row white-box__details-row'>
   694                                                                              <div className='columns small-3'>Maintainers:</div>
   695                                                                              <div className='columns small-9'>{m.maintainers.join(', ')}</div>
   696                                                                          </div>
   697                                                                      )}
   698                                                                  </div>
   699                                                              </div>
   700                                                          )}
   701                                                      </DataLoader>
   702                                                  ) : (
   703                                                      <DataLoader
   704                                                          load={() =>
   705                                                              services.applications.revisionMetadata(application.metadata.name, application.metadata.namespace, this.state.revision)
   706                                                          }>
   707                                                          {metadata => (
   708                                                              <div className='white-box' style={{marginTop: '1.5em'}}>
   709                                                                  <div className='white-box__details'>
   710                                                                      <div className='row white-box__details-row'>
   711                                                                          <div className='columns small-3'>SHA:</div>
   712                                                                          <div className='columns small-9'>
   713                                                                              <Revision repoUrl={source.repoURL} revision={this.state.revision} />
   714                                                                          </div>
   715                                                                      </div>
   716                                                                  </div>
   717                                                                  <div className='white-box__details'>
   718                                                                      <div className='row white-box__details-row'>
   719                                                                          <div className='columns small-3'>Date:</div>
   720                                                                          <div className='columns small-9'>
   721                                                                              <Timestamp date={metadata.date} />
   722                                                                          </div>
   723                                                                      </div>
   724                                                                  </div>
   725                                                                  <div className='white-box__details'>
   726                                                                      <div className='row white-box__details-row'>
   727                                                                          <div className='columns small-3'>Tags:</div>
   728                                                                          <div className='columns small-9'>
   729                                                                              {((metadata.tags || []).length > 0 && metadata.tags.join(', ')) || 'No tags'}
   730                                                                          </div>
   731                                                                      </div>
   732                                                                  </div>
   733                                                                  <div className='white-box__details'>
   734                                                                      <div className='row white-box__details-row'>
   735                                                                          <div className='columns small-3'>Author:</div>
   736                                                                          <div className='columns small-9'>{metadata.author}</div>
   737                                                                      </div>
   738                                                                  </div>
   739                                                                  <div className='white-box__details'>
   740                                                                      <div className='row white-box__details-row'>
   741                                                                          <div className='columns small-3'>Message:</div>
   742                                                                          <div className='columns small-9' style={{display: 'flex', alignItems: 'center'}}>
   743                                                                              <div className='application-details__commit-message'>{renderCommitMessage(metadata.message)}</div>
   744                                                                          </div>
   745                                                                      </div>
   746                                                                  </div>
   747                                                              </div>
   748                                                          )}
   749                                                      </DataLoader>
   750                                                  ))}
   751                                          </SlidingPanel>
   752                                          <SlidingPanel
   753                                              isShown={this.selectedExtension !== '' && activeExtension != null && activeExtension.flyout != null}
   754                                              onClose={() => this.setExtensionPanelVisible('')}>
   755                                              {this.selectedExtension !== '' && activeExtension && activeExtension.flyout && (
   756                                                  <activeExtension.flyout application={application} tree={tree} />
   757                                              )}
   758                                          </SlidingPanel>
   759                                      </Page>
   760                                  </div>
   761                              );
   762                          }}
   763                      </DataLoader>
   764                  )}
   765              </ObservableQuery>
   766          );
   767      }
   768  
   769      private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) {
   770          const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey];
   771          const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
   772          const ActionMenuItem = (prop: {actionLabel: string}) => <span className={needOverlapLabelOnNarrowScreen ? 'show-for-large' : ''}>{prop.actionLabel}</span>;
   773          const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0;
   774          return [
   775              {
   776                  iconClassName: 'fa fa-info-circle',
   777                  title: <ActionMenuItem actionLabel='Details' />,
   778                  action: () => this.selectNode(fullName)
   779              },
   780              {
   781                  iconClassName: 'fa fa-file-medical',
   782                  title: <ActionMenuItem actionLabel='Diff' />,
   783                  action: () => this.selectNode(fullName, 0, 'diff'),
   784                  disabled: app.status.sync.status === appModels.SyncStatuses.Synced
   785              },
   786              {
   787                  iconClassName: 'fa fa-sync',
   788                  title: <ActionMenuItem actionLabel='Sync' />,
   789                  action: () => AppUtils.showDeploy('all', null, this.appContext.apis)
   790              },
   791              {
   792                  iconClassName: 'fa fa-info-circle',
   793                  title: <ActionMenuItem actionLabel='Sync Status' />,
   794                  action: () => this.setOperationStatusVisible(true),
   795                  disabled: !app.status.operationState
   796              },
   797              {
   798                  iconClassName: 'fa fa-history',
   799                  title: hasMultipleSources ? (
   800                      <React.Fragment>
   801                          <ActionMenuItem actionLabel=' History and rollback' />
   802                          {helpTip('Rollback is not supported for apps with multiple sources')}
   803                      </React.Fragment>
   804                  ) : (
   805                      <ActionMenuItem actionLabel='History and rollback' />
   806                  ),
   807                  action: () => {
   808                      this.setRollbackPanelVisible(0);
   809                  },
   810                  disabled: !app.status.operationState || hasMultipleSources
   811              },
   812              {
   813                  iconClassName: 'fa fa-times-circle',
   814                  title: <ActionMenuItem actionLabel='Delete' />,
   815                  action: () => this.deleteApplication()
   816              },
   817              {
   818                  iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}),
   819                  title: (
   820                      <React.Fragment>
   821                          <ActionMenuItem actionLabel='Refresh' />{' '}
   822                          <DropDownMenu
   823                              items={[
   824                                  {
   825                                      title: 'Hard Refresh',
   826                                      action: () => !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, 'hard')
   827                                  }
   828                              ]}
   829                              anchor={() => <i className='fa fa-caret-down' />}
   830                          />
   831                      </React.Fragment>
   832                  ),
   833                  disabled: !!refreshing,
   834                  action: () => {
   835                      if (!refreshing) {
   836                          services.applications.get(app.metadata.name, app.metadata.namespace, 'normal');
   837                          AppUtils.setAppRefreshing(app);
   838                          this.appChanged.next(app);
   839                      }
   840                  }
   841              }
   842          ];
   843      }
   844  
   845      private filterTreeNode(node: ResourceTreeNode, filterInput: FilterInput): boolean {
   846          const syncStatuses = filterInput.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []);
   847  
   848          const root = node.root || ({} as ResourceTreeNode);
   849          const hook = root && root.hook;
   850          if (
   851              (filterInput.name.length === 0 || this.nodeNameMatchesWildcardFilters(node.name, filterInput.name)) &&
   852              (filterInput.kind.length === 0 || filterInput.kind.indexOf(node.kind) > -1) &&
   853              // include if node's root sync matches filter
   854              (syncStatuses.length === 0 || hook || (root.status && syncStatuses.indexOf(root.status) > -1)) &&
   855              // include if node or node's root health matches filter
   856              (filterInput.health.length === 0 ||
   857                  hook ||
   858                  (root.health && filterInput.health.indexOf(root.health.status) > -1) ||
   859                  (node.health && filterInput.health.indexOf(node.health.status) > -1)) &&
   860              (filterInput.namespace.length === 0 || filterInput.namespace.includes(node.namespace))
   861          ) {
   862              return true;
   863          }
   864  
   865          return false;
   866      }
   867  
   868      private nodeNameMatchesWildcardFilters(nodeName: string, filterInputNames: string[]): boolean {
   869          const regularExpression = new RegExp(
   870              filterInputNames
   871                  // Escape any regex input to ensure only * can be used
   872                  .map(pattern => '^' + this.escapeRegex(pattern) + '$')
   873                  // Replace any escaped * with proper regex
   874                  .map(pattern => pattern.replace(/\\\*/g, '.*'))
   875                  // Join all filterInputs to a single regular expression
   876                  .join('|'),
   877              'gi'
   878          );
   879          return regularExpression.test(nodeName);
   880      }
   881  
   882      private escapeRegex(input: string): string {
   883          return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
   884      }
   885  
   886      private loadAppInfo(name: string, appNamespace: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> {
   887          return from(services.applications.get(name, appNamespace))
   888              .pipe(
   889                  mergeMap(app => {
   890                      const fallbackTree = {
   891                          nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})),
   892                          orphanedNodes: [],
   893                          hosts: []
   894                      } as appModels.ApplicationTree;
   895                      return combineLatest(
   896                          merge(
   897                              from([app]),
   898                              this.appChanged.pipe(filter(item => !!item)),
   899                              AppUtils.handlePageVisibility(() =>
   900                                  services.applications
   901                                      .watch({name, appNamespace})
   902                                      .pipe(
   903                                          map(watchEvent => {
   904                                              if (watchEvent.type === 'DELETED') {
   905                                                  this.onAppDeleted();
   906                                              }
   907                                              return watchEvent.application;
   908                                          })
   909                                      )
   910                                      .pipe(repeat())
   911                                      .pipe(retryWhen(errors => errors.pipe(delay(500))))
   912                              )
   913                          ),
   914                          merge(
   915                              from([fallbackTree]),
   916                              services.applications.resourceTree(name, appNamespace).catch(() => fallbackTree),
   917                              AppUtils.handlePageVisibility(() =>
   918                                  services.applications
   919                                      .watchResourceTree(name, appNamespace)
   920                                      .pipe(repeat())
   921                                      .pipe(retryWhen(errors => errors.pipe(delay(500))))
   922                              )
   923                          )
   924                      );
   925                  })
   926              )
   927              .pipe(filter(([application, tree]) => !!application && !!tree))
   928              .pipe(map(([application, tree]) => ({application, tree})));
   929      }
   930  
   931      private onAppDeleted() {
   932          this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`});
   933          this.appContext.apis.navigation.goto('/applications');
   934      }
   935  
   936      private async updateApp(app: appModels.Application, query: {validate?: boolean}) {
   937          const latestApp = await services.applications.get(app.metadata.name, app.metadata.namespace);
   938          latestApp.metadata.labels = app.metadata.labels;
   939          latestApp.metadata.annotations = app.metadata.annotations;
   940          latestApp.spec = app.spec;
   941          const updatedApp = await services.applications.update(latestApp, query);
   942          this.appChanged.next(updatedApp);
   943      }
   944  
   945      private groupAppNodesByKey(application: appModels.Application, tree: appModels.ApplicationTree) {
   946          const nodeByKey = new Map<string, appModels.ResourceDiff | appModels.ResourceNode | appModels.Application>();
   947          tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node));
   948          nodeByKey.set(AppUtils.nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application);
   949          return nodeByKey;
   950      }
   951  
   952      private getTreeFilter(filterInput: string[]): FilterInput {
   953          const name = new Array<string>();
   954          const kind = new Array<string>();
   955          const health = new Array<string>();
   956          const sync = new Array<string>();
   957          const namespace = new Array<string>();
   958          for (const item of filterInput || []) {
   959              const [type, val] = item.split(':');
   960              switch (type) {
   961                  case 'name':
   962                      name.push(val);
   963                      break;
   964                  case 'kind':
   965                      kind.push(val);
   966                      break;
   967                  case 'health':
   968                      health.push(val);
   969                      break;
   970                  case 'sync':
   971                      sync.push(val);
   972                      break;
   973                  case 'namespace':
   974                      namespace.push(val);
   975                      break;
   976              }
   977          }
   978          return {kind, health, sync, namespace, name};
   979      }
   980  
   981      private setOperationStatusVisible(isVisible: boolean) {
   982          this.appContext.apis.navigation.goto('.', {operation: isVisible}, {replace: true});
   983      }
   984  
   985      private setConditionsStatusVisible(isVisible: boolean) {
   986          this.appContext.apis.navigation.goto('.', {conditions: isVisible}, {replace: true});
   987      }
   988  
   989      private setRollbackPanelVisible(selectedDeploymentIndex = 0) {
   990          this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex}, {replace: true});
   991      }
   992  
   993      private setExtensionPanelVisible(selectedExtension = '') {
   994          this.appContext.apis.navigation.goto('.', {extension: selectedExtension}, {replace: true});
   995      }
   996  
   997      private selectNode(fullName: string, containerIndex = 0, tab: string = null) {
   998          SelectNode(fullName, containerIndex, tab, this.appContext.apis);
   999      }
  1000  
  1001      private async rollbackApplication(revisionHistory: appModels.RevisionHistory, application: appModels.Application) {
  1002          try {
  1003              const needDisableRollback = application.spec.syncPolicy && application.spec.syncPolicy.automated;
  1004              let confirmationMessage = `Are you sure you want to rollback application '${this.props.match.params.name}'?`;
  1005              if (needDisableRollback) {
  1006                  confirmationMessage = `Auto-Sync needs to be disabled in order for rollback to occur.
  1007  Are you sure you want to disable auto-sync and rollback application '${this.props.match.params.name}'?`;
  1008              }
  1009  
  1010              const confirmed = await this.appContext.apis.popup.confirm('Rollback application', confirmationMessage);
  1011              if (confirmed) {
  1012                  if (needDisableRollback) {
  1013                      const update = JSON.parse(JSON.stringify(application)) as appModels.Application;
  1014                      update.spec.syncPolicy = {automated: null};
  1015                      await services.applications.update(update);
  1016                  }
  1017                  await services.applications.rollback(this.props.match.params.name, this.appNamespace, revisionHistory.id);
  1018                  this.appChanged.next(await services.applications.get(this.props.match.params.name, this.appNamespace));
  1019                  this.setRollbackPanelVisible(-1);
  1020              }
  1021          } catch (e) {
  1022              this.appContext.apis.notifications.show({
  1023                  content: <ErrorNotification title='Unable to rollback application' e={e} />,
  1024                  type: NotificationType.Error
  1025              });
  1026          }
  1027      }
  1028  
  1029      private get appContext(): AppContext {
  1030          return this.context as AppContext;
  1031      }
  1032  
  1033      private async deleteApplication() {
  1034          await AppUtils.deleteApplication(this.props.match.params.name, this.appNamespace, this.appContext.apis);
  1035      }
  1036  }
  1037  
  1038  const ExtensionView = (props: {extension: AppViewExtension; application: models.Application; tree: models.ApplicationTree}) => {
  1039      const {extension, application, tree} = props;
  1040      return <extension.component application={application} tree={tree} />;
  1041  };