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

     1  import {DropDown} 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  
     7  import * as models from '../../../shared/models';
     8  
     9  import {EmptyState} from '../../../shared/components';
    10  import {Consumer} from '../../../shared/context';
    11  import {ApplicationURLs} from '../application-urls';
    12  import {ResourceIcon} from '../resource-icon';
    13  import {ResourceLabel} from '../resource-label';
    14  import {ComparisonStatusIcon, getAppOverridesCount, HealthStatusIcon, isAppNode, NodeId, nodeKey} from '../utils';
    15  import {NodeUpdateAnimation} from './node-update-animation';
    16  
    17  function treeNodeKey(node: NodeId & {uid?: string}) {
    18      return node.uid || nodeKey(node);
    19  }
    20  
    21  const color = require('color');
    22  
    23  require('./application-resource-tree.scss');
    24  
    25  export interface ResourceTreeNode extends models.ResourceNode {
    26      status?: models.SyncStatusCode;
    27      health?: models.HealthStatus;
    28      hook?: boolean;
    29      root?: ResourceTreeNode;
    30      requiresPruning?: boolean;
    31      orphaned?: boolean;
    32  }
    33  
    34  export interface ApplicationResourceTreeProps {
    35      app: models.Application;
    36      tree: models.ApplicationTree;
    37      useNetworkingHierarchy: boolean;
    38      nodeFilter: (node: ResourceTreeNode) => boolean;
    39      selectedNodeFullName?: string;
    40      onNodeClick?: (fullName: string) => any;
    41      nodeMenu?: (node: models.ResourceNode) => React.ReactNode;
    42      onClearFilter: () => any;
    43      showOrphanedResources: boolean;
    44  }
    45  
    46  interface Line {
    47      x1: number;
    48      y1: number;
    49      x2: number;
    50      y2: number;
    51  }
    52  
    53  const NODE_WIDTH = 282;
    54  const NODE_HEIGHT = 52;
    55  const FILTERED_INDICATOR_NODE = '__filtered_indicator__';
    56  const EXTERNAL_TRAFFIC_NODE = '__external_traffic__';
    57  const INTERNAL_TRAFFIC_NODE = '__internal_traffic__';
    58  const NODE_TYPES = {
    59      filteredIndicator: 'filtered_indicator',
    60      externalTraffic: 'external_traffic',
    61      externalLoadBalancer: 'external_load_balancer',
    62      internalTraffic: 'internal_traffic'
    63  };
    64  
    65  const BASE_COLORS = [
    66      '#0DADEA', // blue
    67      '#95D58F', // green
    68      '#F4C030', // orange
    69      '#FF6262', // red
    70      '#4B0082', // purple
    71      '#964B00' // brown
    72  ];
    73  
    74  // generate lots of colors with different darkness
    75  const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6]
    76      .map(darken =>
    77          BASE_COLORS.map(item =>
    78              color(item)
    79                  .darken(darken)
    80                  .hex()
    81          )
    82      )
    83      .reduce((first, second) => first.concat(second), []);
    84  
    85  function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
    86      let width = 0;
    87      let height = 0;
    88      nodes.forEach(node => {
    89          width = Math.max(node.x + node.width, width);
    90          height = Math.max(node.y + node.height, height);
    91      });
    92      return {width, height};
    93  }
    94  
    95  function filterGraph(app: models.Application, filteredIndicatorParent: string, graph: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
    96      const appKey = appNodeKey(app);
    97      let filtered = 0;
    98      graph.nodes().forEach(nodeId => {
    99          const node: ResourceTreeNode = graph.node(nodeId) as any;
   100          const parentIds = graph.predecessors(nodeId);
   101          if (node.root != null && !predicate(node) && appKey !== nodeId) {
   102              const childIds = graph.successors(nodeId);
   103              graph.removeNode(nodeId);
   104              filtered++;
   105              childIds.forEach((childId: any) => {
   106                  parentIds.forEach((parentId: any) => {
   107                      graph.setEdge(parentId, childId);
   108                  });
   109              });
   110          }
   111      });
   112      if (filtered) {
   113          graph.setNode(FILTERED_INDICATOR_NODE, {height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator});
   114          graph.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE);
   115      }
   116  }
   117  
   118  function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) {
   119      return `${(first.orphaned && '1') || '0'}/${nodeKey(first)}`.localeCompare(`${(second.orphaned && '1') || '0'}/${nodeKey(second)}`);
   120  }
   121  
   122  function appNodeKey(app: models.Application) {
   123      return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
   124  }
   125  
   126  function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) {
   127      const indicators = new Array<number>();
   128      let count = Math.min(node.count - 1, 3);
   129      while (count > 0) {
   130          indicators.push(count--);
   131      }
   132      return (
   133          <React.Fragment>
   134              <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
   135                  <div className='application-resource-tree__node-kind-icon '>
   136                      <i className='icon fa fa-filter' />
   137                  </div>
   138                  <div className='application-resource-tree__node-content-wrap-overflow'>
   139                      <a className='application-resource-tree__node-title' onClick={onClearFilter}>
   140                          clear filters to show {node.count} additional resource{node.count > 1 && 's'}
   141                      </a>
   142                  </div>
   143              </div>
   144              {indicators.map(i => (
   145                  <div
   146                      key={i}
   147                      className='application-resource-tree__node application-resource-tree__filtered-indicator'
   148                      style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
   149                  />
   150              ))}
   151          </React.Fragment>
   152      );
   153  }
   154  
   155  function renderTrafficNode(node: dagre.Node) {
   156      return (
   157          <div style={{position: 'absolute', left: 0, top: node.y, width: node.width, height: node.height}}>
   158              <div className='application-resource-tree__node-kind-icon' style={{fontSize: '2em'}}>
   159                  <i className='icon fa fa-cloud' />
   160              </div>
   161          </div>
   162      );
   163  }
   164  
   165  function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
   166      return (
   167          <div
   168              className='application-resource-tree__node application-resource-tree__node--load-balancer'
   169              style={{
   170                  left: node.x,
   171                  top: node.y,
   172                  width: node.width,
   173                  height: node.height
   174              }}>
   175              <div className='application-resource-tree__node-kind-icon'>
   176                  <i title={node.kind} className={`icon fa fa-network-wired`} style={{color: node.color}} />
   177              </div>
   178              <div className='application-resource-tree__node-content'>
   179                  <span className='application-resource-tree__node-title'>{node.label}</span>
   180              </div>
   181          </div>
   182      );
   183  }
   184  
   185  export const describeNode = (node: ResourceTreeNode) => {
   186      const lines = [`Kind: ${node.kind}`, `Namespace: ${node.namespace}`, `Name: ${node.name}`];
   187      if (node.images) {
   188          lines.push('Images:');
   189          node.images.forEach(i => lines.push(`- ${i}`));
   190      }
   191      return lines.join('\n');
   192  };
   193  
   194  function renderResourceNode(props: ApplicationResourceTreeProps, id: string, node: (ResourceTreeNode) & dagre.Node) {
   195      const fullName = nodeKey(node);
   196      let comparisonStatus: models.SyncStatusCode = null;
   197      let healthState: models.HealthStatus = null;
   198      if (node.status || node.health) {
   199          comparisonStatus = node.status;
   200          healthState = node.health;
   201      }
   202      const appNode = isAppNode(node);
   203      const rootNode = !node.root;
   204      return (
   205          <div
   206              onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
   207              className={classNames('application-resource-tree__node', {
   208                  'active': fullName === props.selectedNodeFullName,
   209                  'application-resource-tree__node--orphaned': node.orphaned
   210              })}
   211              title={describeNode(node)}
   212              style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
   213              {!appNode && <NodeUpdateAnimation resourceVersion={node.resourceVersion} />}
   214              <div
   215                  className={classNames('application-resource-tree__node-kind-icon', {
   216                      'application-resource-tree__node-kind-icon--big': rootNode
   217                  })}>
   218                  <ResourceIcon kind={node.kind} />
   219                  <br />
   220                  {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
   221              </div>
   222              <div className='application-resource-tree__node-content'>
   223                  <span className='application-resource-tree__node-title'>{node.name}</span>
   224                  <br />
   225                  <span
   226                      className={classNames('application-resource-tree__node-status-icon', {
   227                          'application-resource-tree__node-status-icon--offset': rootNode
   228                      })}>
   229                      {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
   230                      {healthState != null && <HealthStatusIcon state={healthState} />}
   231                      {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
   232                      {appNode && !rootNode && (
   233                          <Consumer>
   234                              {ctx => (
   235                                  <a href={ctx.baseHref + 'applications/' + node.name} title='Open application'>
   236                                      <i className='fa fa-external-link-alt' />
   237                                  </a>
   238                              )}
   239                          </Consumer>
   240                      )}
   241                      <ApplicationURLs urls={rootNode ? props.app.status.summary.externalURLs : node.networkingInfo && node.networkingInfo.externalURLs} />
   242                  </span>
   243              </div>
   244              <div className='application-resource-tree__node-labels'>
   245                  {node.createdAt ? (
   246                      <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
   247                          {node.createdAt}
   248                      </Moment>
   249                  ) : null}
   250                  {(node.info || []).map((tag, i) => (
   251                      <span className='application-resource-tree__node-label' title={`${tag.name}:${tag.value}`} key={i}>
   252                          {tag.value}
   253                      </span>
   254                  ))}
   255              </div>
   256              {props.nodeMenu && (
   257                  <div className='application-resource-tree__node-menu'>
   258                      <DropDown
   259                          isMenu={true}
   260                          anchor={() => (
   261                              <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   262                                  <i className='fa fa-ellipsis-v' />
   263                              </button>
   264                          )}>
   265                          {() => props.nodeMenu(node)}
   266                      </DropDown>
   267                  </div>
   268              )}
   269          </div>
   270      );
   271  }
   272  
   273  function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] {
   274      let result = new Array<ResourceTreeNode>();
   275      const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey));
   276      result = result.concat(nodes.filter(target => refs.has(nodeKey(target))));
   277      if (networkingInfo.targetLabels) {
   278          result = result.concat(
   279              nodes.filter(target => {
   280                  if (target.networkingInfo && target.networkingInfo.labels) {
   281                      return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]);
   282                  }
   283                  return false;
   284              })
   285          );
   286      }
   287      return result;
   288  }
   289  export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
   290      const graph = new dagre.graphlib.Graph();
   291      graph.setGraph({nodesep: 15, rankdir: 'LR', marginx: -100});
   292      graph.setDefaultEdgeLabel(() => ({}));
   293      const overridesCount = getAppOverridesCount(props.app);
   294      const appNode = {
   295          kind: props.app.kind,
   296          name: props.app.metadata.name,
   297          namespace: props.app.metadata.namespace,
   298          resourceVersion: props.app.metadata.resourceVersion,
   299          group: 'argoproj.io',
   300          version: '',
   301          children: Array(),
   302          status: props.app.status.sync.status,
   303          health: props.app.status.health,
   304          info:
   305              overridesCount > 0
   306                  ? [
   307                        {
   308                            name: 'Parameter overrides',
   309                            value: `${overridesCount} parameter override(s)`
   310                        }
   311                    ]
   312                  : []
   313      };
   314  
   315      const statusByKey = new Map<string, models.ResourceStatus>();
   316      props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
   317      const nodeByKey = new Map<string, ResourceTreeNode>();
   318      props.tree.nodes
   319          .map(node => ({...node, orphaned: false}))
   320          .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
   321          .forEach(node => {
   322              const status = statusByKey.get(nodeKey(node));
   323              const resourceNode: ResourceTreeNode = {...node};
   324              if (status) {
   325                  resourceNode.health = status.health;
   326                  resourceNode.status = status.status;
   327                  resourceNode.hook = status.hook;
   328                  resourceNode.requiresPruning = status.requiresPruning;
   329              }
   330              nodeByKey.set(treeNodeKey(node), resourceNode);
   331          });
   332      const nodes = Array.from(nodeByKey.values());
   333      let roots: ResourceTreeNode[] = [];
   334      const childrenByParentKey = new Map<string, ResourceTreeNode[]>();
   335      if (props.useNetworkingHierarchy) {
   336          // Network view
   337          const hasParents = new Set<string>();
   338          const networkNodes = nodes.filter(node => node.networkingInfo);
   339          networkNodes.forEach(parent => {
   340              findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => {
   341                  const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
   342                  hasParents.add(treeNodeKey(child));
   343                  children.push(child);
   344                  childrenByParentKey.set(treeNodeKey(parent), children);
   345              });
   346          });
   347          roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node)));
   348          const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes);
   349          const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes);
   350          const colorsBySource = new Map<string, string>();
   351          // sources are root internal services and external ingress/service IPs
   352          const sources = Array.from(
   353              new Set(
   354                  internalRoots
   355                      .map(root => treeNodeKey(root))
   356                      .concat(
   357                          externalRoots.map(root => root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), [])
   358                      )
   359              )
   360          );
   361          // assign unique color to each traffic source
   362          sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
   363  
   364          if (externalRoots.length > 0) {
   365              graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic});
   366              externalRoots.sort(compareNodes).forEach(root => {
   367                  const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip);
   368                  processNode(root, root, loadBalancers.map(lb => colorsBySource.get(lb)));
   369                  loadBalancers.forEach(key => {
   370                      const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`;
   371                      graph.setNode(loadBalancerNodeKey, {
   372                          height: NODE_HEIGHT,
   373                          width: NODE_WIDTH,
   374                          type: NODE_TYPES.externalLoadBalancer,
   375                          label: key,
   376                          color: colorsBySource.get(key)
   377                      });
   378                      graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]});
   379                      graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]});
   380                  });
   381              });
   382          }
   383  
   384          if (internalRoots.length > 0) {
   385              graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic});
   386              internalRoots.forEach(root => {
   387                  processNode(root, root, [colorsBySource.get(treeNodeKey(root))]);
   388                  graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root));
   389              });
   390          }
   391          if (props.nodeFilter) {
   392              // show filtered indicator next to external traffic node is app has it otherwise next to internal traffic node
   393              filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter);
   394          }
   395      } else {
   396          // Tree view
   397          const managedKeys = new Set(props.app.status.resources.map(nodeKey));
   398          const orphans: ResourceTreeNode[] = [];
   399          nodes.forEach(node => {
   400              if ((node.parentRefs || []).length === 0 || managedKeys.has(nodeKey(node))) {
   401                  roots.push(node);
   402              } else {
   403                  orphans.push(node);
   404                  node.parentRefs.forEach(parent => {
   405                      const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
   406                      children.push(node);
   407                      childrenByParentKey.set(treeNodeKey(parent), children);
   408                  });
   409              }
   410          });
   411          roots.sort(compareNodes).forEach(node => {
   412              processNode(node, node);
   413              graph.setEdge(appNodeKey(props.app), treeNodeKey(node));
   414          });
   415          orphans.sort(compareNodes).forEach(node => {
   416              processNode(node, node);
   417          });
   418          graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT});
   419          if (props.nodeFilter) {
   420              filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter);
   421          }
   422      }
   423  
   424      function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) {
   425          graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root});
   426          (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => {
   427              if (treeNodeKey(child) === treeNodeKey(root)) {
   428                  return;
   429              }
   430              graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors});
   431              processNode(child, root, colors);
   432          });
   433      }
   434      dagre.layout(graph);
   435  
   436      const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string}[] = [];
   437      graph.edges().forEach(edgeInfo => {
   438          const edge = graph.edge(edgeInfo);
   439          const colors = (edge.colors as string[]) || [];
   440          let backgroundImage: string;
   441          if (colors.length > 0) {
   442              const step = 100 / colors.length;
   443              const gradient = colors.map((lineColor, i) => {
   444                  return `${lineColor} ${step * i}%, ${lineColor} ${step * i + step / 2}%, transparent ${step * i + step / 2}%, transparent ${step * (i + 1)}%`;
   445              });
   446              backgroundImage = `linear-gradient(90deg, ${gradient})`;
   447          }
   448  
   449          const lines: Line[] = [];
   450          // don't render connections from hidden node representing internal traffic
   451          if (edgeInfo.v === INTERNAL_TRAFFIC_NODE || edgeInfo.w === INTERNAL_TRAFFIC_NODE) {
   452              return;
   453          }
   454          if (edge.points.length > 1) {
   455              for (let i = 1; i < edge.points.length; i++) {
   456                  lines.push({x1: edge.points[i - 1].x, y1: edge.points[i - 1].y, x2: edge.points[i].x, y2: edge.points[i].y});
   457              }
   458          }
   459          edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage});
   460      });
   461      const graphNodes = graph.nodes();
   462      const size = getGraphSize(graphNodes.map(id => graph.node(id)));
   463      return (
   464          (graphNodes.length === 0 && (
   465              <EmptyState icon=' fa fa-network-wired'>
   466                  <h4>Your application has no network resources</h4>
   467                  <h5>Try switching to tree or list view</h5>
   468              </EmptyState>
   469          )) || (
   470              <div
   471                  className={classNames('application-resource-tree', {'application-resource-tree--network': props.useNetworkingHierarchy})}
   472                  style={{width: size.width + 150, height: size.height + 250}}>
   473                  {graphNodes.map(key => {
   474                      const node = graph.node(key);
   475                      const nodeType = node.type;
   476                      switch (nodeType) {
   477                          case NODE_TYPES.filteredIndicator:
   478                              return <React.Fragment key={key}>{renderFilteredNode(node as any, props.onClearFilter)}</React.Fragment>;
   479                          case NODE_TYPES.externalTraffic:
   480                              return <React.Fragment key={key}>{renderTrafficNode(node)}</React.Fragment>;
   481                          case NODE_TYPES.internalTraffic:
   482                              return null;
   483                          case NODE_TYPES.externalLoadBalancer:
   484                              return <React.Fragment key={key}>{renderLoadBalancerNode(node as any)}</React.Fragment>;
   485                          default:
   486                              return <React.Fragment key={key}>{renderResourceNode(props, key, node as (ResourceTreeNode) & dagre.Node)}</React.Fragment>;
   487                      }
   488                  })}
   489                  {edges.map(edge => (
   490                      <div key={`${edge.from}-${edge.to}`} className='application-resource-tree__edge'>
   491                          {edge.lines.map((line, i) => {
   492                              const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2));
   493                              const xMid = (line.x1 + line.x2) / 2;
   494                              const yMid = (line.y1 + line.y2) / 2;
   495                              const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI;
   496                              return (
   497                                  <div
   498                                      className='application-resource-tree__line'
   499                                      key={i}
   500                                      style={{
   501                                          width: distance,
   502                                          left: xMid - distance / 2,
   503                                          top: yMid,
   504                                          backgroundImage: edge.backgroundImage,
   505                                          transform: `translate(150px, 35px) rotate(${angle}deg)`
   506                                      }}
   507                                  />
   508                              );
   509                          })}
   510                      </div>
   511                  ))}
   512              </div>
   513          )
   514      );
   515  };