argo-cd

Форк
0
1294 строки · 58.8 Кб
1
import {DropDown, DropDownMenu, Tooltip} from 'argo-ui';
2
import * as classNames from 'classnames';
3
import * as dagre from 'dagre';
4
import * as React from 'react';
5
import Moment from 'react-moment';
6
import * as moment from 'moment';
7

8
import * as models from '../../../shared/models';
9

10
import {EmptyState} from '../../../shared/components';
11
import {AppContext, Consumer} from '../../../shared/context';
12
import {ApplicationURLs} from '../application-urls';
13
import {ResourceIcon} from '../resource-icon';
14
import {ResourceLabel} from '../resource-label';
15
import {
16
    BASE_COLORS,
17
    ComparisonStatusIcon,
18
    deletePodAction,
19
    getAppOverridesCount,
20
    HealthStatusIcon,
21
    isAppNode,
22
    isYoungerThanXMinutes,
23
    NodeId,
24
    nodeKey,
25
    PodHealthIcon,
26
    getUsrMsgKeyToDisplay
27
} from '../utils';
28
import {NodeUpdateAnimation} from './node-update-animation';
29
import {PodGroup} from '../application-pod-view/pod-view';
30
import './application-resource-tree.scss';
31
import {ArrowConnector} from './arrow-connector';
32

33
function treeNodeKey(node: NodeId & {uid?: string}) {
34
    return node.uid || nodeKey(node);
35
}
36

37
const color = require('color');
38

39
export interface ResourceTreeNode extends models.ResourceNode {
40
    status?: models.SyncStatusCode;
41
    health?: models.HealthStatus;
42
    hook?: boolean;
43
    root?: ResourceTreeNode;
44
    requiresPruning?: boolean;
45
    orphaned?: boolean;
46
    podGroup?: PodGroup;
47
    isExpanded?: boolean;
48
}
49

50
export interface ApplicationResourceTreeProps {
51
    app: models.Application;
52
    tree: models.ApplicationTree;
53
    useNetworkingHierarchy: boolean;
54
    nodeFilter: (node: ResourceTreeNode) => boolean;
55
    selectedNodeFullName?: string;
56
    onNodeClick?: (fullName: string) => any;
57
    onGroupdNodeClick?: (groupedNodeIds: string[]) => any;
58
    nodeMenu?: (node: models.ResourceNode) => React.ReactNode;
59
    onClearFilter: () => any;
60
    appContext?: AppContext;
61
    showOrphanedResources: boolean;
62
    showCompactNodes: boolean;
63
    userMsgs: models.UserMessages[];
64
    updateUsrHelpTipMsgs: (userMsgs: models.UserMessages) => void;
65
    setShowCompactNodes: (showCompactNodes: boolean) => void;
66
    zoom: number;
67
    podGroupCount: number;
68
    filters?: string[];
69
    setTreeFilterGraph?: (filterGraph: any[]) => void;
70
    nameDirection: boolean;
71
    setNodeExpansion: (node: string, isExpanded: boolean) => any;
72
    getNodeExpansion: (node: string) => boolean;
73
}
74

75
interface Line {
76
    x1: number;
77
    y1: number;
78
    x2: number;
79
    y2: number;
80
}
81

82
const NODE_WIDTH = 282;
83
const NODE_HEIGHT = 52;
84
const POD_NODE_HEIGHT = 136;
85
const FILTERED_INDICATOR_NODE = '__filtered_indicator__';
86
const EXTERNAL_TRAFFIC_NODE = '__external_traffic__';
87
const INTERNAL_TRAFFIC_NODE = '__internal_traffic__';
88
const NODE_TYPES = {
89
    filteredIndicator: 'filtered_indicator',
90
    externalTraffic: 'external_traffic',
91
    externalLoadBalancer: 'external_load_balancer',
92
    internalTraffic: 'internal_traffic',
93
    groupedNodes: 'grouped_nodes',
94
    podGroup: 'pod_group'
95
};
96
// generate lots of colors with different darkness
97
const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6]
98
    .map(darken =>
99
        BASE_COLORS.map(item =>
100
            color(item)
101
                .darken(darken)
102
                .hex()
103
        )
104
    )
105
    .reduce((first, second) => first.concat(second), []);
106

107
function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
108
    let width = 0;
109
    let height = 0;
110
    nodes.forEach(node => {
111
        width = Math.max(node.x + node.width, width);
112
        height = Math.max(node.y + node.height, height);
113
    });
114
    return {width, height};
115
}
116

117
function groupNodes(nodes: ResourceTreeNode[], graph: dagre.graphlib.Graph) {
118
    function getNodeGroupingInfo(nodeId: string) {
119
        const node = graph.node(nodeId);
120
        return {
121
            nodeId,
122
            kind: node.kind,
123
            parentIds: graph.predecessors(nodeId),
124
            childIds: graph.successors(nodeId)
125
        };
126
    }
127

128
    function filterNoChildNode(nodeInfo: {childIds: dagre.Node[]}) {
129
        return nodeInfo.childIds.length === 0;
130
    }
131

132
    // create nodes array with parent/child nodeId
133
    const nodesInfoArr = graph.nodes().map(getNodeGroupingInfo);
134

135
    // group sibling nodes into a 2d array
136
    const siblingNodesArr = nodesInfoArr
137
        .reduce((acc, curr) => {
138
            if (curr.childIds.length > 1) {
139
                acc.push(curr.childIds.map(nodeId => getNodeGroupingInfo(nodeId.toString())));
140
            }
141
            return acc;
142
        }, [])
143
        .map(nodeArr => nodeArr.filter(filterNoChildNode));
144

145
    // group sibling nodes with same kind
146
    const groupedNodesArr = siblingNodesArr
147
        .map(eachLevel => {
148
            return eachLevel.reduce(
149
                (groupedNodesInfo: {kind: string; nodeIds?: string[]; parentIds?: dagre.Node[]}[], currentNodeInfo: {kind: string; nodeId: string; parentIds: dagre.Node[]}) => {
150
                    const index = groupedNodesInfo.findIndex((nodeInfo: {kind: string}) => currentNodeInfo.kind === nodeInfo.kind);
151
                    if (index > -1) {
152
                        groupedNodesInfo[index].nodeIds.push(currentNodeInfo.nodeId);
153
                    }
154

155
                    if (groupedNodesInfo.length === 0 || index < 0) {
156
                        const nodeIdArr = [];
157
                        nodeIdArr.push(currentNodeInfo.nodeId);
158
                        const groupedNodesInfoObj = {
159
                            kind: currentNodeInfo.kind,
160
                            nodeIds: nodeIdArr,
161
                            parentIds: currentNodeInfo.parentIds
162
                        };
163
                        groupedNodesInfo.push(groupedNodesInfoObj);
164
                    }
165

166
                    return groupedNodesInfo;
167
                },
168
                []
169
            );
170
        })
171
        .reduce((flattedNodesGroup, groupedNodes) => {
172
            return flattedNodesGroup.concat(groupedNodes);
173
        }, [])
174
        .filter((eachArr: {nodeIds: string[]}) => eachArr.nodeIds.length > 1);
175

176
    // update graph
177
    if (groupedNodesArr.length > 0) {
178
        groupedNodesArr.forEach((obj: {kind: string; nodeIds: string[]; parentIds: dagre.Node[]}) => {
179
            const {nodeIds, kind, parentIds} = obj;
180
            const groupedNodeIds: string[] = [];
181
            const podGroupIds: string[] = [];
182
            nodeIds.forEach((nodeId: string) => {
183
                const index = nodes.findIndex(node => nodeId === node.uid || nodeId === nodeKey(node));
184
                const graphNode = graph.node(nodeId);
185
                if (!graphNode?.podGroup && index > -1) {
186
                    groupedNodeIds.push(nodeId);
187
                } else {
188
                    podGroupIds.push(nodeId);
189
                }
190
            });
191
            const reducedNodeIds = nodeIds.reduce((acc, aNodeId) => {
192
                if (podGroupIds.findIndex(i => i === aNodeId) < 0) {
193
                    acc.push(aNodeId);
194
                }
195
                return acc;
196
            }, []);
197
            if (groupedNodeIds.length > 1) {
198
                groupedNodeIds.forEach(n => graph.removeNode(n));
199
                graph.setNode(`${parentIds[0].toString()}/child/${kind}`, {
200
                    kind,
201
                    groupedNodeIds,
202
                    height: NODE_HEIGHT,
203
                    width: NODE_WIDTH,
204
                    count: reducedNodeIds.length,
205
                    type: NODE_TYPES.groupedNodes
206
                });
207
                graph.setEdge(parentIds[0].toString(), `${parentIds[0].toString()}/child/${kind}`);
208
            }
209
        });
210
    }
211
}
212

213
export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) {
214
    function orphanedToInt(orphaned?: boolean) {
215
        return (orphaned && 1) || 0;
216
    }
217
    function compareRevision(a: string, b: string) {
218
        const numberA = Number(a);
219
        const numberB = Number(b);
220
        if (isNaN(numberA) || isNaN(numberB)) {
221
            return a.localeCompare(b);
222
        }
223
        return Math.sign(numberA - numberB);
224
    }
225
    function getRevision(a: ResourceTreeNode) {
226
        const filtered = (a.info || []).filter(b => b.name === 'Revision' && b)[0];
227
        if (filtered == null) {
228
            return '';
229
        }
230
        const value = filtered.value;
231
        if (value == null) {
232
            return '';
233
        }
234
        return value.replace(/^Rev:/, '');
235
    }
236
    if (first.kind === 'ReplicaSet') {
237
        return (
238
            orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
239
            compareRevision(getRevision(second), getRevision(first)) ||
240
            nodeKey(first).localeCompare(nodeKey(second)) ||
241
            0
242
        );
243
    }
244
    return (
245
        orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
246
        nodeKey(first).localeCompare(nodeKey(second)) ||
247
        compareRevision(getRevision(first), getRevision(second)) ||
248
        0
249
    );
250
}
251

252
function appNodeKey(app: models.Application) {
253
    return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
254
}
255

256
function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) {
257
    const indicators = new Array<number>();
258
    let count = Math.min(node.count - 1, 3);
259
    while (count > 0) {
260
        indicators.push(count--);
261
    }
262
    return (
263
        <React.Fragment>
264
            <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
265
                <div className='application-resource-tree__node-kind-icon '>
266
                    <i className='icon fa fa-filter' />
267
                </div>
268
                <div className='application-resource-tree__node-content-wrap-overflow'>
269
                    <a className='application-resource-tree__node-title' onClick={onClearFilter}>
270
                        clear filters to show {node.count} additional resource{node.count > 1 && 's'}
271
                    </a>
272
                </div>
273
            </div>
274
            {indicators.map(i => (
275
                <div
276
                    key={i}
277
                    className='application-resource-tree__node application-resource-tree__filtered-indicator'
278
                    style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
279
                />
280
            ))}
281
        </React.Fragment>
282
    );
283
}
284

285
function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number} & dagre.Node & ResourceTreeNode) {
286
    const indicators = new Array<number>();
287
    let count = Math.min(node.count - 1, 3);
288
    while (count > 0) {
289
        indicators.push(count--);
290
    }
291
    return (
292
        <React.Fragment>
293
            <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
294
                <div className='application-resource-tree__node-kind-icon'>
295
                    <ResourceIcon kind={node.kind} />
296
                    <br />
297
                    <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>
298
                </div>
299
                <div
300
                    className='application-resource-tree__node-title application-resource-tree__direction-center-left'
301
                    onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}
302
                    title={`Click to see details of ${node.count} collapsed ${node.kind} and doesn't contains any active pods`}>
303
                    {node.kind}
304
                    <span style={{paddingLeft: '.5em', fontSize: 'small'}}>
305
                        {node.kind === 'ReplicaSet' ? (
306
                            <i
307
                                className='fa-solid fa-cart-flatbed icon-background'
308
                                title={`Click to see details of ${node.count} collapsed ${node.kind} and doesn't contains any active pods`}
309
                                key={node.uid}
310
                            />
311
                        ) : (
312
                            <i className='fa fa-info-circle icon-background' title={`Click to see details of ${node.count} collapsed ${node.kind}`} key={node.uid} />
313
                        )}
314
                    </span>
315
                </div>
316
            </div>
317
            {indicators.map(i => (
318
                <div
319
                    key={i}
320
                    className='application-resource-tree__node application-resource-tree__filtered-indicator'
321
                    style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
322
                />
323
            ))}
324
        </React.Fragment>
325
    );
326
}
327

328
function renderTrafficNode(node: dagre.Node) {
329
    return (
330
        <div style={{position: 'absolute', left: 0, top: node.y, width: node.width, height: node.height}}>
331
            <div className='application-resource-tree__node-kind-icon' style={{fontSize: '2em'}}>
332
                <i className='icon fa fa-cloud' />
333
            </div>
334
        </div>
335
    );
336
}
337

338
function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
339
    return (
340
        <div
341
            className='application-resource-tree__node application-resource-tree__node--load-balancer'
342
            style={{
343
                left: node.x,
344
                top: node.y,
345
                width: node.width,
346
                height: node.height
347
            }}>
348
            <div className='application-resource-tree__node-kind-icon'>
349
                <i title={node.kind} className={`icon fa fa-network-wired`} style={{color: node.color}} />
350
            </div>
351
            <div className='application-resource-tree__node-content'>
352
                <span className='application-resource-tree__node-title'>{node.label}</span>
353
            </div>
354
        </div>
355
    );
356
}
357

358
export const describeNode = (node: ResourceTreeNode) => {
359
    const lines = [`Kind: ${node.kind}`, `Namespace: ${node.namespace || '(global)'}`, `Name: ${node.name}`];
360
    if (node.images) {
361
        lines.push('Images:');
362
        node.images.forEach(i => lines.push(`- ${i}`));
363
    }
364
    return lines.join('\n');
365
};
366

367
function processPodGroup(targetPodGroup: ResourceTreeNode, child: ResourceTreeNode, props: ApplicationResourceTreeProps) {
368
    if (!targetPodGroup.podGroup) {
369
        const fullName = nodeKey(targetPodGroup);
370
        if ((targetPodGroup.parentRefs || []).length === 0) {
371
            targetPodGroup.root = targetPodGroup;
372
        }
373
        targetPodGroup.podGroup = {
374
            pods: [] as models.Pod[],
375
            fullName,
376
            ...targetPodGroup.podGroup,
377
            ...targetPodGroup,
378
            info: (targetPodGroup.info || []).filter(i => !i.name.includes('Resource.')),
379
            createdAt: targetPodGroup.createdAt,
380
            renderMenu: () => props.nodeMenu(targetPodGroup),
381
            kind: targetPodGroup.kind,
382
            type: 'parentResource',
383
            name: targetPodGroup.name
384
        };
385
    }
386
    if (child.kind === 'Pod') {
387
        const p: models.Pod = {
388
            ...child,
389
            fullName: nodeKey(child),
390
            metadata: {name: child.name},
391
            spec: {nodeName: 'Unknown'},
392
            health: child.health ? child.health.status : 'Unknown'
393
        } as models.Pod;
394

395
        // Get node name for Pod
396
        child.info?.forEach(i => {
397
            if (i.name === 'Node') {
398
                p.spec.nodeName = i.value;
399
            }
400
        });
401
        targetPodGroup.podGroup.pods.push(p);
402
    }
403
}
404

405
function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, childMap: Map<string, ResourceTreeNode[]>) {
406
    const fullName = nodeKey(node);
407
    let comparisonStatus: models.SyncStatusCode = null;
408
    let healthState: models.HealthStatus = null;
409
    if (node.status || node.health) {
410
        comparisonStatus = node.status;
411
        healthState = node.health;
412
    }
413
    const appNode = isAppNode(node);
414
    const rootNode = !node.root;
415
    const extLinks: string[] = props.app.status.summary.externalURLs;
416
    const podGroupChildren = childMap.get(treeNodeKey(node));
417
    const nonPodChildren = podGroupChildren?.reduce((acc, child) => {
418
        if (child.kind !== 'Pod') {
419
            acc.push(child);
420
        }
421
        return acc;
422
    }, []);
423
    const childCount = nonPodChildren?.length;
424
    const margin = 8;
425
    let topExtra = 0;
426
    const podGroup = node.podGroup;
427
    const podGroupHealthy = [];
428
    const podGroupDegraded = [];
429
    const podGroupInProgress = [];
430

431
    for (const pod of podGroup?.pods || []) {
432
        switch (pod.health) {
433
            case 'Healthy':
434
                podGroupHealthy.push(pod);
435
                break;
436
            case 'Degraded':
437
                podGroupDegraded.push(pod);
438
                break;
439
            case 'Progressing':
440
                podGroupInProgress.push(pod);
441
        }
442
    }
443

444
    const showPodGroupByStatus = props.tree.nodes.filter((rNode: ResourceTreeNode) => rNode.kind === 'Pod').length >= props.podGroupCount;
445
    const numberOfRows = showPodGroupByStatus
446
        ? [podGroupHealthy, podGroupDegraded, podGroupInProgress].reduce((total, podGroupByStatus) => total + (podGroupByStatus.filter(pod => pod).length > 0 ? 1 : 0), 0)
447
        : Math.ceil(podGroup?.pods.length / 8);
448

449
    if (podGroup) {
450
        topExtra = margin + (POD_NODE_HEIGHT / 2 + 30 * numberOfRows) / 2;
451
    }
452

453
    return (
454
        <div
455
            className={classNames('application-resource-tree__node', {
456
                'active': fullName === props.selectedNodeFullName,
457
                'application-resource-tree__node--orphaned': node.orphaned
458
            })}
459
            title={describeNode(node)}
460
            style={{
461
                left: node.x,
462
                top: node.y - topExtra,
463
                width: node.width,
464
                height: showPodGroupByStatus ? POD_NODE_HEIGHT + 20 * numberOfRows : node.height
465
            }}>
466
            <NodeUpdateAnimation resourceVersion={node.resourceVersion} />
467
            <div onClick={() => props.onNodeClick && props.onNodeClick(fullName)} className={`application-resource-tree__node__top-part`}>
468
                <div
469
                    className={classNames('application-resource-tree__node-kind-icon', {
470
                        'application-resource-tree__node-kind-icon--big': rootNode
471
                    })}>
472
                    <ResourceIcon kind={node.kind || 'Unknown'} />
473
                    <br />
474
                    {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
475
                </div>
476
                <div className='application-resource-tree__node-content'>
477
                    <span
478
                        className={classNames('application-resource-tree__node-title', {
479
                            'application-resource-tree__direction-right': props.nameDirection,
480
                            'application-resource-tree__direction-left': !props.nameDirection
481
                        })}
482
                        onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}>
483
                        {node.name}
484
                    </span>
485
                    <span
486
                        className={classNames('application-resource-tree__node-status-icon', {
487
                            'application-resource-tree__node-status-icon--offset': rootNode
488
                        })}>
489
                        {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
490
                        {healthState != null && <HealthStatusIcon state={healthState} />}
491
                        {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
492
                        {appNode && !rootNode && (
493
                            <Consumer>
494
                                {ctx => (
495
                                    <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
496
                                        <i className='fa fa-external-link-alt' />
497
                                    </a>
498
                                )}
499
                            </Consumer>
500
                        )}
501
                        <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
502
                    </span>
503
                    {childCount > 0 && (
504
                        <>
505
                            <br />
506
                            <div
507
                                style={{top: node.height / 2 - 6}}
508
                                className='application-resource-tree__node--podgroup--expansion'
509
                                onClick={event => {
510
                                    expandCollapse(node, props);
511
                                    event.stopPropagation();
512
                                }}>
513
                                {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
514
                            </div>
515
                        </>
516
                    )}
517
                </div>
518
                <div className='application-resource-tree__node-labels'>
519
                    {node.createdAt || rootNode ? (
520
                        <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
521
                            {node.createdAt || props.app.metadata.creationTimestamp}
522
                        </Moment>
523
                    ) : null}
524
                    {(node.info || [])
525
                        .filter(tag => !tag.name.includes('Node'))
526
                        .slice(0, 4)
527
                        .map((tag, i) => (
528
                            <span className='application-resource-tree__node-label' title={`${tag.name}:${tag.value}`} key={i}>
529
                                {tag.value}
530
                            </span>
531
                        ))}
532
                    {(node.info || []).length > 4 && (
533
                        <Tooltip
534
                            content={
535
                                <>
536
                                    {(node.info || []).map(i => (
537
                                        <div key={i.name}>
538
                                            {i.name}: {i.value}
539
                                        </div>
540
                                    ))}
541
                                </>
542
                            }
543
                            key={node.uid}>
544
                            <span className='application-resource-tree__node-label' title='More'>
545
                                More
546
                            </span>
547
                        </Tooltip>
548
                    )}
549
                </div>
550
                {props.nodeMenu && (
551
                    <div className='application-resource-tree__node-menu'>
552
                        <DropDown
553
                            key={node.uid}
554
                            isMenu={true}
555
                            anchor={() => (
556
                                <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
557
                                    <i className='fa fa-ellipsis-v' />
558
                                </button>
559
                            )}>
560
                            {() => props.nodeMenu(node)}
561
                        </DropDown>
562
                    </div>
563
                )}
564
            </div>
565
            <div className='application-resource-tree__node--lower-section'>
566
                {[podGroupHealthy, podGroupDegraded, podGroupInProgress].map((pods, index) => {
567
                    if (pods.length > 0) {
568
                        return (
569
                            <div key={index} className={`application-resource-tree__node--lower-section__pod-group`}>
570
                                {renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)}
571
                            </div>
572
                        );
573
                    }
574
                })}
575
            </div>
576
        </div>
577
    );
578
}
579

580
function renderPodGroupByStatus(props: ApplicationResourceTreeProps, node: any, pods: models.Pod[], showPodGroupByStatus: boolean) {
581
    return (
582
        <div className='application-resource-tree__node--lower-section__pod-group__pod-container__pods'>
583
            {pods.length !== 0 && showPodGroupByStatus ? (
584
                <React.Fragment>
585
                    <div className={`pod-view__node__pod pod-view__node__pod--${pods[0].health.toLowerCase()}`}>
586
                        <PodHealthIcon state={{status: pods[0].health, message: ''}} key={pods[0].uid} />
587
                    </div>
588

589
                    <div className='pod-view__node__label--large'>
590
                        <a
591
                            className='application-resource-tree__node-title'
592
                            onClick={() =>
593
                                props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupdedNodeIds === 'undefined' ? node.groupdedNodeIds : pods.map(pod => pod.uid))
594
                            }>
595
                            &nbsp;
596
                            <span title={`Click to view the ${pods[0].health.toLowerCase()} pods list`}>
597
                                {pods[0].health} {pods.length} pods
598
                            </span>
599
                        </a>
600
                    </div>
601
                </React.Fragment>
602
            ) : (
603
                pods.map(pod => (
604
                    <DropDownMenu
605
                        key={pod.uid}
606
                        anchor={() => (
607
                            <Tooltip
608
                                content={
609
                                    <div>
610
                                        {pod.metadata.name}
611
                                        <div>Health: {pod.health}</div>
612
                                        {pod.createdAt && (
613
                                            <span>
614
                                                <span>Created: </span>
615
                                                <Moment fromNow={true} ago={true}>
616
                                                    {pod.createdAt}
617
                                                </Moment>
618
                                                <span> ago ({<Moment local={true}>{pod.createdAt}</Moment>})</span>
619
                                            </span>
620
                                        )}
621
                                    </div>
622
                                }
623
                                popperOptions={{
624
                                    modifiers: {
625
                                        preventOverflow: {
626
                                            enabled: true
627
                                        },
628
                                        hide: {
629
                                            enabled: false
630
                                        },
631
                                        flip: {
632
                                            enabled: false
633
                                        }
634
                                    }
635
                                }}
636
                                key={pod.metadata.name}>
637
                                <div style={{position: 'relative'}}>
638
                                    {isYoungerThanXMinutes(pod, 30) && (
639
                                        <i className='fas fa-star application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod__star-icon' />
640
                                    )}
641
                                    <div
642
                                        className={`application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod--${pod.health.toLowerCase()}`}>
643
                                        <PodHealthIcon state={{status: pod.health, message: ''}} />
644
                                    </div>
645
                                </div>
646
                            </Tooltip>
647
                        )}
648
                        items={[
649
                            {
650
                                title: (
651
                                    <React.Fragment>
652
                                        <i className='fa fa-info-circle' /> Info
653
                                    </React.Fragment>
654
                                ),
655
                                action: () => props.onNodeClick(pod.fullName)
656
                            },
657
                            {
658
                                title: (
659
                                    <React.Fragment>
660
                                        <i className='fa fa-align-left' /> Logs
661
                                    </React.Fragment>
662
                                ),
663
                                action: () => {
664
                                    props.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'logs'}, {replace: true});
665
                                }
666
                            },
667
                            {
668
                                title: (
669
                                    <React.Fragment>
670
                                        <i className='fa fa-times-circle' /> Delete
671
                                    </React.Fragment>
672
                                ),
673
                                action: () => {
674
                                    deletePodAction(pod, props.appContext, props.app.metadata.name, props.app.metadata.namespace);
675
                                }
676
                            }
677
                        ]}
678
                    />
679
                ))
680
            )}
681
        </div>
682
    );
683
}
684

685
function expandCollapse(node: ResourceTreeNode, props: ApplicationResourceTreeProps) {
686
    const isExpanded = !props.getNodeExpansion(node.uid);
687
    node.isExpanded = isExpanded;
688
    props.setNodeExpansion(node.uid, isExpanded);
689
}
690

691
function NodeInfoDetails({tag: tag, kind: kind}: {tag: models.InfoItem; kind: string}) {
692
    if (kind === 'Pod') {
693
        const val = `${tag.name}`;
694
        if (val === 'Status Reason') {
695
            if (`${tag.value}` !== 'ImagePullBackOff')
696
                return (
697
                    <span className='application-resource-tree__node-label' title={`Status: ${tag.value}`}>
698
                        {tag.value}
699
                    </span>
700
                );
701
            else {
702
                return (
703
                    <span
704
                        className='application-resource-tree__node-label'
705
                        title='One of the containers may have the incorrect image name/tag, or you may be fetching from the incorrect repository, or the repository requires authentication.'>
706
                        {tag.value}
707
                    </span>
708
                );
709
            }
710
        } else if (val === 'Containers') {
711
            const arr = `${tag.value}`.split('/');
712
            const title = `Number of containers in total: ${arr[1]} \nNumber of ready containers: ${arr[0]}`;
713
            return (
714
                <span className='application-resource-tree__node-label' title={`${title}`}>
715
                    {tag.value}
716
                </span>
717
            );
718
        } else if (val === 'Restart Count') {
719
            return (
720
                <span className='application-resource-tree__node-label' title={`The total number of restarts of the containers: ${tag.value}`}>
721
                    {tag.value}
722
                </span>
723
            );
724
        } else if (val === 'Revision') {
725
            return (
726
                <span className='application-resource-tree__node-label' title={`The revision in which pod present is: ${tag.value}`}>
727
                    {tag.value}
728
                </span>
729
            );
730
        } else {
731
            return (
732
                <span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
733
                    {tag.value}
734
                </span>
735
            );
736
        }
737
    } else {
738
        return (
739
            <span className='application-resource-tree__node-label' title={`${tag.name}: ${tag.value}`}>
740
                {tag.value}
741
            </span>
742
        );
743
    }
744
}
745

746
function renderResourceNode(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, nodesHavingChildren: Map<string, number>) {
747
    const fullName = nodeKey(node);
748
    let comparisonStatus: models.SyncStatusCode = null;
749
    let healthState: models.HealthStatus = null;
750
    if (node.status || node.health) {
751
        comparisonStatus = node.status;
752
        healthState = node.health;
753
    }
754
    const appNode = isAppNode(node);
755
    const rootNode = !node.root;
756
    const extLinks: string[] = props.app.status.summary.externalURLs;
757
    const childCount = nodesHavingChildren.get(node.uid);
758
    return (
759
        <div
760
            onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
761
            className={classNames('application-resource-tree__node', 'application-resource-tree__node--' + node.kind.toLowerCase(), {
762
                'active': fullName === props.selectedNodeFullName,
763
                'application-resource-tree__node--orphaned': node.orphaned
764
            })}
765
            title={describeNode(node)}
766
            style={{
767
                left: node.x,
768
                top: node.y,
769
                width: node.width,
770
                height: node.height
771
            }}>
772
            {!appNode && <NodeUpdateAnimation resourceVersion={node.resourceVersion} />}
773
            <div
774
                className={classNames('application-resource-tree__node-kind-icon', {
775
                    'application-resource-tree__node-kind-icon--big': rootNode
776
                })}>
777
                <ResourceIcon kind={node.kind} />
778
                <br />
779
                {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
780
            </div>
781
            <div className='application-resource-tree__node-content'>
782
                <div
783
                    className={classNames('application-resource-tree__node-title', {
784
                        'application-resource-tree__direction-right': props.nameDirection,
785
                        'application-resource-tree__direction-left': !props.nameDirection
786
                    })}>
787
                    {node.name}
788
                </div>
789
                <div
790
                    className={classNames('application-resource-tree__node-status-icon', {
791
                        'application-resource-tree__node-status-icon--offset': rootNode
792
                    })}>
793
                    {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
794
                    {healthState != null && <HealthStatusIcon state={healthState} />}
795
                    {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
796
                    {appNode && !rootNode && (
797
                        <Consumer>
798
                            {ctx => (
799
                                <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
800
                                    <i className='fa fa-external-link-alt' />
801
                                </a>
802
                            )}
803
                        </Consumer>
804
                    )}
805
                    <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
806
                </div>
807
                {childCount > 0 && (
808
                    <div
809
                        className='application-resource-tree__node--expansion'
810
                        onClick={event => {
811
                            expandCollapse(node, props);
812
                            event.stopPropagation();
813
                        }}>
814
                        {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
815
                    </div>
816
                )}
817
            </div>
818
            <div className='application-resource-tree__node-labels'>
819
                {node.createdAt || rootNode ? (
820
                    <span title={`${node.kind} was created ${moment(node.createdAt).fromNow()}`}>
821
                        <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
822
                            {node.createdAt || props.app.metadata.creationTimestamp}
823
                        </Moment>
824
                    </span>
825
                ) : null}
826
                {(node.info || [])
827
                    .filter(tag => !tag.name.includes('Node'))
828
                    .slice(0, 4)
829
                    .map((tag, i) => {
830
                        return <NodeInfoDetails tag={tag} kind={node.kind} key={i} />;
831
                    })}
832
                {(node.info || []).length > 4 && (
833
                    <Tooltip
834
                        content={
835
                            <>
836
                                {(node.info || []).map(i => (
837
                                    <div key={i.name}>
838
                                        {i.name}: {i.value}
839
                                    </div>
840
                                ))}
841
                            </>
842
                        }
843
                        key={node.uid}>
844
                        <span className='application-resource-tree__node-label' title='More'>
845
                            More
846
                        </span>
847
                    </Tooltip>
848
                )}
849
            </div>
850
            {props.nodeMenu && (
851
                <div className='application-resource-tree__node-menu'>
852
                    <DropDown
853
                        isMenu={true}
854
                        anchor={() => (
855
                            <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
856
                                <i className='fa fa-ellipsis-v' />
857
                            </button>
858
                        )}>
859
                        {() => props.nodeMenu(node)}
860
                    </DropDown>
861
                </div>
862
            )}
863
        </div>
864
    );
865
}
866

867
function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] {
868
    let result = new Array<ResourceTreeNode>();
869
    const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey));
870
    result = result.concat(nodes.filter(target => refs.has(nodeKey(target))));
871
    if (networkingInfo.targetLabels) {
872
        result = result.concat(
873
            nodes.filter(target => {
874
                if (target.networkingInfo && target.networkingInfo.labels) {
875
                    return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]);
876
                }
877
                return false;
878
            })
879
        );
880
    }
881
    return result;
882
}
883
export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
884
    const graph = new dagre.graphlib.Graph();
885
    graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80});
886
    graph.setDefaultEdgeLabel(() => ({}));
887
    const overridesCount = getAppOverridesCount(props.app);
888
    const appNode = {
889
        kind: props.app.kind,
890
        name: props.app.metadata.name,
891
        namespace: props.app.metadata.namespace,
892
        resourceVersion: props.app.metadata.resourceVersion,
893
        group: 'argoproj.io',
894
        version: '',
895
        children: Array(),
896
        status: props.app.status.sync.status,
897
        health: props.app.status.health,
898
        uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name,
899
        info:
900
            overridesCount > 0
901
                ? [
902
                      {
903
                          name: 'Parameter overrides',
904
                          value: `${overridesCount} parameter override(s)`
905
                      }
906
                  ]
907
                : []
908
    };
909

910
    const statusByKey = new Map<string, models.ResourceStatus>();
911
    props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
912
    const nodeByKey = new Map<string, ResourceTreeNode>();
913
    props.tree.nodes
914
        .map(node => ({...node, orphaned: false}))
915
        .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
916
        .forEach(node => {
917
            const status = statusByKey.get(nodeKey(node));
918
            const resourceNode: ResourceTreeNode = {...node};
919
            if (status) {
920
                resourceNode.health = status.health;
921
                resourceNode.status = status.status;
922
                resourceNode.hook = status.hook;
923
                resourceNode.requiresPruning = status.requiresPruning;
924
            }
925
            nodeByKey.set(treeNodeKey(node), resourceNode);
926
        });
927
    const nodes = Array.from(nodeByKey.values());
928
    let roots: ResourceTreeNode[] = [];
929
    const childrenByParentKey = new Map<string, ResourceTreeNode[]>();
930
    const nodesHavingChildren = new Map<string, number>();
931
    const childrenMap = new Map<string, ResourceTreeNode[]>();
932
    const [filters, setFilters] = React.useState(props.filters);
933
    const [filteredGraph, setFilteredGraph] = React.useState([]);
934
    const filteredNodes: any[] = [];
935

936
    React.useEffect(() => {
937
        if (props.filters !== filters) {
938
            setFilters(props.filters);
939
            setFilteredGraph(filteredNodes);
940
            props.setTreeFilterGraph(filteredGraph);
941
        }
942
    }, [props.filters]);
943
    const {podGroupCount, userMsgs, updateUsrHelpTipMsgs, setShowCompactNodes} = props;
944
    const podCount = nodes.filter(node => node.kind === 'Pod').length;
945

946
    React.useEffect(() => {
947
        if (podCount > podGroupCount) {
948
            const userMsg = getUsrMsgKeyToDisplay(appNode.name, 'groupNodes', userMsgs);
949
            updateUsrHelpTipMsgs(userMsg);
950
            if (!userMsg.display) {
951
                setShowCompactNodes(true);
952
            }
953
        }
954
    }, [podCount]);
955

956
    function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
957
        const appKey = appNodeKey(app);
958
        let filtered = 0;
959
        graphNodesFilter.nodes().forEach(nodeId => {
960
            const node: ResourceTreeNode = graphNodesFilter.node(nodeId) as any;
961
            const parentIds = graphNodesFilter.predecessors(nodeId);
962
            if (node.root != null && !predicate(node) && appKey !== nodeId) {
963
                const childIds = graphNodesFilter.successors(nodeId);
964
                graphNodesFilter.removeNode(nodeId);
965
                filtered++;
966
                childIds.forEach((childId: any) => {
967
                    parentIds.forEach((parentId: any) => {
968
                        graphNodesFilter.setEdge(parentId, childId);
969
                    });
970
                });
971
            } else {
972
                if (node.root != null) filteredNodes.push(node);
973
            }
974
        });
975
        if (filtered) {
976
            graphNodesFilter.setNode(FILTERED_INDICATOR_NODE, {height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator});
977
            graphNodesFilter.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE);
978
        }
979
    }
980

981
    if (props.useNetworkingHierarchy) {
982
        // Network view
983
        const hasParents = new Set<string>();
984
        const networkNodes = nodes.filter(node => node.networkingInfo);
985
        const hiddenNodes: ResourceTreeNode[] = [];
986
        networkNodes.forEach(parent => {
987
            findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => {
988
                const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
989
                hasParents.add(treeNodeKey(child));
990
                const parentId = parent.uid;
991
                if (nodesHavingChildren.has(parentId)) {
992
                    nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
993
                } else {
994
                    nodesHavingChildren.set(parentId, 1);
995
                }
996
                if (child.kind !== 'Pod' || !props.showCompactNodes) {
997
                    if (props.getNodeExpansion(parentId)) {
998
                        hasParents.add(treeNodeKey(child));
999
                        children.push(child);
1000
                        childrenByParentKey.set(treeNodeKey(parent), children);
1001
                    } else {
1002
                        hiddenNodes.push(child);
1003
                    }
1004
                } else {
1005
                    processPodGroup(parent, child, props);
1006
                }
1007
            });
1008
        });
1009
        roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node)));
1010
        roots = roots.reduce((acc, curr) => {
1011
            if (hiddenNodes.indexOf(curr) < 0) {
1012
                acc.push(curr);
1013
            }
1014
            return acc;
1015
        }, []);
1016
        const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes);
1017
        const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes);
1018
        const colorsBySource = new Map<string, string>();
1019
        // sources are root internal services and external ingress/service IPs
1020
        const sources = Array.from(
1021
            new Set(
1022
                internalRoots
1023
                    .map(root => treeNodeKey(root))
1024
                    .concat(
1025
                        externalRoots.map(root => root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), [])
1026
                    )
1027
            )
1028
        );
1029
        // assign unique color to each traffic source
1030
        sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
1031

1032
        if (externalRoots.length > 0) {
1033
            graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic});
1034
            externalRoots.sort(compareNodes).forEach(root => {
1035
                const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip);
1036
                const colorByService = new Map<string, string>();
1037
                (childrenByParentKey.get(treeNodeKey(root)) || []).forEach((child, i) => colorByService.set(treeNodeKey(child), TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
1038
                (childrenByParentKey.get(treeNodeKey(root)) || []).sort(compareNodes).forEach((child, i) => {
1039
                    processNode(child, root, [colorByService.get(treeNodeKey(child))]);
1040
                });
1041
                if (root.podGroup && props.showCompactNodes) {
1042
                    setPodGroupNode(root, root);
1043
                } else {
1044
                    graph.setNode(treeNodeKey(root), {...root, width: NODE_WIDTH, height: NODE_HEIGHT, root});
1045
                }
1046
                (childrenByParentKey.get(treeNodeKey(root)) || []).forEach(child => {
1047
                    if (root.namespace === child.namespace) {
1048
                        graph.setEdge(treeNodeKey(root), treeNodeKey(child), {colors: [colorByService.get(treeNodeKey(child))]});
1049
                    }
1050
                });
1051
                loadBalancers.forEach(key => {
1052
                    const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`;
1053
                    graph.setNode(loadBalancerNodeKey, {
1054
                        height: NODE_HEIGHT,
1055
                        width: NODE_WIDTH,
1056
                        type: NODE_TYPES.externalLoadBalancer,
1057
                        label: key,
1058
                        color: colorsBySource.get(key)
1059
                    });
1060
                    graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]});
1061
                    graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]});
1062
                });
1063
            });
1064
        }
1065

1066
        if (internalRoots.length > 0) {
1067
            graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic});
1068
            internalRoots.forEach(root => {
1069
                processNode(root, root, [colorsBySource.get(treeNodeKey(root))]);
1070
                graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root));
1071
            });
1072
        }
1073
        if (props.nodeFilter) {
1074
            // show filtered indicator next to external traffic node is app has it otherwise next to internal traffic node
1075
            filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter);
1076
        }
1077
    } else {
1078
        // Tree view
1079
        const managedKeys = new Set(props.app.status.resources.map(nodeKey));
1080
        const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey));
1081
        const orphans: ResourceTreeNode[] = [];
1082
        let allChildNodes: ResourceTreeNode[] = [];
1083
        nodesHavingChildren.set(appNode.uid, 1);
1084
        if (props.getNodeExpansion(appNode.uid)) {
1085
            nodes.forEach(node => {
1086
                allChildNodes = [];
1087
                if ((node.parentRefs || []).length === 0 || managedKeys.has(nodeKey(node))) {
1088
                    roots.push(node);
1089
                } else {
1090
                    if (orphanedKeys.has(nodeKey(node))) {
1091
                        orphans.push(node);
1092
                    }
1093
                    node.parentRefs.forEach(parent => {
1094
                        const parentId = treeNodeKey(parent);
1095
                        const children = childrenByParentKey.get(parentId) || [];
1096
                        if (nodesHavingChildren.has(parentId)) {
1097
                            nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
1098
                        } else {
1099
                            nodesHavingChildren.set(parentId, 1);
1100
                        }
1101
                        allChildNodes.push(node);
1102
                        if (node.kind !== 'Pod' || !props.showCompactNodes) {
1103
                            if (props.getNodeExpansion(parentId)) {
1104
                                children.push(node);
1105
                                childrenByParentKey.set(parentId, children);
1106
                            }
1107
                        } else {
1108
                            const parentTreeNode = nodeByKey.get(parentId);
1109
                            processPodGroup(parentTreeNode, node, props);
1110
                        }
1111
                        if (props.showCompactNodes) {
1112
                            if (childrenMap.has(parentId)) {
1113
                                childrenMap.set(parentId, childrenMap.get(parentId).concat(allChildNodes));
1114
                            } else {
1115
                                childrenMap.set(parentId, allChildNodes);
1116
                            }
1117
                        }
1118
                    });
1119
                }
1120
            });
1121
        }
1122
        roots.sort(compareNodes).forEach(node => {
1123
            processNode(node, node);
1124
            graph.setEdge(appNodeKey(props.app), treeNodeKey(node));
1125
        });
1126
        orphans.sort(compareNodes).forEach(node => {
1127
            processNode(node, node);
1128
        });
1129
        graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT});
1130
        if (props.nodeFilter) {
1131
            filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter);
1132
        }
1133
        if (props.showCompactNodes) {
1134
            groupNodes(nodes, graph);
1135
        }
1136
    }
1137

1138
    function setPodGroupNode(node: ResourceTreeNode, root: ResourceTreeNode) {
1139
        const numberOfRows = Math.ceil(node.podGroup.pods.length / 8);
1140
        graph.setNode(treeNodeKey(node), {...node, type: NODE_TYPES.podGroup, width: NODE_WIDTH, height: POD_NODE_HEIGHT + 30 * numberOfRows, root});
1141
    }
1142

1143
    function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) {
1144
        if (props.showCompactNodes && node.podGroup) {
1145
            setPodGroupNode(node, root);
1146
        } else {
1147
            graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root});
1148
        }
1149
        (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => {
1150
            if (treeNodeKey(child) === treeNodeKey(root)) {
1151
                return;
1152
            }
1153
            if (node.namespace === child.namespace) {
1154
                graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors});
1155
            }
1156
            processNode(child, root, colors);
1157
        });
1158
    }
1159
    dagre.layout(graph);
1160

1161
    const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string; color?: string; colors?: string | {[key: string]: any}}[] = [];
1162
    const nodeOffset = new Map<string, number>();
1163
    const reverseEdge = new Map<string, number>();
1164
    graph.edges().forEach(edgeInfo => {
1165
        const edge = graph.edge(edgeInfo);
1166
        if (edge.points.length > 1) {
1167
            if (!reverseEdge.has(edgeInfo.w)) {
1168
                reverseEdge.set(edgeInfo.w, 1);
1169
            } else {
1170
                reverseEdge.set(edgeInfo.w, reverseEdge.get(edgeInfo.w) + 1);
1171
            }
1172
            if (!nodeOffset.has(edgeInfo.v)) {
1173
                nodeOffset.set(edgeInfo.v, reverseEdge.get(edgeInfo.w) - 1);
1174
            }
1175
        }
1176
    });
1177
    graph.edges().forEach(edgeInfo => {
1178
        const edge = graph.edge(edgeInfo);
1179
        const colors = (edge.colors as string[]) || [];
1180
        let backgroundImage: string;
1181
        if (colors.length > 0) {
1182
            const step = 100 / colors.length;
1183
            const gradient = colors.map((lineColor, i) => {
1184
                return `${lineColor} ${step * i}%, ${lineColor} ${step * i + step / 2}%, transparent ${step * i + step / 2}%, transparent ${step * (i + 1)}%`;
1185
            });
1186
            backgroundImage = `linear-gradient(90deg, ${gradient})`;
1187
        }
1188

1189
        const lines: Line[] = [];
1190
        // don't render connections from hidden node representing internal traffic
1191
        if (edgeInfo.v === INTERNAL_TRAFFIC_NODE || edgeInfo.w === INTERNAL_TRAFFIC_NODE) {
1192
            return;
1193
        }
1194
        if (edge.points.length > 1) {
1195
            const startNode = graph.node(edgeInfo.v);
1196
            const endNode = graph.node(edgeInfo.w);
1197
            const offset = nodeOffset.get(edgeInfo.v);
1198
            let startNodeRight = props.useNetworkingHierarchy ? 162 : 142;
1199
            const endNodeLeft = 140;
1200
            let spaceForExpansionIcon = 0;
1201
            if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE) && !edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
1202
                lines.push({x1: startNode.x + 10, y1: startNode.y, x2: endNode.x - endNodeLeft, y2: endNode.y});
1203
            } else {
1204
                if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
1205
                    startNodeRight = 152;
1206
                    spaceForExpansionIcon = 5;
1207
                }
1208
                const len = reverseEdge.get(edgeInfo.w) + 1;
1209
                const yEnd = endNode.y - endNode.height / 2 + (endNode.height / len + (endNode.height / len) * offset);
1210
                const firstBend =
1211
                    spaceForExpansionIcon +
1212
                    startNode.x +
1213
                    startNodeRight +
1214
                    (endNode.x - startNode.x - startNodeRight - endNodeLeft) / len +
1215
                    ((endNode.x - startNode.x - startNodeRight - endNodeLeft) / len) * offset;
1216
                lines.push({x1: startNode.x + startNodeRight, y1: startNode.y, x2: firstBend, y2: startNode.y});
1217
                if (startNode.y - yEnd >= 1 || yEnd - startNode.y >= 1) {
1218
                    lines.push({x1: firstBend, y1: startNode.y, x2: firstBend, y2: yEnd});
1219
                }
1220
                lines.push({x1: firstBend, y1: yEnd, x2: endNode.x - endNodeLeft, y2: yEnd});
1221
            }
1222
        }
1223
        edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage, colors: [{colors}]});
1224
    });
1225
    const graphNodes = graph.nodes();
1226
    const size = getGraphSize(graphNodes.map(id => graph.node(id)));
1227
    return (
1228
        (graphNodes.length === 0 && (
1229
            <EmptyState icon=' fa fa-network-wired'>
1230
                <h4>Your application has no network resources</h4>
1231
                <h5>Try switching to tree or list view</h5>
1232
            </EmptyState>
1233
        )) || (
1234
            <div
1235
                className={classNames('application-resource-tree', {'application-resource-tree--network': props.useNetworkingHierarchy})}
1236
                style={{width: size.width + 150, height: size.height + 250, transformOrigin: '0% 0%', transform: `scale(${props.zoom})`}}>
1237
                {graphNodes.map(key => {
1238
                    const node = graph.node(key);
1239
                    const nodeType = node.type;
1240
                    switch (nodeType) {
1241
                        case NODE_TYPES.filteredIndicator:
1242
                            return <React.Fragment key={key}>{renderFilteredNode(node as any, props.onClearFilter)}</React.Fragment>;
1243
                        case NODE_TYPES.externalTraffic:
1244
                            return <React.Fragment key={key}>{renderTrafficNode(node)}</React.Fragment>;
1245
                        case NODE_TYPES.internalTraffic:
1246
                            return null;
1247
                        case NODE_TYPES.externalLoadBalancer:
1248
                            return <React.Fragment key={key}>{renderLoadBalancerNode(node as any)}</React.Fragment>;
1249
                        case NODE_TYPES.groupedNodes:
1250
                            return <React.Fragment key={key}>{renderGroupedNodes(props, node as any)}</React.Fragment>;
1251
                        case NODE_TYPES.podGroup:
1252
                            return <React.Fragment key={key}>{renderPodGroup(props, key, node as ResourceTreeNode & dagre.Node, childrenMap)}</React.Fragment>;
1253
                        default:
1254
                            return <React.Fragment key={key}>{renderResourceNode(props, key, node as ResourceTreeNode & dagre.Node, nodesHavingChildren)}</React.Fragment>;
1255
                    }
1256
                })}
1257
                {edges.map(edge => (
1258
                    <div key={`${edge.from}-${edge.to}`} className='application-resource-tree__edge'>
1259
                        {edge.lines.map((line, i) => {
1260
                            const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2));
1261
                            const xMid = (line.x1 + line.x2) / 2;
1262
                            const yMid = (line.y1 + line.y2) / 2;
1263
                            const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI;
1264
                            const lastLine = i === edge.lines.length - 1 ? line : null;
1265
                            let arrowColor = null;
1266
                            if (edge.colors) {
1267
                                if (Array.isArray(edge.colors)) {
1268
                                    const firstColor = edge.colors[0];
1269
                                    if (firstColor.colors) {
1270
                                        arrowColor = firstColor.colors;
1271
                                    }
1272
                                }
1273
                            }
1274
                            return (
1275
                                <div
1276
                                    className='application-resource-tree__line'
1277
                                    key={i}
1278
                                    style={{
1279
                                        width: distance,
1280
                                        left: xMid - distance / 2,
1281
                                        top: yMid,
1282
                                        backgroundImage: edge.backgroundImage,
1283
                                        transform: props.useNetworkingHierarchy ? `translate(140px, 35px) rotate(${angle}deg)` : `translate(150px, 35px) rotate(${angle}deg)`
1284
                                    }}>
1285
                                    {lastLine && props.useNetworkingHierarchy && <ArrowConnector color={arrowColor} left={xMid + distance / 2} top={yMid} angle={angle} />}
1286
                                </div>
1287
                            );
1288
                        })}
1289
                    </div>
1290
                ))}
1291
            </div>
1292
        )
1293
    );
1294
};
1295

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.