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

     1  import {DropDown, Tooltip} from 'argo-ui';
     2  import * as classNames from 'classnames';
     3  import * as dagre from 'dagre';
     4  import * as React from 'react';
     5  import Moment from 'react-moment';
     6  import * as moment from 'moment';
     7  
     8  import * as models from '../../../shared/models';
     9  
    10  import {EmptyState} from '../../../shared/components';
    11  import {AppContext, Consumer} from '../../../shared/context';
    12  import {ApplicationURLs} from '../application-urls';
    13  import {ResourceIcon} from '../resource-icon';
    14  import {ResourceLabel} from '../resource-label';
    15  import {
    16      BASE_COLORS,
    17      ComparisonStatusIcon,
    18      getAppOverridesCount,
    19      HealthStatusIcon,
    20      isAppNode,
    21      isYoungerThanXMinutes,
    22      NodeId,
    23      nodeKey,
    24      PodHealthIcon,
    25      getUsrMsgKeyToDisplay,
    26      formatResourceInfo
    27  } from '../utils';
    28  import {NodeUpdateAnimation} from './node-update-animation';
    29  import {PodGroup} from '../application-pod-view/pod-view';
    30  import './application-resource-tree.scss';
    31  import {ArrowConnector} from './arrow-connector';
    32  
    33  function treeNodeKey(node: NodeId & {uid?: string}) {
    34      return node.uid || nodeKey(node);
    35  }
    36  
    37  const color = require('color');
    38  
    39  export interface ResourceTreeNode extends models.ResourceNode {
    40      status?: models.SyncStatusCode;
    41      health?: models.HealthStatus;
    42      hook?: boolean;
    43      root?: ResourceTreeNode;
    44      requiresPruning?: boolean;
    45      orphaned?: boolean;
    46      podGroup?: PodGroup;
    47      isExpanded?: boolean;
    48  }
    49  
    50  export interface ApplicationResourceTreeProps {
    51      app: models.Application;
    52      tree: models.ApplicationTree;
    53      useNetworkingHierarchy: boolean;
    54      nodeFilter: (node: ResourceTreeNode) => boolean;
    55      selectedNodeFullName?: string;
    56      onNodeClick?: (fullName: string) => any;
    57      onGroupdNodeClick?: (groupedNodeIds: string[]) => any;
    58      nodeMenu?: (node: models.ResourceNode) => React.ReactNode;
    59      onClearFilter: () => any;
    60      appContext?: AppContext;
    61      showOrphanedResources: boolean;
    62      showCompactNodes: boolean;
    63      userMsgs: models.UserMessages[];
    64      updateUsrHelpTipMsgs: (userMsgs: models.UserMessages) => void;
    65      setShowCompactNodes: (showCompactNodes: boolean) => void;
    66      zoom: number;
    67      podGroupCount: number;
    68      filters?: string[];
    69      setTreeFilterGraph?: (filterGraph: any[]) => void;
    70      nameDirection: boolean;
    71      nameWrap: boolean;
    72      setNodeExpansion: (node: string, isExpanded: boolean) => any;
    73      getNodeExpansion: (node: string) => boolean;
    74  }
    75  
    76  interface Line {
    77      x1: number;
    78      y1: number;
    79      x2: number;
    80      y2: number;
    81  }
    82  
    83  const NODE_WIDTH = 282;
    84  const NODE_HEIGHT = 52;
    85  const POD_NODE_HEIGHT = 136;
    86  const FILTERED_INDICATOR_NODE = '__filtered_indicator__';
    87  const EXTERNAL_TRAFFIC_NODE = '__external_traffic__';
    88  const INTERNAL_TRAFFIC_NODE = '__internal_traffic__';
    89  const NODE_TYPES = {
    90      filteredIndicator: 'filtered_indicator',
    91      externalTraffic: 'external_traffic',
    92      externalLoadBalancer: 'external_load_balancer',
    93      internalTraffic: 'internal_traffic',
    94      groupedNodes: 'grouped_nodes',
    95      podGroup: 'pod_group'
    96  };
    97  // generate lots of colors with different darkness
    98  const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6].map(darken => BASE_COLORS.map(item => color(item).darken(darken).hex())).reduce((first, second) => first.concat(second), []);
    99  
   100  function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
   101      let width = 0;
   102      let height = 0;
   103      nodes.forEach(node => {
   104          width = Math.max(node.x + node.width, width);
   105          height = Math.max(node.y + node.height, height);
   106      });
   107      return {width, height};
   108  }
   109  
   110  function groupNodes(nodes: ResourceTreeNode[], graph: dagre.graphlib.Graph) {
   111      function getNodeGroupingInfo(nodeId: string) {
   112          const node = graph.node(nodeId);
   113          return {
   114              nodeId,
   115              kind: node.kind,
   116              parentIds: graph.predecessors(nodeId),
   117              childIds: graph.successors(nodeId)
   118          };
   119      }
   120  
   121      function filterNoChildNode(nodeInfo: {childIds: dagre.Node[]}) {
   122          return nodeInfo.childIds.length === 0;
   123      }
   124  
   125      // create nodes array with parent/child nodeId
   126      const nodesInfoArr = graph.nodes().map(getNodeGroupingInfo);
   127  
   128      // group sibling nodes into a 2d array
   129      const siblingNodesArr = nodesInfoArr
   130          .reduce((acc, curr) => {
   131              if (curr.childIds.length > 1) {
   132                  acc.push(curr.childIds.map(nodeId => getNodeGroupingInfo(nodeId.toString())));
   133              }
   134              return acc;
   135          }, [])
   136          .map(nodeArr => nodeArr.filter(filterNoChildNode));
   137  
   138      // group sibling nodes with same kind
   139      const groupedNodesArr = siblingNodesArr
   140          .map(eachLevel => {
   141              return eachLevel.reduce(
   142                  (groupedNodesInfo: {kind: string; nodeIds?: string[]; parentIds?: dagre.Node[]}[], currentNodeInfo: {kind: string; nodeId: string; parentIds: dagre.Node[]}) => {
   143                      const index = groupedNodesInfo.findIndex((nodeInfo: {kind: string}) => currentNodeInfo.kind === nodeInfo.kind);
   144                      if (index > -1) {
   145                          groupedNodesInfo[index].nodeIds.push(currentNodeInfo.nodeId);
   146                      }
   147  
   148                      if (groupedNodesInfo.length === 0 || index < 0) {
   149                          const nodeIdArr = [];
   150                          nodeIdArr.push(currentNodeInfo.nodeId);
   151                          const groupedNodesInfoObj = {
   152                              kind: currentNodeInfo.kind,
   153                              nodeIds: nodeIdArr,
   154                              parentIds: currentNodeInfo.parentIds
   155                          };
   156                          groupedNodesInfo.push(groupedNodesInfoObj);
   157                      }
   158  
   159                      return groupedNodesInfo;
   160                  },
   161                  []
   162              );
   163          })
   164          .reduce((flattedNodesGroup, groupedNodes) => {
   165              return flattedNodesGroup.concat(groupedNodes);
   166          }, [])
   167          .filter((eachArr: {nodeIds: string[]}) => eachArr.nodeIds.length > 1);
   168  
   169      // update graph
   170      if (groupedNodesArr.length > 0) {
   171          groupedNodesArr.forEach((obj: {kind: string; nodeIds: string[]; parentIds: dagre.Node[]}) => {
   172              const {nodeIds, kind, parentIds} = obj;
   173              const groupedNodeIds: string[] = [];
   174              const podGroupIds: string[] = [];
   175              nodeIds.forEach((nodeId: string) => {
   176                  const index = nodes.findIndex(node => nodeId === node.uid || nodeId === nodeKey(node));
   177                  const graphNode = graph.node(nodeId);
   178                  if (!graphNode?.podGroup && index > -1) {
   179                      groupedNodeIds.push(nodeId);
   180                  } else {
   181                      podGroupIds.push(nodeId);
   182                  }
   183              });
   184              const reducedNodeIds = nodeIds.reduce((acc, aNodeId) => {
   185                  if (podGroupIds.findIndex(i => i === aNodeId) < 0) {
   186                      acc.push(aNodeId);
   187                  }
   188                  return acc;
   189              }, []);
   190              if (groupedNodeIds.length > 1) {
   191                  groupedNodeIds.forEach(n => graph.removeNode(n));
   192                  graph.setNode(`${parentIds[0].toString()}/child/${kind}`, {
   193                      kind,
   194                      groupedNodeIds,
   195                      height: NODE_HEIGHT,
   196                      width: NODE_WIDTH,
   197                      count: reducedNodeIds.length,
   198                      type: NODE_TYPES.groupedNodes
   199                  });
   200                  graph.setEdge(parentIds[0].toString(), `${parentIds[0].toString()}/child/${kind}`);
   201              }
   202          });
   203      }
   204  }
   205  
   206  export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) {
   207      function orphanedToInt(orphaned?: boolean) {
   208          return (orphaned && 1) || 0;
   209      }
   210      function compareRevision(a: string, b: string) {
   211          const numberA = Number(a);
   212          const numberB = Number(b);
   213          if (isNaN(numberA) || isNaN(numberB)) {
   214              return a.localeCompare(b, undefined, {numeric: true});
   215          }
   216          return Math.sign(numberA - numberB);
   217      }
   218      function getRevision(a: ResourceTreeNode) {
   219          const filtered = (a.info || []).filter(b => b.name === 'Revision' && b)[0];
   220          if (filtered == null) {
   221              return '';
   222          }
   223          const value = filtered.value;
   224          if (value == null) {
   225              return '';
   226          }
   227          return value.replace(/^Rev:/, '');
   228      }
   229      if (first.kind === 'ReplicaSet') {
   230          return (
   231              orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
   232              compareRevision(getRevision(second), getRevision(first)) ||
   233              nodeKey(first).localeCompare(nodeKey(second), undefined, {numeric: true}) ||
   234              0
   235          );
   236      }
   237      return (
   238          orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
   239          nodeKey(first).localeCompare(nodeKey(second), undefined, {numeric: true}) ||
   240          compareRevision(getRevision(first), getRevision(second)) ||
   241          0
   242      );
   243  }
   244  
   245  function appNodeKey(app: models.Application) {
   246      return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
   247  }
   248  
   249  function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) {
   250      const indicators = new Array<number>();
   251      let count = Math.min(node.count - 1, 3);
   252      while (count > 0) {
   253          indicators.push(count--);
   254      }
   255      return (
   256          <React.Fragment>
   257              <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
   258                  <div className='application-resource-tree__node-kind-icon '>
   259                      <i className='icon fa fa-filter' />
   260                  </div>
   261                  <div className='application-resource-tree__node-content-wrap-overflow'>
   262                      <a className='application-resource-tree__node-title' onClick={onClearFilter}>
   263                          clear filters to show {node.count} additional resource{node.count > 1 && 's'}
   264                      </a>
   265                  </div>
   266              </div>
   267              {indicators.map(i => (
   268                  <div
   269                      key={i}
   270                      className='application-resource-tree__node application-resource-tree__filtered-indicator'
   271                      style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
   272                  />
   273              ))}
   274          </React.Fragment>
   275      );
   276  }
   277  
   278  function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number} & dagre.Node & ResourceTreeNode) {
   279      const indicators = new Array<number>();
   280      let count = Math.min(node.count - 1, 3);
   281      while (count > 0) {
   282          indicators.push(count--);
   283      }
   284      return (
   285          <React.Fragment>
   286              <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
   287                  <div className='application-resource-tree__node-kind-icon'>
   288                      <ResourceIcon kind={node.kind} />
   289                      <br />
   290                      <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>
   291                  </div>
   292                  <div
   293                      className='application-resource-tree__node-title application-resource-tree__direction-center-left'
   294                      onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}
   295                      title={`Click to see details of ${node.count} collapsed ${node.kind} and doesn't contains any active pods`}>
   296                      {node.count} {node.kind.endsWith('s') ? node.kind : `${node.kind}s`}
   297                      <span style={{paddingLeft: '.5em', fontSize: 'small'}}>
   298                          {node.kind === 'ReplicaSet' ? (
   299                              <i
   300                                  className='fa-solid fa-cart-flatbed icon-background'
   301                                  title={`Click to see details of ${node.count} collapsed ${node.kind} and doesn't contains any active pods`}
   302                                  key={node.uid}
   303                              />
   304                          ) : (
   305                              <i className='fa fa-info-circle icon-background' title={`Click to see details of ${node.count} collapsed ${node.kind}`} key={node.uid} />
   306                          )}
   307                      </span>
   308                  </div>
   309              </div>
   310              {indicators.map(i => (
   311                  <div
   312                      key={i}
   313                      className='application-resource-tree__node application-resource-tree__filtered-indicator'
   314                      style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
   315                  />
   316              ))}
   317          </React.Fragment>
   318      );
   319  }
   320  
   321  function renderTrafficNode(node: dagre.Node) {
   322      return (
   323          <div style={{position: 'absolute', left: 0, top: node.y, width: node.width, height: node.height}}>
   324              <div className='application-resource-tree__node-kind-icon' style={{fontSize: '2em'}}>
   325                  <i className='icon fa fa-cloud' />
   326              </div>
   327          </div>
   328      );
   329  }
   330  
   331  function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
   332      return (
   333          <div
   334              className='application-resource-tree__node application-resource-tree__node--load-balancer'
   335              style={{
   336                  left: node.x,
   337                  top: node.y,
   338                  width: node.width,
   339                  height: node.height
   340              }}>
   341              <div className='application-resource-tree__node-kind-icon'>
   342                  <i title={node.kind} className={`icon fa fa-network-wired`} style={{color: node.color}} />
   343              </div>
   344              <div className='application-resource-tree__node-content'>
   345                  <span className='application-resource-tree__node-title'>{node.label}</span>
   346              </div>
   347          </div>
   348      );
   349  }
   350  
   351  export const describeNode = (node: ResourceTreeNode) => {
   352      const lines = [`Kind: ${node.kind}`, `Namespace: ${node.namespace || '(global)'}`, `Name: ${node.name}`];
   353      if (node.images) {
   354          lines.push('Images:');
   355          node.images.forEach(i => lines.push(`- ${i}`));
   356      }
   357      return lines.join('\n');
   358  };
   359  
   360  function processPodGroup(targetPodGroup: ResourceTreeNode, child: ResourceTreeNode, props: ApplicationResourceTreeProps) {
   361      if (!targetPodGroup.podGroup) {
   362          const fullName = nodeKey(targetPodGroup);
   363          if ((targetPodGroup.parentRefs || []).length === 0) {
   364              targetPodGroup.root = targetPodGroup;
   365          }
   366          targetPodGroup.podGroup = {
   367              pods: [] as models.Pod[],
   368              fullName,
   369              ...targetPodGroup.podGroup,
   370              ...targetPodGroup,
   371              info: (targetPodGroup.info || []).filter(i => !i.name.includes('Resource.')),
   372              createdAt: targetPodGroup.createdAt,
   373              renderMenu: () => props.nodeMenu(targetPodGroup),
   374              kind: targetPodGroup.kind,
   375              type: 'parentResource',
   376              name: targetPodGroup.name
   377          };
   378      }
   379      if (child.kind === 'Pod') {
   380          const p: models.Pod = {
   381              ...child,
   382              fullName: nodeKey(child),
   383              metadata: {name: child.name},
   384              spec: {nodeName: 'Unknown'},
   385              health: child.health ? child.health.status : 'Unknown'
   386          } as models.Pod;
   387  
   388          // Get node name for Pod
   389          child.info?.forEach(i => {
   390              if (i.name === 'Node') {
   391                  p.spec.nodeName = i.value;
   392              }
   393          });
   394          targetPodGroup.podGroup.pods.push(p);
   395      }
   396  }
   397  
   398  function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, childMap: Map<string, ResourceTreeNode[]>) {
   399      const fullName = nodeKey(node);
   400      let comparisonStatus: models.SyncStatusCode = null;
   401      let healthState: models.HealthStatus = null;
   402      if (node.status || node.health) {
   403          comparisonStatus = node.status;
   404          healthState = node.health;
   405      }
   406      const appNode = isAppNode(node);
   407      const rootNode = !node.root;
   408      const extLinks: string[] = props.app.status.summary.externalURLs;
   409      const podGroupChildren = childMap.get(treeNodeKey(node));
   410      const nonPodChildren = podGroupChildren?.reduce((acc, child) => {
   411          if (child.kind !== 'Pod') {
   412              acc.push(child);
   413          }
   414          return acc;
   415      }, []);
   416      const childCount = nonPodChildren?.length;
   417      const margin = 8;
   418      let topExtra = 0;
   419      const podGroup = node.podGroup;
   420      const podGroupHealthy = [];
   421      const podGroupDegraded = [];
   422      const podGroupInProgress = [];
   423  
   424      for (const pod of podGroup?.pods || []) {
   425          switch (pod.health) {
   426              case 'Healthy':
   427                  podGroupHealthy.push(pod);
   428                  break;
   429              case 'Degraded':
   430                  podGroupDegraded.push(pod);
   431                  break;
   432              case 'Progressing':
   433                  podGroupInProgress.push(pod);
   434          }
   435      }
   436  
   437      const showPodGroupByStatus = props.tree.nodes.filter((rNode: ResourceTreeNode) => rNode.kind === 'Pod').length >= props.podGroupCount;
   438      const numberOfRows = showPodGroupByStatus
   439          ? [podGroupHealthy, podGroupDegraded, podGroupInProgress].reduce((total, podGroupByStatus) => total + (podGroupByStatus.filter(pod => pod).length > 0 ? 1 : 0), 0)
   440          : Math.ceil(podGroup?.pods.length / 8);
   441  
   442      if (podGroup) {
   443          topExtra = margin + (POD_NODE_HEIGHT / 2 + 30 * numberOfRows) / 2;
   444      }
   445  
   446      return (
   447          <div
   448              className={classNames('application-resource-tree__node', {
   449                  'active': fullName === props.selectedNodeFullName,
   450                  'application-resource-tree__node--orphaned': node.orphaned,
   451                  'application-resource-tree__node--grouped-node': !showPodGroupByStatus
   452              })}
   453              title={describeNode(node)}
   454              style={{
   455                  left: node.x,
   456                  top: node.y - topExtra,
   457                  width: node.width,
   458                  height: showPodGroupByStatus ? POD_NODE_HEIGHT + 20 * numberOfRows : node.height
   459              }}>
   460              <NodeUpdateAnimation resourceVersion={node.resourceVersion} />
   461              <div onClick={() => props.onNodeClick && props.onNodeClick(fullName)} className={`application-resource-tree__node__top-part`}>
   462                  <div
   463                      className={classNames('application-resource-tree__node-kind-icon', {
   464                          'application-resource-tree__node-kind-icon--big': rootNode
   465                      })}>
   466                      <ResourceIcon kind={node.kind || 'Unknown'} />
   467                      <br />
   468                      {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
   469                  </div>
   470                  <div
   471                      className={classNames('application-resource-tree__node-content', {
   472                          'application-resource-tree__fullname': props.nameWrap,
   473                          'application-resource-tree__wrappedname': !props.nameWrap
   474                      })}>
   475                      <span
   476                          className={classNames('application-resource-tree__node-title', {
   477                              'application-resource-tree__direction-right': props.nameDirection,
   478                              'application-resource-tree__direction-left': !props.nameDirection
   479                          })}
   480                          onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}>
   481                          {node.name}
   482                      </span>
   483                      <span
   484                          className={classNames('application-resource-tree__node-status-icon', {
   485                              'application-resource-tree__node-status-icon--offset': rootNode
   486                          })}>
   487                          {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
   488                          {healthState != null && <HealthStatusIcon state={healthState} />}
   489                          {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
   490                          {appNode && !rootNode && (
   491                              <Consumer>
   492                                  {ctx => (
   493                                      <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
   494                                          <i className='fa fa-external-link-alt' />
   495                                      </a>
   496                                  )}
   497                              </Consumer>
   498                          )}
   499                          <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
   500                      </span>
   501                      {childCount > 0 && (
   502                          <>
   503                              <br />
   504                              <div
   505                                  style={{top: node.height / 2 - 6}}
   506                                  className='application-resource-tree__node--podgroup--expansion'
   507                                  onClick={event => {
   508                                      expandCollapse(node, props);
   509                                      event.stopPropagation();
   510                                  }}>
   511                                  {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
   512                              </div>
   513                          </>
   514                      )}
   515                  </div>
   516                  <div className='application-resource-tree__node-labels'>
   517                      {node.createdAt || rootNode ? (
   518                          <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
   519                              {node.createdAt || props.app.metadata.creationTimestamp}
   520                          </Moment>
   521                      ) : null}
   522                      {(node.info || [])
   523                          .filter(tag => !tag.name.includes('Node'))
   524                          .slice(0, 4)
   525                          .map((tag, i) => (
   526                              <span className='application-resource-tree__node-label' title={`${tag.name}:${tag.value}`} key={i}>
   527                                  {tag.value}
   528                              </span>
   529                          ))}
   530                      {(node.info || []).length > 4 && (
   531                          <Tooltip
   532                              content={
   533                                  <>
   534                                      {(node.info || []).map(i => {
   535                                          // Use common formatting function for CPU and Memory
   536                                          if (i.name === 'cpu' || i.name === 'memory') {
   537                                              const {tooltipValue} = formatResourceInfo(i.name, `${i.value}`);
   538                                              return <div key={i.name}>{tooltipValue}</div>;
   539                                          } else {
   540                                              return (
   541                                                  <div key={i.name}>
   542                                                      {i.name}: {i.value}
   543                                                  </div>
   544                                              );
   545                                          }
   546                                      })}
   547                                  </>
   548                              }
   549                              key={node.uid}>
   550                              <span className='application-resource-tree__node-label' title='More'>
   551                                  More
   552                              </span>
   553                          </Tooltip>
   554                      )}
   555                  </div>
   556                  {props.nodeMenu && (
   557                      <div className='application-resource-tree__node-menu'>
   558                          <DropDown
   559                              key={node.uid}
   560                              isMenu={true}
   561                              anchor={() => (
   562                                  <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   563                                      <i className='fa fa-ellipsis-v' />
   564                                  </button>
   565                              )}>
   566                              {() => props.nodeMenu(node)}
   567                          </DropDown>
   568                      </div>
   569                  )}
   570              </div>
   571              <div className='application-resource-tree__node--lower-section'>
   572                  {[podGroupHealthy, podGroupDegraded, podGroupInProgress].map((pods, index) => {
   573                      if (pods.length > 0) {
   574                          return (
   575                              <div key={index} className={`application-resource-tree__node--lower-section__pod-group`}>
   576                                  {renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)}
   577                              </div>
   578                          );
   579                      }
   580                  })}
   581              </div>
   582          </div>
   583      );
   584  }
   585  
   586  function renderPodGroupByStatus(props: ApplicationResourceTreeProps, node: any, pods: models.Pod[], showPodGroupByStatus: boolean) {
   587      return (
   588          <div className='application-resource-tree__node--lower-section__pod-group__pod-container__pods'>
   589              {pods.length !== 0 && showPodGroupByStatus ? (
   590                  <React.Fragment>
   591                      <div className={`pod-view__node__pod pod-view__node__pod--${pods[0].health.toLowerCase()}`}>
   592                          <PodHealthIcon state={{status: pods[0].health, message: ''}} key={pods[0].uid} />
   593                      </div>
   594  
   595                      <div className='pod-view__node__label--large'>
   596                          <a
   597                              className='application-resource-tree__node-title'
   598                              onClick={() =>
   599                                  props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupdedNodeIds === 'undefined' ? node.groupdedNodeIds : pods.map(pod => pod.uid))
   600                              }>
   601                              &nbsp;
   602                              <span title={`Click to view the ${pods[0].health.toLowerCase()} pods list`}>
   603                                  {pods[0].health} {pods.length} pods
   604                              </span>
   605                          </a>
   606                      </div>
   607                  </React.Fragment>
   608              ) : (
   609                  pods.map(
   610                      pod =>
   611                          props.nodeMenu && (
   612                              <DropDown
   613                                  key={pod.uid}
   614                                  isMenu={true}
   615                                  anchor={() => (
   616                                      <Tooltip
   617                                          content={
   618                                              <div>
   619                                                  {pod.metadata.name}
   620                                                  <div>Health: {pod.health}</div>
   621                                                  {pod.createdAt && (
   622                                                      <span>
   623                                                          <span>Created: </span>
   624                                                          <Moment fromNow={true} ago={true}>
   625                                                              {pod.createdAt}
   626                                                          </Moment>
   627                                                          <span> ago ({<Moment local={true}>{pod.createdAt}</Moment>})</span>
   628                                                      </span>
   629                                                  )}
   630                                              </div>
   631                                          }
   632                                          popperOptions={{
   633                                              modifiers: {
   634                                                  preventOverflow: {
   635                                                      enabled: true
   636                                                  },
   637                                                  hide: {
   638                                                      enabled: false
   639                                                  },
   640                                                  flip: {
   641                                                      enabled: false
   642                                                  }
   643                                              }
   644                                          }}
   645                                          key={pod.metadata.name}>
   646                                          <div style={{position: 'relative'}}>
   647                                              {isYoungerThanXMinutes(pod, 30) && (
   648                                                  <i className='fas fa-star application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod__star-icon' />
   649                                              )}
   650                                              <div
   651                                                  className={`application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod--${pod.health.toLowerCase()}`}>
   652                                                  <PodHealthIcon state={{status: pod.health, message: ''}} />
   653                                              </div>
   654                                          </div>
   655                                      </Tooltip>
   656                                  )}>
   657                                  {() => props.nodeMenu(pod)}
   658                              </DropDown>
   659                          )
   660                  )
   661              )}
   662          </div>
   663      );
   664  }
   665  
   666  function expandCollapse(node: ResourceTreeNode, props: ApplicationResourceTreeProps) {
   667      const isExpanded = !props.getNodeExpansion(node.uid);
   668      node.isExpanded = isExpanded;
   669      props.setNodeExpansion(node.uid, isExpanded);
   670  }
   671  
   672  function NodeInfoDetails({tag: tag, kind: kind}: {tag: models.InfoItem; kind: string}) {
   673      if (kind === 'Pod') {
   674          const val = tag.name;
   675          if (val === 'Status Reason') {
   676              if (String(tag.value) !== 'ImagePullBackOff')
   677                  return (
   678                      <span className='application-resource-tree__node-label' title={`Status: ${tag.value}`}>
   679                          {tag.value}
   680                      </span>
   681                  );
   682              else {
   683                  return (
   684                      <span
   685                          className='application-resource-tree__node-label'
   686                          title='One of the containers may have the incorrect image name/tag, or you may be fetching from the incorrect repository, or the repository requires authentication.'>
   687                          {tag.value}
   688                      </span>
   689                  );
   690              }
   691          } else if (val === 'Containers') {
   692              const arr = String(tag.value).split('/');
   693              const title = `Number of containers in total: ${arr[1]} \nNumber of ready containers: ${arr[0]}`;
   694              return (
   695                  <span className='application-resource-tree__node-label' title={title}>
   696                      {tag.value}
   697                  </span>
   698              );
   699          } else if (val === 'Restart Count') {
   700              return (
   701                  <span className='application-resource-tree__node-label' title={`The total number of restarts of the containers: ${tag.value}`}>
   702                      {tag.value}
   703                  </span>
   704              );
   705          } else if (val === 'Revision') {
   706              return (
   707                  <span className='application-resource-tree__node-label' title={`The revision in which pod present is: ${tag.value}`}>
   708                      {tag.value}
   709                  </span>
   710              );
   711          } else if (val === 'cpu' || val === 'memory') {
   712              // Use common formatting function for CPU and Memory
   713              const {displayValue, tooltipValue} = formatResourceInfo(val, String(tag.value));
   714              return (
   715                  <span className='application-resource-tree__node-label' title={tooltipValue}>
   716                      {displayValue}
   717                  </span>
   718              );
   719          } else {
   720              return (
   721                  <span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
   722                      {tag.value}
   723                  </span>
   724              );
   725          }
   726      } else {
   727          return (
   728              <span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
   729                  {tag.value}
   730              </span>
   731          );
   732      }
   733  }
   734  
   735  function renderResourceNode(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, nodesHavingChildren: Map<string, number>) {
   736      const fullName = nodeKey(node);
   737      let comparisonStatus: models.SyncStatusCode = null;
   738      let healthState: models.HealthStatus = null;
   739      if (node.status || node.health) {
   740          comparisonStatus = node.status;
   741          healthState = node.health;
   742      }
   743      const appNode = isAppNode(node);
   744      const rootNode = !node.root;
   745      const extLinks: string[] = props.app.status.summary.externalURLs;
   746      const childCount = nodesHavingChildren.get(node.uid);
   747      return (
   748          <div
   749              onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
   750              className={classNames('application-resource-tree__node', 'application-resource-tree__node--' + node.kind.toLowerCase(), {
   751                  'active': fullName === props.selectedNodeFullName,
   752                  'application-resource-tree__node--orphaned': node.orphaned
   753              })}
   754              title={describeNode(node)}
   755              style={{
   756                  left: node.x,
   757                  top: node.y,
   758                  width: node.width,
   759                  height: node.height
   760              }}>
   761              {!appNode && <NodeUpdateAnimation resourceVersion={node.resourceVersion} />}
   762              <div
   763                  className={classNames('application-resource-tree__node-kind-icon', {
   764                      'application-resource-tree__node-kind-icon--big': rootNode
   765                  })}>
   766                  <ResourceIcon kind={node.kind} />
   767                  <br />
   768                  {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
   769              </div>
   770              <div
   771                  className={classNames('application-resource-tree__node-content', {
   772                      'application-resource-tree__fullname': props.nameWrap,
   773                      'application-resource-tree__wrappedname': !props.nameWrap
   774                  })}>
   775                  <div
   776                      className={classNames('application-resource-tree__node-title', {
   777                          'application-resource-tree__direction-right': props.nameDirection,
   778                          'application-resource-tree__direction-left': !props.nameDirection
   779                      })}>
   780                      {node.name}
   781                  </div>
   782                  <div
   783                      className={classNames('application-resource-tree__node-status-icon', {
   784                          'application-resource-tree__node-status-icon--offset': rootNode
   785                      })}>
   786                      {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
   787                      {healthState != null && <HealthStatusIcon state={healthState} />}
   788                      {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
   789                      {appNode && !rootNode && (
   790                          <Consumer>
   791                              {ctx => (
   792                                  <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
   793                                      <i className='fa fa-external-link-alt' />
   794                                  </a>
   795                              )}
   796                          </Consumer>
   797                      )}
   798                      <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
   799                  </div>
   800                  {childCount > 0 && (
   801                      <div
   802                          className='application-resource-tree__node--expansion'
   803                          onClick={event => {
   804                              expandCollapse(node, props);
   805                              event.stopPropagation();
   806                          }}>
   807                          {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
   808                      </div>
   809                  )}
   810              </div>
   811              <div className='application-resource-tree__node-labels'>
   812                  {node.createdAt || rootNode ? (
   813                      <span title={`${node.kind} was created ${moment(node.createdAt).fromNow()}`}>
   814                          <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
   815                              {node.createdAt || props.app.metadata.creationTimestamp}
   816                          </Moment>
   817                      </span>
   818                  ) : null}
   819                  {(node.info || [])
   820                      .filter(tag => !tag.name.includes('Node'))
   821                      .slice(0, 2)
   822                      .map((tag, i) => {
   823                          return <NodeInfoDetails tag={tag} kind={node.kind} key={i} />;
   824                      })}
   825                  {(node.info || []).length > 3 && (
   826                      <Tooltip
   827                          content={
   828                              <>
   829                                  {(node.info || []).map(i => {
   830                                      // Use common formatting function for CPU and Memory
   831                                      if (i.name === 'cpu' || i.name === 'memory') {
   832                                          const {tooltipValue} = formatResourceInfo(i.name, `${i.value}`);
   833                                          return <div key={i.name}>{tooltipValue}</div>;
   834                                      } else {
   835                                          return (
   836                                              <div key={i.name}>
   837                                                  {i.name}: {i.value}
   838                                              </div>
   839                                          );
   840                                      }
   841                                  })}
   842                              </>
   843                          }
   844                          key={node.uid}>
   845                          <span className='application-resource-tree__node-label' title='More'>
   846                              More
   847                          </span>
   848                      </Tooltip>
   849                  )}
   850              </div>
   851              {props.nodeMenu && (
   852                  <div className='application-resource-tree__node-menu'>
   853                      <DropDown
   854                          isMenu={true}
   855                          anchor={() => (
   856                              <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   857                                  <i className='fa fa-ellipsis-v' />
   858                              </button>
   859                          )}>
   860                          {() => props.nodeMenu(node)}
   861                      </DropDown>
   862                  </div>
   863              )}
   864          </div>
   865      );
   866  }
   867  
   868  function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] {
   869      let result = new Array<ResourceTreeNode>();
   870      const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey));
   871      result = result.concat(nodes.filter(target => refs.has(nodeKey(target))));
   872      if (networkingInfo.targetLabels) {
   873          result = result.concat(
   874              nodes.filter(target => {
   875                  if (target.networkingInfo && target.networkingInfo.labels) {
   876                      return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]);
   877                  }
   878                  return false;
   879              })
   880          );
   881      }
   882      return result;
   883  }
   884  export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
   885      const graph = new dagre.graphlib.Graph();
   886      graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80});
   887      graph.setDefaultEdgeLabel(() => ({}));
   888      const overridesCount = getAppOverridesCount(props.app);
   889      const appNode = {
   890          kind: props.app.kind,
   891          name: props.app.metadata.name,
   892          namespace: props.app.metadata.namespace,
   893          resourceVersion: props.app.metadata.resourceVersion,
   894          group: 'argoproj.io',
   895          version: '',
   896          // @ts-expect-error its not any
   897          children: [],
   898          status: props.app.status.sync.status,
   899          health: props.app.status.health,
   900          uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name,
   901          info:
   902              overridesCount > 0
   903                  ? [
   904                        {
   905                            name: 'Parameter overrides',
   906                            value: `${overridesCount} parameter override(s)`
   907                        }
   908                    ]
   909                  : []
   910      };
   911  
   912      const statusByKey = new Map<string, models.ResourceStatus>();
   913      props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
   914      const nodeByKey = new Map<string, ResourceTreeNode>();
   915      props.tree.nodes
   916          .map(node => ({...node, orphaned: false}))
   917          .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
   918          .forEach(node => {
   919              const status = statusByKey.get(nodeKey(node));
   920              const resourceNode: ResourceTreeNode = {...node};
   921              if (status) {
   922                  resourceNode.health = status.health;
   923                  resourceNode.status = status.status;
   924                  resourceNode.hook = status.hook;
   925                  resourceNode.requiresPruning = status.requiresPruning;
   926              }
   927              nodeByKey.set(treeNodeKey(node), resourceNode);
   928          });
   929      const nodes = Array.from(nodeByKey.values());
   930      let roots: ResourceTreeNode[] = [];
   931      const childrenByParentKey = new Map<string, ResourceTreeNode[]>();
   932      const nodesHavingChildren = new Map<string, number>();
   933      const childrenMap = new Map<string, ResourceTreeNode[]>();
   934      const [filters, setFilters] = React.useState(props.filters);
   935      const [filteredGraph, setFilteredGraph] = React.useState([]);
   936      const filteredNodes: any[] = [];
   937  
   938      React.useEffect(() => {
   939          if (props.filters !== filters) {
   940              setFilters(props.filters);
   941              setFilteredGraph(filteredNodes);
   942              props.setTreeFilterGraph(filteredGraph);
   943          }
   944      }, [props.filters]);
   945      const {podGroupCount, userMsgs, updateUsrHelpTipMsgs, setShowCompactNodes} = props;
   946      const podCount = nodes.filter(node => node.kind === 'Pod').length;
   947  
   948      React.useEffect(() => {
   949          if (podCount > podGroupCount) {
   950              const userMsg = getUsrMsgKeyToDisplay(appNode.name, 'groupNodes', userMsgs);
   951              updateUsrHelpTipMsgs(userMsg);
   952              if (!userMsg.display) {
   953                  setShowCompactNodes(true);
   954              }
   955          }
   956      }, [podCount]);
   957  
   958      function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
   959          const appKey = appNodeKey(app);
   960          let filtered = 0;
   961          graphNodesFilter.nodes().forEach(nodeId => {
   962              const node: ResourceTreeNode = graphNodesFilter.node(nodeId) as any;
   963              const parentIds = graphNodesFilter.predecessors(nodeId);
   964  
   965              const shouldKeepNode = () => {
   966                  //case for podgroup in group node view
   967                  if (node.podGroup) {
   968                      return predicate(node) || node.podGroup.pods.some(pod => predicate({...node, kind: 'Pod', name: pod.name}));
   969                  }
   970                  return predicate(node);
   971              };
   972  
   973              if (node.root != null && !shouldKeepNode() && appKey !== nodeId) {
   974                  const childIds = graphNodesFilter.successors(nodeId);
   975                  graphNodesFilter.removeNode(nodeId);
   976                  filtered++;
   977                  childIds.forEach((childId: any) => {
   978                      parentIds.forEach((parentId: any) => {
   979                          graphNodesFilter.setEdge(parentId, childId);
   980                      });
   981                  });
   982              } else {
   983                  if (node.root != null) filteredNodes.push(node);
   984              }
   985          });
   986  
   987          if (filtered) {
   988              graphNodesFilter.setNode(FILTERED_INDICATOR_NODE, {
   989                  height: NODE_HEIGHT,
   990                  width: NODE_WIDTH,
   991                  count: filtered,
   992                  type: NODE_TYPES.filteredIndicator
   993              });
   994              graphNodesFilter.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE);
   995          }
   996      }
   997  
   998      if (props.useNetworkingHierarchy) {
   999          // Network view
  1000          const hasParents = new Set<string>();
  1001          const networkNodes = nodes.filter(node => node.networkingInfo);
  1002          const hiddenNodes: ResourceTreeNode[] = [];
  1003          networkNodes.forEach(parent => {
  1004              findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => {
  1005                  const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
  1006                  hasParents.add(treeNodeKey(child));
  1007                  const parentId = parent.uid;
  1008                  if (nodesHavingChildren.has(parentId)) {
  1009                      nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
  1010                  } else {
  1011                      nodesHavingChildren.set(parentId, 1);
  1012                  }
  1013                  if (child.kind !== 'Pod' || !props.showCompactNodes) {
  1014                      if (props.getNodeExpansion(parentId)) {
  1015                          hasParents.add(treeNodeKey(child));
  1016                          children.push(child);
  1017                          childrenByParentKey.set(treeNodeKey(parent), children);
  1018                      } else {
  1019                          hiddenNodes.push(child);
  1020                      }
  1021                  } else {
  1022                      processPodGroup(parent, child, props);
  1023                  }
  1024              });
  1025          });
  1026          roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node)));
  1027          roots = roots.reduce((acc, curr) => {
  1028              if (hiddenNodes.indexOf(curr) < 0) {
  1029                  acc.push(curr);
  1030              }
  1031              return acc;
  1032          }, []);
  1033          const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes);
  1034          const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes);
  1035          const colorsBySource = new Map<string, string>();
  1036          // sources are root internal services and external ingress/service IPs
  1037          const sources = Array.from(
  1038              new Set(
  1039                  internalRoots
  1040                      .map(root => treeNodeKey(root))
  1041                      .concat(
  1042                          externalRoots.map(root => root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), [])
  1043                      )
  1044              )
  1045          );
  1046          // assign unique color to each traffic source
  1047          sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
  1048  
  1049          if (externalRoots.length > 0) {
  1050              graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic});
  1051              externalRoots.sort(compareNodes).forEach(root => {
  1052                  const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip);
  1053                  const colorByService = new Map<string, string>();
  1054                  (childrenByParentKey.get(treeNodeKey(root)) || []).forEach((child, i) => colorByService.set(treeNodeKey(child), TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
  1055                  (childrenByParentKey.get(treeNodeKey(root)) || []).sort(compareNodes).forEach(child => {
  1056                      processNode(child, root, [colorByService.get(treeNodeKey(child))]);
  1057                  });
  1058                  if (root.podGroup && props.showCompactNodes) {
  1059                      setPodGroupNode(root, root);
  1060                  } else {
  1061                      graph.setNode(treeNodeKey(root), {...root, width: NODE_WIDTH, height: NODE_HEIGHT, root});
  1062                  }
  1063                  (childrenByParentKey.get(treeNodeKey(root)) || []).forEach(child => {
  1064                      if (root.namespace === child.namespace) {
  1065                          graph.setEdge(treeNodeKey(root), treeNodeKey(child), {colors: [colorByService.get(treeNodeKey(child))]});
  1066                      }
  1067                  });
  1068                  loadBalancers.forEach(key => {
  1069                      const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`;
  1070                      graph.setNode(loadBalancerNodeKey, {
  1071                          height: NODE_HEIGHT,
  1072                          width: NODE_WIDTH,
  1073                          type: NODE_TYPES.externalLoadBalancer,
  1074                          label: key,
  1075                          color: colorsBySource.get(key)
  1076                      });
  1077                      graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]});
  1078                      graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]});
  1079                  });
  1080              });
  1081          }
  1082  
  1083          if (internalRoots.length > 0) {
  1084              graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic});
  1085              internalRoots.forEach(root => {
  1086                  processNode(root, root, [colorsBySource.get(treeNodeKey(root))]);
  1087                  graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root));
  1088              });
  1089          }
  1090          if (props.nodeFilter) {
  1091              // show filtered indicator next to external traffic node is app has it otherwise next to internal traffic node
  1092              filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter);
  1093          }
  1094      } else {
  1095          // Tree view
  1096          const managedKeys = new Set(props.app.status.resources.map(nodeKey));
  1097          const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey));
  1098          const orphans: ResourceTreeNode[] = [];
  1099          let allChildNodes: ResourceTreeNode[] = [];
  1100          nodesHavingChildren.set(appNode.uid, 1);
  1101          if (props.getNodeExpansion(appNode.uid)) {
  1102              nodes.forEach(node => {
  1103                  allChildNodes = [];
  1104                  if ((node.parentRefs || []).length === 0 || managedKeys.has(nodeKey(node))) {
  1105                      roots.push(node);
  1106                  } else {
  1107                      if (orphanedKeys.has(nodeKey(node))) {
  1108                          orphans.push(node);
  1109                      }
  1110                      node.parentRefs.forEach(parent => {
  1111                          const parentId = treeNodeKey(parent);
  1112                          const children = childrenByParentKey.get(parentId) || [];
  1113                          if (nodesHavingChildren.has(parentId)) {
  1114                              nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
  1115                          } else {
  1116                              nodesHavingChildren.set(parentId, 1);
  1117                          }
  1118                          allChildNodes.push(node);
  1119                          if (node.kind !== 'Pod' || !props.showCompactNodes) {
  1120                              if (props.getNodeExpansion(parentId)) {
  1121                                  children.push(node);
  1122                                  childrenByParentKey.set(parentId, children);
  1123                              }
  1124                          } else {
  1125                              const parentTreeNode = nodeByKey.get(parentId);
  1126                              processPodGroup(parentTreeNode, node, props);
  1127                          }
  1128                          if (props.showCompactNodes) {
  1129                              if (childrenMap.has(parentId)) {
  1130                                  childrenMap.set(parentId, childrenMap.get(parentId).concat(allChildNodes));
  1131                              } else {
  1132                                  childrenMap.set(parentId, allChildNodes);
  1133                              }
  1134                          }
  1135                      });
  1136                  }
  1137              });
  1138          }
  1139          roots.sort(compareNodes).forEach(node => {
  1140              processNode(node, node);
  1141              graph.setEdge(appNodeKey(props.app), treeNodeKey(node));
  1142          });
  1143          orphans.sort(compareNodes).forEach(node => {
  1144              processNode(node, node);
  1145          });
  1146          graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT});
  1147          if (props.nodeFilter) {
  1148              filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter);
  1149          }
  1150          if (props.showCompactNodes) {
  1151              groupNodes(nodes, graph);
  1152          }
  1153      }
  1154  
  1155      function setPodGroupNode(node: ResourceTreeNode, root: ResourceTreeNode) {
  1156          const numberOfRows = Math.ceil(node.podGroup.pods.length / 8);
  1157          graph.setNode(treeNodeKey(node), {...node, type: NODE_TYPES.podGroup, width: NODE_WIDTH, height: POD_NODE_HEIGHT + 30 * numberOfRows, root});
  1158      }
  1159  
  1160      function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) {
  1161          if (props.showCompactNodes && node.podGroup) {
  1162              setPodGroupNode(node, root);
  1163          } else {
  1164              graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root});
  1165          }
  1166          (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => {
  1167              if (treeNodeKey(child) === treeNodeKey(root)) {
  1168                  return;
  1169              }
  1170              if (node.namespace === child.namespace) {
  1171                  graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors});
  1172              }
  1173              processNode(child, root, colors);
  1174          });
  1175      }
  1176      dagre.layout(graph);
  1177  
  1178      const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string; color?: string; colors?: string | {[key: string]: any}}[] = [];
  1179      const nodeOffset = new Map<string, number>();
  1180      const reverseEdge = new Map<string, number>();
  1181      graph.edges().forEach(edgeInfo => {
  1182          const edge = graph.edge(edgeInfo);
  1183          if (edge.points.length > 1) {
  1184              if (!reverseEdge.has(edgeInfo.w)) {
  1185                  reverseEdge.set(edgeInfo.w, 1);
  1186              } else {
  1187                  reverseEdge.set(edgeInfo.w, reverseEdge.get(edgeInfo.w) + 1);
  1188              }
  1189              if (!nodeOffset.has(edgeInfo.v)) {
  1190                  nodeOffset.set(edgeInfo.v, reverseEdge.get(edgeInfo.w) - 1);
  1191              }
  1192          }
  1193      });
  1194      graph.edges().forEach(edgeInfo => {
  1195          const edge = graph.edge(edgeInfo);
  1196          const colors = (edge.colors as string[]) || [];
  1197          let backgroundImage: string;
  1198          if (colors.length > 0) {
  1199              const step = 100 / colors.length;
  1200              const gradient = colors.map((lineColor, i) => {
  1201                  return `${lineColor} ${step * i}%, ${lineColor} ${step * i + step / 2}%, transparent ${step * i + step / 2}%, transparent ${step * (i + 1)}%`;
  1202              });
  1203              backgroundImage = `linear-gradient(90deg, ${gradient})`;
  1204          }
  1205  
  1206          const lines: Line[] = [];
  1207          // don't render connections from hidden node representing internal traffic
  1208          if (edgeInfo.v === INTERNAL_TRAFFIC_NODE || edgeInfo.w === INTERNAL_TRAFFIC_NODE) {
  1209              return;
  1210          }
  1211          if (edge.points.length > 1) {
  1212              const startNode = graph.node(edgeInfo.v);
  1213              const endNode = graph.node(edgeInfo.w);
  1214              const offset = nodeOffset.get(edgeInfo.v);
  1215              let startNodeRight = props.useNetworkingHierarchy ? 162 : 142;
  1216              const endNodeLeft = 140;
  1217              let spaceForExpansionIcon = 0;
  1218              if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE) && !edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
  1219                  lines.push({x1: startNode.x + 10, y1: startNode.y, x2: endNode.x - endNodeLeft, y2: endNode.y});
  1220              } else {
  1221                  if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
  1222                      startNodeRight = 152;
  1223                      spaceForExpansionIcon = 5;
  1224                  }
  1225                  const len = reverseEdge.get(edgeInfo.w) + 1;
  1226                  const yEnd = endNode.y - endNode.height / 2 + (endNode.height / len + (endNode.height / len) * offset);
  1227                  const firstBend =
  1228                      spaceForExpansionIcon +
  1229                      startNode.x +
  1230                      startNodeRight +
  1231                      (endNode.x - startNode.x - startNodeRight - endNodeLeft) / len +
  1232                      ((endNode.x - startNode.x - startNodeRight - endNodeLeft) / len) * offset;
  1233                  lines.push({x1: startNode.x + startNodeRight, y1: startNode.y, x2: firstBend, y2: startNode.y});
  1234                  if (startNode.y - yEnd >= 1 || yEnd - startNode.y >= 1) {
  1235                      lines.push({x1: firstBend, y1: startNode.y, x2: firstBend, y2: yEnd});
  1236                  }
  1237                  lines.push({x1: firstBend, y1: yEnd, x2: endNode.x - endNodeLeft, y2: yEnd});
  1238              }
  1239          }
  1240          edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage, colors: [{colors}]});
  1241      });
  1242      const graphNodes = graph.nodes();
  1243      const size = getGraphSize(graphNodes.map(id => graph.node(id)));
  1244  
  1245      const resourceTreeRef = React.useRef<HTMLDivElement>();
  1246  
  1247      const graphMoving = React.useRef({
  1248          enable: false,
  1249          x: 0,
  1250          y: 0
  1251      });
  1252  
  1253      const onGraphDragStart: React.PointerEventHandler<HTMLDivElement> = e => {
  1254          if (e.target !== resourceTreeRef.current) {
  1255              return;
  1256          }
  1257  
  1258          if (!resourceTreeRef.current?.parentElement) {
  1259              return;
  1260          }
  1261  
  1262          graphMoving.current.enable = true;
  1263          graphMoving.current.x = e.clientX;
  1264          graphMoving.current.y = e.clientY;
  1265      };
  1266  
  1267      const onGraphDragMoving: React.PointerEventHandler<HTMLDivElement> = e => {
  1268          if (!graphMoving.current.enable) {
  1269              return;
  1270          }
  1271  
  1272          if (!resourceTreeRef.current?.parentElement) {
  1273              return;
  1274          }
  1275  
  1276          const graphContainer = resourceTreeRef.current?.parentElement;
  1277  
  1278          const currentPositionX = graphContainer.scrollLeft;
  1279          const currentPositionY = graphContainer.scrollTop;
  1280  
  1281          const scrollLeft = currentPositionX + graphMoving.current.x - e.clientX;
  1282          const scrollTop = currentPositionY + graphMoving.current.y - e.clientY;
  1283  
  1284          graphContainer.scrollTo(scrollLeft, scrollTop);
  1285  
  1286          graphMoving.current.x = e.clientX;
  1287          graphMoving.current.y = e.clientY;
  1288      };
  1289  
  1290      const onGraphDragEnd: React.PointerEventHandler<HTMLDivElement> = e => {
  1291          if (graphMoving.current.enable) {
  1292              graphMoving.current.enable = false;
  1293              e.preventDefault();
  1294          }
  1295      };
  1296      return (
  1297          (graphNodes.length === 0 && (
  1298              <EmptyState icon=' fa fa-network-wired'>
  1299                  <h4>Your application has no network resources</h4>
  1300                  <h5>Try switching to tree or list view</h5>
  1301              </EmptyState>
  1302          )) || (
  1303              <div
  1304                  ref={resourceTreeRef}
  1305                  onPointerDown={onGraphDragStart}
  1306                  onPointerMove={onGraphDragMoving}
  1307                  onPointerUp={onGraphDragEnd}
  1308                  onPointerLeave={onGraphDragEnd}
  1309                  className={classNames('application-resource-tree', {'application-resource-tree--network': props.useNetworkingHierarchy})}
  1310                  style={{width: size.width + 150, height: size.height + 250, transformOrigin: '0% 4%', transform: `scale(${props.zoom})`}}>
  1311                  {graphNodes.map(key => {
  1312                      const node = graph.node(key);
  1313                      const nodeType = node.type;
  1314                      switch (nodeType) {
  1315                          case NODE_TYPES.filteredIndicator:
  1316                              return <React.Fragment key={key}>{renderFilteredNode(node as any, props.onClearFilter)}</React.Fragment>;
  1317                          case NODE_TYPES.externalTraffic:
  1318                              return <React.Fragment key={key}>{renderTrafficNode(node)}</React.Fragment>;
  1319                          case NODE_TYPES.internalTraffic:
  1320                              return null;
  1321                          case NODE_TYPES.externalLoadBalancer:
  1322                              return <React.Fragment key={key}>{renderLoadBalancerNode(node as any)}</React.Fragment>;
  1323                          case NODE_TYPES.groupedNodes:
  1324                              return <React.Fragment key={key}>{renderGroupedNodes(props, node as any)}</React.Fragment>;
  1325                          case NODE_TYPES.podGroup:
  1326                              return <React.Fragment key={key}>{renderPodGroup(props, key, node as ResourceTreeNode & dagre.Node, childrenMap)}</React.Fragment>;
  1327                          default:
  1328                              return <React.Fragment key={key}>{renderResourceNode(props, key, node as ResourceTreeNode & dagre.Node, nodesHavingChildren)}</React.Fragment>;
  1329                      }
  1330                  })}
  1331                  {edges.map(edge => (
  1332                      <div key={`${edge.from}-${edge.to}`} className='application-resource-tree__edge'>
  1333                          {edge.lines.map((line, i) => {
  1334                              const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2));
  1335                              const xMid = (line.x1 + line.x2) / 2;
  1336                              const yMid = (line.y1 + line.y2) / 2;
  1337                              const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI;
  1338                              const lastLine = i === edge.lines.length - 1 ? line : null;
  1339                              let arrowColor = null;
  1340                              if (edge.colors) {
  1341                                  if (Array.isArray(edge.colors)) {
  1342                                      const firstColor = edge.colors[0];
  1343                                      if (firstColor.colors) {
  1344                                          arrowColor = firstColor.colors;
  1345                                      }
  1346                                  }
  1347                              }
  1348                              return (
  1349                                  <div
  1350                                      className='application-resource-tree__line'
  1351                                      key={i}
  1352                                      style={{
  1353                                          width: distance,
  1354                                          left: xMid - distance / 2,
  1355                                          top: yMid,
  1356                                          backgroundImage: edge.backgroundImage,
  1357                                          transform: props.useNetworkingHierarchy ? `translate(140px, 35px) rotate(${angle}deg)` : `translate(150px, 35px) rotate(${angle}deg)`
  1358                                      }}>
  1359                                      {lastLine && props.useNetworkingHierarchy && <ArrowConnector color={arrowColor} left={xMid + distance / 2} top={yMid} angle={angle} />}
  1360                                  </div>
  1361                              );
  1362                          })}
  1363                      </div>
  1364                  ))}
  1365              </div>
  1366          )
  1367      );
  1368  };