argo-cd
1294 строки · 58.8 Кб
1import {DropDown, DropDownMenu, Tooltip} from 'argo-ui';
2import * as classNames from 'classnames';
3import * as dagre from 'dagre';
4import * as React from 'react';
5import Moment from 'react-moment';
6import * as moment from 'moment';
7
8import * as models from '../../../shared/models';
9
10import {EmptyState} from '../../../shared/components';
11import {AppContext, Consumer} from '../../../shared/context';
12import {ApplicationURLs} from '../application-urls';
13import {ResourceIcon} from '../resource-icon';
14import {ResourceLabel} from '../resource-label';
15import {
16BASE_COLORS,
17ComparisonStatusIcon,
18deletePodAction,
19getAppOverridesCount,
20HealthStatusIcon,
21isAppNode,
22isYoungerThanXMinutes,
23NodeId,
24nodeKey,
25PodHealthIcon,
26getUsrMsgKeyToDisplay
27} from '../utils';
28import {NodeUpdateAnimation} from './node-update-animation';
29import {PodGroup} from '../application-pod-view/pod-view';
30import './application-resource-tree.scss';
31import {ArrowConnector} from './arrow-connector';
32
33function treeNodeKey(node: NodeId & {uid?: string}) {
34return node.uid || nodeKey(node);
35}
36
37const color = require('color');
38
39export interface ResourceTreeNode extends models.ResourceNode {
40status?: models.SyncStatusCode;
41health?: models.HealthStatus;
42hook?: boolean;
43root?: ResourceTreeNode;
44requiresPruning?: boolean;
45orphaned?: boolean;
46podGroup?: PodGroup;
47isExpanded?: boolean;
48}
49
50export interface ApplicationResourceTreeProps {
51app: models.Application;
52tree: models.ApplicationTree;
53useNetworkingHierarchy: boolean;
54nodeFilter: (node: ResourceTreeNode) => boolean;
55selectedNodeFullName?: string;
56onNodeClick?: (fullName: string) => any;
57onGroupdNodeClick?: (groupedNodeIds: string[]) => any;
58nodeMenu?: (node: models.ResourceNode) => React.ReactNode;
59onClearFilter: () => any;
60appContext?: AppContext;
61showOrphanedResources: boolean;
62showCompactNodes: boolean;
63userMsgs: models.UserMessages[];
64updateUsrHelpTipMsgs: (userMsgs: models.UserMessages) => void;
65setShowCompactNodes: (showCompactNodes: boolean) => void;
66zoom: number;
67podGroupCount: number;
68filters?: string[];
69setTreeFilterGraph?: (filterGraph: any[]) => void;
70nameDirection: boolean;
71setNodeExpansion: (node: string, isExpanded: boolean) => any;
72getNodeExpansion: (node: string) => boolean;
73}
74
75interface Line {
76x1: number;
77y1: number;
78x2: number;
79y2: number;
80}
81
82const NODE_WIDTH = 282;
83const NODE_HEIGHT = 52;
84const POD_NODE_HEIGHT = 136;
85const FILTERED_INDICATOR_NODE = '__filtered_indicator__';
86const EXTERNAL_TRAFFIC_NODE = '__external_traffic__';
87const INTERNAL_TRAFFIC_NODE = '__internal_traffic__';
88const NODE_TYPES = {
89filteredIndicator: 'filtered_indicator',
90externalTraffic: 'external_traffic',
91externalLoadBalancer: 'external_load_balancer',
92internalTraffic: 'internal_traffic',
93groupedNodes: 'grouped_nodes',
94podGroup: 'pod_group'
95};
96// generate lots of colors with different darkness
97const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6]
98.map(darken =>
99BASE_COLORS.map(item =>
100color(item)
101.darken(darken)
102.hex()
103)
104)
105.reduce((first, second) => first.concat(second), []);
106
107function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
108let width = 0;
109let height = 0;
110nodes.forEach(node => {
111width = Math.max(node.x + node.width, width);
112height = Math.max(node.y + node.height, height);
113});
114return {width, height};
115}
116
117function groupNodes(nodes: ResourceTreeNode[], graph: dagre.graphlib.Graph) {
118function getNodeGroupingInfo(nodeId: string) {
119const node = graph.node(nodeId);
120return {
121nodeId,
122kind: node.kind,
123parentIds: graph.predecessors(nodeId),
124childIds: graph.successors(nodeId)
125};
126}
127
128function filterNoChildNode(nodeInfo: {childIds: dagre.Node[]}) {
129return nodeInfo.childIds.length === 0;
130}
131
132// create nodes array with parent/child nodeId
133const nodesInfoArr = graph.nodes().map(getNodeGroupingInfo);
134
135// group sibling nodes into a 2d array
136const siblingNodesArr = nodesInfoArr
137.reduce((acc, curr) => {
138if (curr.childIds.length > 1) {
139acc.push(curr.childIds.map(nodeId => getNodeGroupingInfo(nodeId.toString())));
140}
141return acc;
142}, [])
143.map(nodeArr => nodeArr.filter(filterNoChildNode));
144
145// group sibling nodes with same kind
146const groupedNodesArr = siblingNodesArr
147.map(eachLevel => {
148return eachLevel.reduce(
149(groupedNodesInfo: {kind: string; nodeIds?: string[]; parentIds?: dagre.Node[]}[], currentNodeInfo: {kind: string; nodeId: string; parentIds: dagre.Node[]}) => {
150const index = groupedNodesInfo.findIndex((nodeInfo: {kind: string}) => currentNodeInfo.kind === nodeInfo.kind);
151if (index > -1) {
152groupedNodesInfo[index].nodeIds.push(currentNodeInfo.nodeId);
153}
154
155if (groupedNodesInfo.length === 0 || index < 0) {
156const nodeIdArr = [];
157nodeIdArr.push(currentNodeInfo.nodeId);
158const groupedNodesInfoObj = {
159kind: currentNodeInfo.kind,
160nodeIds: nodeIdArr,
161parentIds: currentNodeInfo.parentIds
162};
163groupedNodesInfo.push(groupedNodesInfoObj);
164}
165
166return groupedNodesInfo;
167},
168[]
169);
170})
171.reduce((flattedNodesGroup, groupedNodes) => {
172return flattedNodesGroup.concat(groupedNodes);
173}, [])
174.filter((eachArr: {nodeIds: string[]}) => eachArr.nodeIds.length > 1);
175
176// update graph
177if (groupedNodesArr.length > 0) {
178groupedNodesArr.forEach((obj: {kind: string; nodeIds: string[]; parentIds: dagre.Node[]}) => {
179const {nodeIds, kind, parentIds} = obj;
180const groupedNodeIds: string[] = [];
181const podGroupIds: string[] = [];
182nodeIds.forEach((nodeId: string) => {
183const index = nodes.findIndex(node => nodeId === node.uid || nodeId === nodeKey(node));
184const graphNode = graph.node(nodeId);
185if (!graphNode?.podGroup && index > -1) {
186groupedNodeIds.push(nodeId);
187} else {
188podGroupIds.push(nodeId);
189}
190});
191const reducedNodeIds = nodeIds.reduce((acc, aNodeId) => {
192if (podGroupIds.findIndex(i => i === aNodeId) < 0) {
193acc.push(aNodeId);
194}
195return acc;
196}, []);
197if (groupedNodeIds.length > 1) {
198groupedNodeIds.forEach(n => graph.removeNode(n));
199graph.setNode(`${parentIds[0].toString()}/child/${kind}`, {
200kind,
201groupedNodeIds,
202height: NODE_HEIGHT,
203width: NODE_WIDTH,
204count: reducedNodeIds.length,
205type: NODE_TYPES.groupedNodes
206});
207graph.setEdge(parentIds[0].toString(), `${parentIds[0].toString()}/child/${kind}`);
208}
209});
210}
211}
212
213export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) {
214function orphanedToInt(orphaned?: boolean) {
215return (orphaned && 1) || 0;
216}
217function compareRevision(a: string, b: string) {
218const numberA = Number(a);
219const numberB = Number(b);
220if (isNaN(numberA) || isNaN(numberB)) {
221return a.localeCompare(b);
222}
223return Math.sign(numberA - numberB);
224}
225function getRevision(a: ResourceTreeNode) {
226const filtered = (a.info || []).filter(b => b.name === 'Revision' && b)[0];
227if (filtered == null) {
228return '';
229}
230const value = filtered.value;
231if (value == null) {
232return '';
233}
234return value.replace(/^Rev:/, '');
235}
236if (first.kind === 'ReplicaSet') {
237return (
238orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
239compareRevision(getRevision(second), getRevision(first)) ||
240nodeKey(first).localeCompare(nodeKey(second)) ||
2410
242);
243}
244return (
245orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
246nodeKey(first).localeCompare(nodeKey(second)) ||
247compareRevision(getRevision(first), getRevision(second)) ||
2480
249);
250}
251
252function appNodeKey(app: models.Application) {
253return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
254}
255
256function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) {
257const indicators = new Array<number>();
258let count = Math.min(node.count - 1, 3);
259while (count > 0) {
260indicators.push(count--);
261}
262return (
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}>
270clear filters to show {node.count} additional resource{node.count > 1 && 's'}
271</a>
272</div>
273</div>
274{indicators.map(i => (
275<div
276key={i}
277className='application-resource-tree__node application-resource-tree__filtered-indicator'
278style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
279/>
280))}
281</React.Fragment>
282);
283}
284
285function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number} & dagre.Node & ResourceTreeNode) {
286const indicators = new Array<number>();
287let count = Math.min(node.count - 1, 3);
288while (count > 0) {
289indicators.push(count--);
290}
291return (
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
300className='application-resource-tree__node-title application-resource-tree__direction-center-left'
301onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}
302title={`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
307className='fa-solid fa-cart-flatbed icon-background'
308title={`Click to see details of ${node.count} collapsed ${node.kind} and doesn't contains any active pods`}
309key={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
319key={i}
320className='application-resource-tree__node application-resource-tree__filtered-indicator'
321style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
322/>
323))}
324</React.Fragment>
325);
326}
327
328function renderTrafficNode(node: dagre.Node) {
329return (
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
338function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
339return (
340<div
341className='application-resource-tree__node application-resource-tree__node--load-balancer'
342style={{
343left: node.x,
344top: node.y,
345width: node.width,
346height: 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
358export const describeNode = (node: ResourceTreeNode) => {
359const lines = [`Kind: ${node.kind}`, `Namespace: ${node.namespace || '(global)'}`, `Name: ${node.name}`];
360if (node.images) {
361lines.push('Images:');
362node.images.forEach(i => lines.push(`- ${i}`));
363}
364return lines.join('\n');
365};
366
367function processPodGroup(targetPodGroup: ResourceTreeNode, child: ResourceTreeNode, props: ApplicationResourceTreeProps) {
368if (!targetPodGroup.podGroup) {
369const fullName = nodeKey(targetPodGroup);
370if ((targetPodGroup.parentRefs || []).length === 0) {
371targetPodGroup.root = targetPodGroup;
372}
373targetPodGroup.podGroup = {
374pods: [] as models.Pod[],
375fullName,
376...targetPodGroup.podGroup,
377...targetPodGroup,
378info: (targetPodGroup.info || []).filter(i => !i.name.includes('Resource.')),
379createdAt: targetPodGroup.createdAt,
380renderMenu: () => props.nodeMenu(targetPodGroup),
381kind: targetPodGroup.kind,
382type: 'parentResource',
383name: targetPodGroup.name
384};
385}
386if (child.kind === 'Pod') {
387const p: models.Pod = {
388...child,
389fullName: nodeKey(child),
390metadata: {name: child.name},
391spec: {nodeName: 'Unknown'},
392health: child.health ? child.health.status : 'Unknown'
393} as models.Pod;
394
395// Get node name for Pod
396child.info?.forEach(i => {
397if (i.name === 'Node') {
398p.spec.nodeName = i.value;
399}
400});
401targetPodGroup.podGroup.pods.push(p);
402}
403}
404
405function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, childMap: Map<string, ResourceTreeNode[]>) {
406const fullName = nodeKey(node);
407let comparisonStatus: models.SyncStatusCode = null;
408let healthState: models.HealthStatus = null;
409if (node.status || node.health) {
410comparisonStatus = node.status;
411healthState = node.health;
412}
413const appNode = isAppNode(node);
414const rootNode = !node.root;
415const extLinks: string[] = props.app.status.summary.externalURLs;
416const podGroupChildren = childMap.get(treeNodeKey(node));
417const nonPodChildren = podGroupChildren?.reduce((acc, child) => {
418if (child.kind !== 'Pod') {
419acc.push(child);
420}
421return acc;
422}, []);
423const childCount = nonPodChildren?.length;
424const margin = 8;
425let topExtra = 0;
426const podGroup = node.podGroup;
427const podGroupHealthy = [];
428const podGroupDegraded = [];
429const podGroupInProgress = [];
430
431for (const pod of podGroup?.pods || []) {
432switch (pod.health) {
433case 'Healthy':
434podGroupHealthy.push(pod);
435break;
436case 'Degraded':
437podGroupDegraded.push(pod);
438break;
439case 'Progressing':
440podGroupInProgress.push(pod);
441}
442}
443
444const showPodGroupByStatus = props.tree.nodes.filter((rNode: ResourceTreeNode) => rNode.kind === 'Pod').length >= props.podGroupCount;
445const 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
449if (podGroup) {
450topExtra = margin + (POD_NODE_HEIGHT / 2 + 30 * numberOfRows) / 2;
451}
452
453return (
454<div
455className={classNames('application-resource-tree__node', {
456'active': fullName === props.selectedNodeFullName,
457'application-resource-tree__node--orphaned': node.orphaned
458})}
459title={describeNode(node)}
460style={{
461left: node.x,
462top: node.y - topExtra,
463width: node.width,
464height: 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
469className={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
478className={classNames('application-resource-tree__node-title', {
479'application-resource-tree__direction-right': props.nameDirection,
480'application-resource-tree__direction-left': !props.nameDirection
481})}
482onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}>
483{node.name}
484</span>
485<span
486className={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
507style={{top: node.height / 2 - 6}}
508className='application-resource-tree__node--podgroup--expansion'
509onClick={event => {
510expandCollapse(node, props);
511event.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
534content={
535<>
536{(node.info || []).map(i => (
537<div key={i.name}>
538{i.name}: {i.value}
539</div>
540))}
541</>
542}
543key={node.uid}>
544<span className='application-resource-tree__node-label' title='More'>
545More
546</span>
547</Tooltip>
548)}
549</div>
550{props.nodeMenu && (
551<div className='application-resource-tree__node-menu'>
552<DropDown
553key={node.uid}
554isMenu={true}
555anchor={() => (
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) => {
567if (pods.length > 0) {
568return (
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
580function renderPodGroupByStatus(props: ApplicationResourceTreeProps, node: any, pods: models.Pod[], showPodGroupByStatus: boolean) {
581return (
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
591className='application-resource-tree__node-title'
592onClick={() =>
593props.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) : (
603pods.map(pod => (
604<DropDownMenu
605key={pod.uid}
606anchor={() => (
607<Tooltip
608content={
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}
623popperOptions={{
624modifiers: {
625preventOverflow: {
626enabled: true
627},
628hide: {
629enabled: false
630},
631flip: {
632enabled: false
633}
634}
635}}
636key={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
642className={`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)}
648items={[
649{
650title: (
651<React.Fragment>
652<i className='fa fa-info-circle' /> Info
653</React.Fragment>
654),
655action: () => props.onNodeClick(pod.fullName)
656},
657{
658title: (
659<React.Fragment>
660<i className='fa fa-align-left' /> Logs
661</React.Fragment>
662),
663action: () => {
664props.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'logs'}, {replace: true});
665}
666},
667{
668title: (
669<React.Fragment>
670<i className='fa fa-times-circle' /> Delete
671</React.Fragment>
672),
673action: () => {
674deletePodAction(pod, props.appContext, props.app.metadata.name, props.app.metadata.namespace);
675}
676}
677]}
678/>
679))
680)}
681</div>
682);
683}
684
685function expandCollapse(node: ResourceTreeNode, props: ApplicationResourceTreeProps) {
686const isExpanded = !props.getNodeExpansion(node.uid);
687node.isExpanded = isExpanded;
688props.setNodeExpansion(node.uid, isExpanded);
689}
690
691function NodeInfoDetails({tag: tag, kind: kind}: {tag: models.InfoItem; kind: string}) {
692if (kind === 'Pod') {
693const val = `${tag.name}`;
694if (val === 'Status Reason') {
695if (`${tag.value}` !== 'ImagePullBackOff')
696return (
697<span className='application-resource-tree__node-label' title={`Status: ${tag.value}`}>
698{tag.value}
699</span>
700);
701else {
702return (
703<span
704className='application-resource-tree__node-label'
705title='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') {
711const arr = `${tag.value}`.split('/');
712const title = `Number of containers in total: ${arr[1]} \nNumber of ready containers: ${arr[0]}`;
713return (
714<span className='application-resource-tree__node-label' title={`${title}`}>
715{tag.value}
716</span>
717);
718} else if (val === 'Restart Count') {
719return (
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') {
725return (
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 {
731return (
732<span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
733{tag.value}
734</span>
735);
736}
737} else {
738return (
739<span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
740{tag.value}
741</span>
742);
743}
744}
745
746function renderResourceNode(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, nodesHavingChildren: Map<string, number>) {
747const fullName = nodeKey(node);
748let comparisonStatus: models.SyncStatusCode = null;
749let healthState: models.HealthStatus = null;
750if (node.status || node.health) {
751comparisonStatus = node.status;
752healthState = node.health;
753}
754const appNode = isAppNode(node);
755const rootNode = !node.root;
756const extLinks: string[] = props.app.status.summary.externalURLs;
757const childCount = nodesHavingChildren.get(node.uid);
758return (
759<div
760onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
761className={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})}
765title={describeNode(node)}
766style={{
767left: node.x,
768top: node.y,
769width: node.width,
770height: node.height
771}}>
772{!appNode && <NodeUpdateAnimation resourceVersion={node.resourceVersion} />}
773<div
774className={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
783className={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
790className={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
809className='application-resource-tree__node--expansion'
810onClick={event => {
811expandCollapse(node, props);
812event.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) => {
830return <NodeInfoDetails tag={tag} kind={node.kind} key={i} />;
831})}
832{(node.info || []).length > 4 && (
833<Tooltip
834content={
835<>
836{(node.info || []).map(i => (
837<div key={i.name}>
838{i.name}: {i.value}
839</div>
840))}
841</>
842}
843key={node.uid}>
844<span className='application-resource-tree__node-label' title='More'>
845More
846</span>
847</Tooltip>
848)}
849</div>
850{props.nodeMenu && (
851<div className='application-resource-tree__node-menu'>
852<DropDown
853isMenu={true}
854anchor={() => (
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
867function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] {
868let result = new Array<ResourceTreeNode>();
869const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey));
870result = result.concat(nodes.filter(target => refs.has(nodeKey(target))));
871if (networkingInfo.targetLabels) {
872result = result.concat(
873nodes.filter(target => {
874if (target.networkingInfo && target.networkingInfo.labels) {
875return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]);
876}
877return false;
878})
879);
880}
881return result;
882}
883export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
884const graph = new dagre.graphlib.Graph();
885graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80});
886graph.setDefaultEdgeLabel(() => ({}));
887const overridesCount = getAppOverridesCount(props.app);
888const appNode = {
889kind: props.app.kind,
890name: props.app.metadata.name,
891namespace: props.app.metadata.namespace,
892resourceVersion: props.app.metadata.resourceVersion,
893group: 'argoproj.io',
894version: '',
895children: Array(),
896status: props.app.status.sync.status,
897health: props.app.status.health,
898uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name,
899info:
900overridesCount > 0
901? [
902{
903name: 'Parameter overrides',
904value: `${overridesCount} parameter override(s)`
905}
906]
907: []
908};
909
910const statusByKey = new Map<string, models.ResourceStatus>();
911props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
912const nodeByKey = new Map<string, ResourceTreeNode>();
913props.tree.nodes
914.map(node => ({...node, orphaned: false}))
915.concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
916.forEach(node => {
917const status = statusByKey.get(nodeKey(node));
918const resourceNode: ResourceTreeNode = {...node};
919if (status) {
920resourceNode.health = status.health;
921resourceNode.status = status.status;
922resourceNode.hook = status.hook;
923resourceNode.requiresPruning = status.requiresPruning;
924}
925nodeByKey.set(treeNodeKey(node), resourceNode);
926});
927const nodes = Array.from(nodeByKey.values());
928let roots: ResourceTreeNode[] = [];
929const childrenByParentKey = new Map<string, ResourceTreeNode[]>();
930const nodesHavingChildren = new Map<string, number>();
931const childrenMap = new Map<string, ResourceTreeNode[]>();
932const [filters, setFilters] = React.useState(props.filters);
933const [filteredGraph, setFilteredGraph] = React.useState([]);
934const filteredNodes: any[] = [];
935
936React.useEffect(() => {
937if (props.filters !== filters) {
938setFilters(props.filters);
939setFilteredGraph(filteredNodes);
940props.setTreeFilterGraph(filteredGraph);
941}
942}, [props.filters]);
943const {podGroupCount, userMsgs, updateUsrHelpTipMsgs, setShowCompactNodes} = props;
944const podCount = nodes.filter(node => node.kind === 'Pod').length;
945
946React.useEffect(() => {
947if (podCount > podGroupCount) {
948const userMsg = getUsrMsgKeyToDisplay(appNode.name, 'groupNodes', userMsgs);
949updateUsrHelpTipMsgs(userMsg);
950if (!userMsg.display) {
951setShowCompactNodes(true);
952}
953}
954}, [podCount]);
955
956function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
957const appKey = appNodeKey(app);
958let filtered = 0;
959graphNodesFilter.nodes().forEach(nodeId => {
960const node: ResourceTreeNode = graphNodesFilter.node(nodeId) as any;
961const parentIds = graphNodesFilter.predecessors(nodeId);
962if (node.root != null && !predicate(node) && appKey !== nodeId) {
963const childIds = graphNodesFilter.successors(nodeId);
964graphNodesFilter.removeNode(nodeId);
965filtered++;
966childIds.forEach((childId: any) => {
967parentIds.forEach((parentId: any) => {
968graphNodesFilter.setEdge(parentId, childId);
969});
970});
971} else {
972if (node.root != null) filteredNodes.push(node);
973}
974});
975if (filtered) {
976graphNodesFilter.setNode(FILTERED_INDICATOR_NODE, {height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator});
977graphNodesFilter.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE);
978}
979}
980
981if (props.useNetworkingHierarchy) {
982// Network view
983const hasParents = new Set<string>();
984const networkNodes = nodes.filter(node => node.networkingInfo);
985const hiddenNodes: ResourceTreeNode[] = [];
986networkNodes.forEach(parent => {
987findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => {
988const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
989hasParents.add(treeNodeKey(child));
990const parentId = parent.uid;
991if (nodesHavingChildren.has(parentId)) {
992nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
993} else {
994nodesHavingChildren.set(parentId, 1);
995}
996if (child.kind !== 'Pod' || !props.showCompactNodes) {
997if (props.getNodeExpansion(parentId)) {
998hasParents.add(treeNodeKey(child));
999children.push(child);
1000childrenByParentKey.set(treeNodeKey(parent), children);
1001} else {
1002hiddenNodes.push(child);
1003}
1004} else {
1005processPodGroup(parent, child, props);
1006}
1007});
1008});
1009roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node)));
1010roots = roots.reduce((acc, curr) => {
1011if (hiddenNodes.indexOf(curr) < 0) {
1012acc.push(curr);
1013}
1014return acc;
1015}, []);
1016const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes);
1017const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes);
1018const colorsBySource = new Map<string, string>();
1019// sources are root internal services and external ingress/service IPs
1020const sources = Array.from(
1021new Set(
1022internalRoots
1023.map(root => treeNodeKey(root))
1024.concat(
1025externalRoots.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
1030sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
1031
1032if (externalRoots.length > 0) {
1033graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic});
1034externalRoots.sort(compareNodes).forEach(root => {
1035const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip);
1036const 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) => {
1039processNode(child, root, [colorByService.get(treeNodeKey(child))]);
1040});
1041if (root.podGroup && props.showCompactNodes) {
1042setPodGroupNode(root, root);
1043} else {
1044graph.setNode(treeNodeKey(root), {...root, width: NODE_WIDTH, height: NODE_HEIGHT, root});
1045}
1046(childrenByParentKey.get(treeNodeKey(root)) || []).forEach(child => {
1047if (root.namespace === child.namespace) {
1048graph.setEdge(treeNodeKey(root), treeNodeKey(child), {colors: [colorByService.get(treeNodeKey(child))]});
1049}
1050});
1051loadBalancers.forEach(key => {
1052const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`;
1053graph.setNode(loadBalancerNodeKey, {
1054height: NODE_HEIGHT,
1055width: NODE_WIDTH,
1056type: NODE_TYPES.externalLoadBalancer,
1057label: key,
1058color: colorsBySource.get(key)
1059});
1060graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]});
1061graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]});
1062});
1063});
1064}
1065
1066if (internalRoots.length > 0) {
1067graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic});
1068internalRoots.forEach(root => {
1069processNode(root, root, [colorsBySource.get(treeNodeKey(root))]);
1070graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root));
1071});
1072}
1073if (props.nodeFilter) {
1074// show filtered indicator next to external traffic node is app has it otherwise next to internal traffic node
1075filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter);
1076}
1077} else {
1078// Tree view
1079const managedKeys = new Set(props.app.status.resources.map(nodeKey));
1080const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey));
1081const orphans: ResourceTreeNode[] = [];
1082let allChildNodes: ResourceTreeNode[] = [];
1083nodesHavingChildren.set(appNode.uid, 1);
1084if (props.getNodeExpansion(appNode.uid)) {
1085nodes.forEach(node => {
1086allChildNodes = [];
1087if ((node.parentRefs || []).length === 0 || managedKeys.has(nodeKey(node))) {
1088roots.push(node);
1089} else {
1090if (orphanedKeys.has(nodeKey(node))) {
1091orphans.push(node);
1092}
1093node.parentRefs.forEach(parent => {
1094const parentId = treeNodeKey(parent);
1095const children = childrenByParentKey.get(parentId) || [];
1096if (nodesHavingChildren.has(parentId)) {
1097nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
1098} else {
1099nodesHavingChildren.set(parentId, 1);
1100}
1101allChildNodes.push(node);
1102if (node.kind !== 'Pod' || !props.showCompactNodes) {
1103if (props.getNodeExpansion(parentId)) {
1104children.push(node);
1105childrenByParentKey.set(parentId, children);
1106}
1107} else {
1108const parentTreeNode = nodeByKey.get(parentId);
1109processPodGroup(parentTreeNode, node, props);
1110}
1111if (props.showCompactNodes) {
1112if (childrenMap.has(parentId)) {
1113childrenMap.set(parentId, childrenMap.get(parentId).concat(allChildNodes));
1114} else {
1115childrenMap.set(parentId, allChildNodes);
1116}
1117}
1118});
1119}
1120});
1121}
1122roots.sort(compareNodes).forEach(node => {
1123processNode(node, node);
1124graph.setEdge(appNodeKey(props.app), treeNodeKey(node));
1125});
1126orphans.sort(compareNodes).forEach(node => {
1127processNode(node, node);
1128});
1129graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT});
1130if (props.nodeFilter) {
1131filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter);
1132}
1133if (props.showCompactNodes) {
1134groupNodes(nodes, graph);
1135}
1136}
1137
1138function setPodGroupNode(node: ResourceTreeNode, root: ResourceTreeNode) {
1139const numberOfRows = Math.ceil(node.podGroup.pods.length / 8);
1140graph.setNode(treeNodeKey(node), {...node, type: NODE_TYPES.podGroup, width: NODE_WIDTH, height: POD_NODE_HEIGHT + 30 * numberOfRows, root});
1141}
1142
1143function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) {
1144if (props.showCompactNodes && node.podGroup) {
1145setPodGroupNode(node, root);
1146} else {
1147graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root});
1148}
1149(childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => {
1150if (treeNodeKey(child) === treeNodeKey(root)) {
1151return;
1152}
1153if (node.namespace === child.namespace) {
1154graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors});
1155}
1156processNode(child, root, colors);
1157});
1158}
1159dagre.layout(graph);
1160
1161const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string; color?: string; colors?: string | {[key: string]: any}}[] = [];
1162const nodeOffset = new Map<string, number>();
1163const reverseEdge = new Map<string, number>();
1164graph.edges().forEach(edgeInfo => {
1165const edge = graph.edge(edgeInfo);
1166if (edge.points.length > 1) {
1167if (!reverseEdge.has(edgeInfo.w)) {
1168reverseEdge.set(edgeInfo.w, 1);
1169} else {
1170reverseEdge.set(edgeInfo.w, reverseEdge.get(edgeInfo.w) + 1);
1171}
1172if (!nodeOffset.has(edgeInfo.v)) {
1173nodeOffset.set(edgeInfo.v, reverseEdge.get(edgeInfo.w) - 1);
1174}
1175}
1176});
1177graph.edges().forEach(edgeInfo => {
1178const edge = graph.edge(edgeInfo);
1179const colors = (edge.colors as string[]) || [];
1180let backgroundImage: string;
1181if (colors.length > 0) {
1182const step = 100 / colors.length;
1183const gradient = colors.map((lineColor, i) => {
1184return `${lineColor} ${step * i}%, ${lineColor} ${step * i + step / 2}%, transparent ${step * i + step / 2}%, transparent ${step * (i + 1)}%`;
1185});
1186backgroundImage = `linear-gradient(90deg, ${gradient})`;
1187}
1188
1189const lines: Line[] = [];
1190// don't render connections from hidden node representing internal traffic
1191if (edgeInfo.v === INTERNAL_TRAFFIC_NODE || edgeInfo.w === INTERNAL_TRAFFIC_NODE) {
1192return;
1193}
1194if (edge.points.length > 1) {
1195const startNode = graph.node(edgeInfo.v);
1196const endNode = graph.node(edgeInfo.w);
1197const offset = nodeOffset.get(edgeInfo.v);
1198let startNodeRight = props.useNetworkingHierarchy ? 162 : 142;
1199const endNodeLeft = 140;
1200let spaceForExpansionIcon = 0;
1201if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE) && !edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
1202lines.push({x1: startNode.x + 10, y1: startNode.y, x2: endNode.x - endNodeLeft, y2: endNode.y});
1203} else {
1204if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
1205startNodeRight = 152;
1206spaceForExpansionIcon = 5;
1207}
1208const len = reverseEdge.get(edgeInfo.w) + 1;
1209const yEnd = endNode.y - endNode.height / 2 + (endNode.height / len + (endNode.height / len) * offset);
1210const firstBend =
1211spaceForExpansionIcon +
1212startNode.x +
1213startNodeRight +
1214(endNode.x - startNode.x - startNodeRight - endNodeLeft) / len +
1215((endNode.x - startNode.x - startNodeRight - endNodeLeft) / len) * offset;
1216lines.push({x1: startNode.x + startNodeRight, y1: startNode.y, x2: firstBend, y2: startNode.y});
1217if (startNode.y - yEnd >= 1 || yEnd - startNode.y >= 1) {
1218lines.push({x1: firstBend, y1: startNode.y, x2: firstBend, y2: yEnd});
1219}
1220lines.push({x1: firstBend, y1: yEnd, x2: endNode.x - endNodeLeft, y2: yEnd});
1221}
1222}
1223edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage, colors: [{colors}]});
1224});
1225const graphNodes = graph.nodes();
1226const size = getGraphSize(graphNodes.map(id => graph.node(id)));
1227return (
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
1235className={classNames('application-resource-tree', {'application-resource-tree--network': props.useNetworkingHierarchy})}
1236style={{width: size.width + 150, height: size.height + 250, transformOrigin: '0% 0%', transform: `scale(${props.zoom})`}}>
1237{graphNodes.map(key => {
1238const node = graph.node(key);
1239const nodeType = node.type;
1240switch (nodeType) {
1241case NODE_TYPES.filteredIndicator:
1242return <React.Fragment key={key}>{renderFilteredNode(node as any, props.onClearFilter)}</React.Fragment>;
1243case NODE_TYPES.externalTraffic:
1244return <React.Fragment key={key}>{renderTrafficNode(node)}</React.Fragment>;
1245case NODE_TYPES.internalTraffic:
1246return null;
1247case NODE_TYPES.externalLoadBalancer:
1248return <React.Fragment key={key}>{renderLoadBalancerNode(node as any)}</React.Fragment>;
1249case NODE_TYPES.groupedNodes:
1250return <React.Fragment key={key}>{renderGroupedNodes(props, node as any)}</React.Fragment>;
1251case NODE_TYPES.podGroup:
1252return <React.Fragment key={key}>{renderPodGroup(props, key, node as ResourceTreeNode & dagre.Node, childrenMap)}</React.Fragment>;
1253default:
1254return <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) => {
1260const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2));
1261const xMid = (line.x1 + line.x2) / 2;
1262const yMid = (line.y1 + line.y2) / 2;
1263const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI;
1264const lastLine = i === edge.lines.length - 1 ? line : null;
1265let arrowColor = null;
1266if (edge.colors) {
1267if (Array.isArray(edge.colors)) {
1268const firstColor = edge.colors[0];
1269if (firstColor.colors) {
1270arrowColor = firstColor.colors;
1271}
1272}
1273}
1274return (
1275<div
1276className='application-resource-tree__line'
1277key={i}
1278style={{
1279width: distance,
1280left: xMid - distance / 2,
1281top: yMid,
1282backgroundImage: edge.backgroundImage,
1283transform: 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};
1295