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

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