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 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 };