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