argo-cd

Форк
0
1041 строка · 68.3 Кб
1
import {DropDownMenu, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
2
import * as classNames from 'classnames';
3
import * as PropTypes from 'prop-types';
4
import * as React from 'react';
5
import * as ReactDOM from 'react-dom';
6
import * as models from '../../../shared/models';
7
import {RouteComponentProps} from 'react-router';
8
import {BehaviorSubject, combineLatest, from, merge, Observable} from 'rxjs';
9
import {delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
10

11
import {DataLoader, EmptyState, ErrorNotification, ObservableQuery, Page, Paginate, Revision, Timestamp} from '../../../shared/components';
12
import {AppContext, ContextApis} from '../../../shared/context';
13
import * as appModels from '../../../shared/models';
14
import {AppDetailsPreferences, AppsDetailsViewKey, AppsDetailsViewType, services} from '../../../shared/services';
15

16
import {ApplicationConditions} from '../application-conditions/application-conditions';
17
import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history';
18
import {ApplicationOperationState} from '../application-operation-state/application-operation-state';
19
import {PodGroupType, PodView} from '../application-pod-view/pod-view';
20
import {ApplicationResourceTree, ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
21
import {ApplicationStatusPanel} from '../application-status-panel/application-status-panel';
22
import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
23
import {ResourceDetails} from '../resource-details/resource-details';
24
import * as AppUtils from '../utils';
25
import {ApplicationResourceList} from './application-resource-list';
26
import {Filters, FiltersProps} from './application-resource-filter';
27
import {getAppDefaultSource, urlPattern, helpTip} from '../utils';
28
import {ChartDetails, ResourceStatus} from '../../../shared/models';
29
import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown';
30
import {useSidebarTarget} from '../../../sidebar/sidebar';
31

32
import './application-details.scss';
33
import {AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service';
34

35
interface ApplicationDetailsState {
36
    page: number;
37
    revision?: string;
38
    groupedResources?: ResourceStatus[];
39
    slidingPanelPage?: number;
40
    filteredGraph?: any[];
41
    truncateNameOnRight?: boolean;
42
    collapsedNodes?: string[];
43
    extensions?: AppViewExtension[];
44
    extensionsMap?: {[key: string]: AppViewExtension};
45
    statusExtensions?: StatusPanelExtension[];
46
    statusExtensionsMap?: {[key: string]: StatusPanelExtension};
47
}
48

49
interface FilterInput {
50
    name: string[];
51
    kind: string[];
52
    health: string[];
53
    sync: string[];
54
    namespace: string[];
55
}
56

57
const ApplicationDetailsFilters = (props: FiltersProps) => {
58
    const sidebarTarget = useSidebarTarget();
59
    return ReactDOM.createPortal(<Filters {...props} />, sidebarTarget?.current);
60
};
61

62
export const NodeInfo = (node?: string): {key: string; container: number} => {
63
    const nodeContainer = {key: '', container: 0};
64
    if (node) {
65
        const parts = node.split('/');
66
        nodeContainer.key = parts.slice(0, 4).join('/');
67
        nodeContainer.container = parseInt(parts[4] || '0', 10);
68
    }
69
    return nodeContainer;
70
};
71

72
export const SelectNode = (fullName: string, containerIndex = 0, tab: string = null, appContext: ContextApis) => {
73
    const node = fullName ? `${fullName}/${containerIndex}` : null;
74
    appContext.navigation.goto('.', {node, tab}, {replace: true});
75
};
76

77
export class ApplicationDetails extends React.Component<RouteComponentProps<{appnamespace: string; name: string}>, ApplicationDetailsState> {
78
    public static contextTypes = {
79
        apis: PropTypes.object
80
    };
81

82
    private appChanged = new BehaviorSubject<appModels.Application>(null);
83
    private appNamespace: string;
84

85
    constructor(props: RouteComponentProps<{appnamespace: string; name: string}>) {
86
        super(props);
87
        const extensions = services.extensions.getAppViewExtensions();
88
        const extensionsMap: {[key: string]: AppViewExtension} = {};
89
        extensions.forEach(ext => {
90
            extensionsMap[ext.title] = ext;
91
        });
92
        const statusExtensions = services.extensions.getStatusPanelExtensions();
93
        const statusExtensionsMap: {[key: string]: StatusPanelExtension} = {};
94
        statusExtensions.forEach(ext => {
95
            statusExtensionsMap[ext.id] = ext;
96
        });
97
        this.state = {
98
            page: 0,
99
            groupedResources: [],
100
            slidingPanelPage: 0,
101
            filteredGraph: [],
102
            truncateNameOnRight: false,
103
            collapsedNodes: [],
104
            extensions,
105
            extensionsMap,
106
            statusExtensions,
107
            statusExtensionsMap
108
        };
109
        if (typeof this.props.match.params.appnamespace === 'undefined') {
110
            this.appNamespace = '';
111
        } else {
112
            this.appNamespace = this.props.match.params.appnamespace;
113
        }
114
    }
115

116
    private get showOperationState() {
117
        return new URLSearchParams(this.props.history.location.search).get('operation') === 'true';
118
    }
119

120
    private setNodeExpansion(node: string, isExpanded: boolean) {
121
        const index = this.state.collapsedNodes.indexOf(node);
122
        if (isExpanded && index >= 0) {
123
            this.state.collapsedNodes.splice(index, 1);
124
            const updatedNodes = this.state.collapsedNodes.slice();
125
            this.setState({collapsedNodes: updatedNodes});
126
        } else if (!isExpanded && index < 0) {
127
            const updatedNodes = this.state.collapsedNodes.slice();
128
            updatedNodes.push(node);
129
            this.setState({collapsedNodes: updatedNodes});
130
        }
131
    }
132

133
    private getNodeExpansion(node: string): boolean {
134
        return this.state.collapsedNodes.indexOf(node) < 0;
135
    }
136

137
    private get showConditions() {
138
        return new URLSearchParams(this.props.history.location.search).get('conditions') === 'true';
139
    }
140

141
    private get selectedRollbackDeploymentIndex() {
142
        return parseInt(new URLSearchParams(this.props.history.location.search).get('rollback'), 10);
143
    }
144

145
    private get selectedNodeInfo() {
146
        return NodeInfo(new URLSearchParams(this.props.history.location.search).get('node'));
147
    }
148

149
    private get selectedNodeKey() {
150
        const nodeContainer = this.selectedNodeInfo;
151
        return nodeContainer.key;
152
    }
153

154
    private get selectedExtension() {
155
        return new URLSearchParams(this.props.history.location.search).get('extension');
156
    }
157

158
    private closeGroupedNodesPanel() {
159
        this.setState({groupedResources: []});
160
        this.setState({slidingPanelPage: 0});
161
    }
162

163
    private toggleCompactView(appName: string, pref: AppDetailsPreferences) {
164
        pref.userHelpTipMsgs = pref.userHelpTipMsgs.map(usrMsg => (usrMsg.appName === appName && usrMsg.msgKey === 'groupNodes' ? {...usrMsg, display: true} : usrMsg));
165
        services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: !pref.groupNodes}});
166
    }
167

168
    private getPageTitle(view: string) {
169
        const {Tree, Pods, Network, List} = AppsDetailsViewKey;
170
        switch (view) {
171
            case Tree:
172
                return 'Application Details Tree';
173
            case Network:
174
                return 'Application Details Network';
175
            case Pods:
176
                return 'Application Details Pods';
177
            case List:
178
                return 'Application Details List';
179
        }
180
        return '';
181
    }
182

183
    public render() {
184
        return (
185
            <ObservableQuery>
186
                {q => (
187
                    <DataLoader
188
                        errorRenderer={error => <Page title='Application Details'>{error}</Page>}
189
                        loadingRenderer={() => <Page title='Application Details'>Loading...</Page>}
190
                        input={this.props.match.params.name}
191
                        load={name =>
192
                            combineLatest([this.loadAppInfo(name, this.appNamespace), services.viewPreferences.getPreferences(), q]).pipe(
193
                                map(items => {
194
                                    const application = items[0].application;
195
                                    const pref = items[1].appDetails;
196
                                    const params = items[2];
197
                                    if (params.get('resource') != null) {
198
                                        pref.resourceFilter = params
199
                                            .get('resource')
200
                                            .split(',')
201
                                            .filter(item => !!item);
202
                                    }
203
                                    if (params.get('view') != null) {
204
                                        pref.view = params.get('view') as AppsDetailsViewType;
205
                                    } else {
206
                                        const appDefaultView = (application.metadata &&
207
                                            application.metadata.annotations &&
208
                                            application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType;
209
                                        if (appDefaultView != null) {
210
                                            pref.view = appDefaultView;
211
                                        }
212
                                    }
213
                                    if (params.get('orphaned') != null) {
214
                                        pref.orphanedResources = params.get('orphaned') === 'true';
215
                                    }
216
                                    if (params.get('podSortMode') != null) {
217
                                        pref.podView.sortMode = params.get('podSortMode') as PodGroupType;
218
                                    } else {
219
                                        const appDefaultPodSort = (application.metadata &&
220
                                            application.metadata.annotations &&
221
                                            application.metadata.annotations[appModels.AnnotationDefaultPodSort]) as PodGroupType;
222
                                        if (appDefaultPodSort != null) {
223
                                            pref.podView.sortMode = appDefaultPodSort;
224
                                        }
225
                                    }
226
                                    return {...items[0], pref};
227
                                })
228
                            )
229
                        }>
230
                        {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => {
231
                            tree.nodes = tree.nodes || [];
232
                            const treeFilter = this.getTreeFilter(pref.resourceFilter);
233
                            const setFilter = (items: string[]) => {
234
                                this.appContext.apis.navigation.goto('.', {resource: items.join(',')}, {replace: true});
235
                                services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
236
                            };
237
                            const clearFilter = () => setFilter([]);
238
                            const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
239
                            const appNodesByName = this.groupAppNodesByKey(application, tree);
240
                            const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null;
241
                            const isAppSelected = selectedItem === application;
242
                            const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode);
243
                            const operationState = application.status.operationState;
244
                            const conditions = application.status.conditions || [];
245
                            const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy');
246
                            const tab = new URLSearchParams(this.props.history.location.search).get('tab');
247
                            const source = getAppDefaultSource(application);
248
                            const showToolTip = pref?.userHelpTipMsgs.find(usrMsg => usrMsg.appName === application.metadata.name);
249
                            const resourceNodes = (): any[] => {
250
                                const statusByKey = new Map<string, models.ResourceStatus>();
251
                                application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res));
252
                                const resources = new Map<string, any>();
253
                                tree.nodes
254
                                    .map(node => ({...node, orphaned: false}))
255
                                    .concat(((pref.orphanedResources && tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
256
                                    .forEach(node => {
257
                                        const resource: any = {...node};
258
                                        resource.uid = node.uid;
259
                                        const status = statusByKey.get(AppUtils.nodeKey(node));
260
                                        if (status) {
261
                                            resource.health = status.health;
262
                                            resource.status = status.status;
263
                                            resource.hook = status.hook;
264
                                            resource.syncWave = status.syncWave;
265
                                            resource.requiresPruning = status.requiresPruning;
266
                                        }
267
                                        resources.set(node.uid || AppUtils.nodeKey(node), resource);
268
                                    });
269
                                const resourcesRef = Array.from(resources.values());
270
                                return resourcesRef;
271
                            };
272

273
                            const filteredRes = resourceNodes().filter(res => {
274
                                const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''};
275
                                resNode.root = resNode;
276
                                return this.filterTreeNode(resNode, treeFilter);
277
                            });
278
                            const openGroupNodeDetails = (groupdedNodeIds: string[]) => {
279
                                const resources = resourceNodes();
280
                                this.setState({
281
                                    groupedResources: groupdedNodeIds
282
                                        ? resources.filter(res => groupdedNodeIds.includes(res.uid) || groupdedNodeIds.includes(AppUtils.nodeKey(res)))
283
                                        : []
284
                                });
285
                            };
286

287
                            const renderCommitMessage = (message: string) =>
288
                                message.split(/\s/).map(part =>
289
                                    urlPattern.test(part) ? (
290
                                        <a href={part} target='_blank' rel='noopener noreferrer' style={{overflowWrap: 'anywhere', wordBreak: 'break-word'}}>
291
                                            {part}{' '}
292
                                        </a>
293
                                    ) : (
294
                                        part + ' '
295
                                    )
296
                                );
297
                            const {Tree, Pods, Network, List} = AppsDetailsViewKey;
298
                            const zoomNum = (pref.zoom * 100).toFixed(0);
299
                            const setZoom = (s: number) => {
300
                                let targetZoom: number = pref.zoom + s;
301
                                if (targetZoom <= 0.05) {
302
                                    targetZoom = 0.1;
303
                                } else if (targetZoom > 2.0) {
304
                                    targetZoom = 2.0;
305
                                }
306
                                services.viewPreferences.updatePreferences({appDetails: {...pref, zoom: targetZoom}});
307
                            };
308
                            const setFilterGraph = (filterGraph: any[]) => {
309
                                this.setState({filteredGraph: filterGraph});
310
                            };
311
                            const setShowCompactNodes = (showCompactView: boolean) => {
312
                                services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: showCompactView}});
313
                            };
314
                            const updateHelpTipState = (usrHelpTip: models.UserMessages) => {
315
                                const existingIndex = pref.userHelpTipMsgs.findIndex(msg => msg.appName === usrHelpTip.appName && msg.msgKey === usrHelpTip.msgKey);
316
                                if (existingIndex !== -1) {
317
                                    pref.userHelpTipMsgs[existingIndex] = usrHelpTip;
318
                                } else {
319
                                    (pref.userHelpTipMsgs || []).push(usrHelpTip);
320
                                }
321
                            };
322
                            const toggleNameDirection = () => {
323
                                this.setState({truncateNameOnRight: !this.state.truncateNameOnRight});
324
                            };
325
                            const expandAll = () => {
326
                                this.setState({collapsedNodes: []});
327
                            };
328
                            const collapseAll = () => {
329
                                const nodes = new Array<ResourceTreeNode>();
330
                                tree.nodes
331
                                    .map(node => ({...node, orphaned: false}))
332
                                    .concat((tree.orphanedNodes || []).map(node => ({...node, orphaned: true})))
333
                                    .forEach(node => {
334
                                        const resourceNode: ResourceTreeNode = {...node};
335
                                        nodes.push(resourceNode);
336
                                    });
337
                                const collapsedNodesList = this.state.collapsedNodes.slice();
338
                                if (pref.view === 'network') {
339
                                    const networkNodes = nodes.filter(node => node.networkingInfo);
340
                                    networkNodes.forEach(parent => {
341
                                        const parentId = parent.uid;
342
                                        if (collapsedNodesList.indexOf(parentId) < 0) {
343
                                            collapsedNodesList.push(parentId);
344
                                        }
345
                                    });
346
                                    this.setState({collapsedNodes: collapsedNodesList});
347
                                } else {
348
                                    const managedKeys = new Set(application.status.resources.map(AppUtils.nodeKey));
349
                                    nodes.forEach(node => {
350
                                        if (!((node.parentRefs || []).length === 0 || managedKeys.has(AppUtils.nodeKey(node)))) {
351
                                            node.parentRefs.forEach(parent => {
352
                                                const parentId = parent.uid;
353
                                                if (collapsedNodesList.indexOf(parentId) < 0) {
354
                                                    collapsedNodesList.push(parentId);
355
                                                }
356
                                            });
357
                                        }
358
                                    });
359
                                    collapsedNodesList.push(application.kind + '-' + application.metadata.namespace + '-' + application.metadata.name);
360
                                    this.setState({collapsedNodes: collapsedNodesList});
361
                                }
362
                            };
363
                            const appFullName = AppUtils.nodeKey({
364
                                group: 'argoproj.io',
365
                                kind: application.kind,
366
                                name: application.metadata.name,
367
                                namespace: application.metadata.namespace
368
                            });
369

370
                            const activeExtension = this.state.statusExtensionsMap[this.selectedExtension];
371

372
                            return (
373
                                <div className={`application-details ${this.props.match.params.name}`}>
374
                                    <Page
375
                                        title={this.props.match.params.name + ' - ' + this.getPageTitle(pref.view)}
376
                                        useTitleOnly={true}
377
                                        topBarTitle={this.getPageTitle(pref.view)}
378
                                        toolbar={{
379
                                            breadcrumbs: [
380
                                                {title: 'Applications', path: '/applications'},
381
                                                {title: <ApplicationsDetailsAppDropdown appName={this.props.match.params.name} />}
382
                                            ],
383
                                            actionMenu: {items: this.getApplicationActionMenu(application, true)},
384
                                            tools: (
385
                                                <React.Fragment key='app-list-tools'>
386
                                                    <div className='application-details__view-type'>
387
                                                        <i
388
                                                            className={classNames('fa fa-sitemap', {selected: pref.view === Tree})}
389
                                                            title='Tree'
390
                                                            onClick={() => {
391
                                                                this.appContext.apis.navigation.goto('.', {view: Tree});
392
                                                                services.viewPreferences.updatePreferences({appDetails: {...pref, view: Tree}});
393
                                                            }}
394
                                                        />
395
                                                        <i
396
                                                            className={classNames('fa fa-th', {selected: pref.view === Pods})}
397
                                                            title='Pods'
398
                                                            onClick={() => {
399
                                                                this.appContext.apis.navigation.goto('.', {view: Pods});
400
                                                                services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}});
401
                                                            }}
402
                                                        />
403
                                                        <i
404
                                                            className={classNames('fa fa-network-wired', {selected: pref.view === Network})}
405
                                                            title='Network'
406
                                                            onClick={() => {
407
                                                                this.appContext.apis.navigation.goto('.', {view: Network});
408
                                                                services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}});
409
                                                            }}
410
                                                        />
411
                                                        <i
412
                                                            className={classNames('fa fa-th-list', {selected: pref.view === List})}
413
                                                            title='List'
414
                                                            onClick={() => {
415
                                                                this.appContext.apis.navigation.goto('.', {view: List});
416
                                                                services.viewPreferences.updatePreferences({appDetails: {...pref, view: List}});
417
                                                            }}
418
                                                        />
419
                                                        {this.state.extensions &&
420
                                                            (this.state.extensions || []).map(ext => (
421
                                                                <i
422
                                                                    key={ext.title}
423
                                                                    className={classNames(`fa ${ext.icon}`, {selected: pref.view === ext.title})}
424
                                                                    title={ext.title}
425
                                                                    onClick={() => {
426
                                                                        this.appContext.apis.navigation.goto('.', {view: ext.title});
427
                                                                        services.viewPreferences.updatePreferences({appDetails: {...pref, view: ext.title}});
428
                                                                    }}
429
                                                                />
430
                                                            ))}
431
                                                    </div>
432
                                                </React.Fragment>
433
                                            )
434
                                        }}>
435
                                        <div className='application-details__wrapper'>
436
                                            <div className='application-details__status-panel'>
437
                                                <ApplicationStatusPanel
438
                                                    application={application}
439
                                                    showDiff={() => this.selectNode(appFullName, 0, 'diff')}
440
                                                    showOperation={() => this.setOperationStatusVisible(true)}
441
                                                    showConditions={() => this.setConditionsStatusVisible(true)}
442
                                                    showExtension={id => this.setExtensionPanelVisible(id)}
443
                                                    showMetadataInfo={revision => this.setState({...this.state, revision})}
444
                                                />
445
                                            </div>
446
                                            <div className='application-details__tree'>
447
                                                {refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
448
                                                {((pref.view === 'tree' || pref.view === 'network') && (
449
                                                    <>
450
                                                        <DataLoader load={() => services.viewPreferences.getPreferences()}>
451
                                                            {viewPref => (
452
                                                                <ApplicationDetailsFilters
453
                                                                    pref={pref}
454
                                                                    tree={tree}
455
                                                                    onSetFilter={setFilter}
456
                                                                    onClearFilter={clearFilter}
457
                                                                    collapsed={viewPref.hideSidebar}
458
                                                                    resourceNodes={this.state.filteredGraph}
459
                                                                />
460
                                                            )}
461
                                                        </DataLoader>
462
                                                        <div className='graph-options-panel'>
463
                                                            <a
464
                                                                className={`group-nodes-button`}
465
                                                                onClick={() => {
466
                                                                    toggleNameDirection();
467
                                                                }}
468
                                                                title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
469
                                                                <i
470
                                                                    className={classNames({
471
                                                                        'fa fa-align-right': this.state.truncateNameOnRight,
472
                                                                        'fa fa-align-left': !this.state.truncateNameOnRight
473
                                                                    })}
474
                                                                />
475
                                                            </a>
476
                                                            {(pref.view === 'tree' || pref.view === 'network') && (
477
                                                                <Tooltip
478
                                                                    content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
479
                                                                    visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
480
                                                                    duration={showToolTip?.duration}
481
                                                                    zIndex={1}>
482
                                                                    <a
483
                                                                        className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
484
                                                                        title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
485
                                                                        onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
486
                                                                        <i className={classNames('fa fa-object-group fa-fw')} />
487
                                                                    </a>
488
                                                                </Tooltip>
489
                                                            )}
490

491
                                                            <span className={`separator`} />
492
                                                            <a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
493
                                                                <i className='fa fa-plus fa-fw' />
494
                                                            </a>
495
                                                            <a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
496
                                                                <i className='fa fa-minus fa-fw' />
497
                                                            </a>
498
                                                            <span className={`separator`} />
499
                                                            <span>
500
                                                                <a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
501
                                                                    <i className='fa fa-search-plus fa-fw' />
502
                                                                </a>
503
                                                                <a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
504
                                                                    <i className='fa fa-search-minus fa-fw' />
505
                                                                </a>
506
                                                                <div className={`zoom-value`}>{zoomNum}%</div>
507
                                                            </span>
508
                                                        </div>
509
                                                        <ApplicationResourceTree
510
                                                            nodeFilter={node => this.filterTreeNode(node, treeFilter)}
511
                                                            selectedNodeFullName={this.selectedNodeKey}
512
                                                            onNodeClick={fullName => this.selectNode(fullName)}
513
                                                            nodeMenu={node =>
514
                                                                AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
515
                                                                    this.getApplicationActionMenu(application, false)
516
                                                                )
517
                                                            }
518
                                                            showCompactNodes={pref.groupNodes}
519
                                                            userMsgs={pref.userHelpTipMsgs}
520
                                                            tree={tree}
521
                                                            app={application}
522
                                                            showOrphanedResources={pref.orphanedResources}
523
                                                            useNetworkingHierarchy={pref.view === 'network'}
524
                                                            onClearFilter={clearFilter}
525
                                                            onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
526
                                                            zoom={pref.zoom}
527
                                                            podGroupCount={pref.podGroupCount}
528
                                                            appContext={this.appContext}
529
                                                            nameDirection={this.state.truncateNameOnRight}
530
                                                            filters={pref.resourceFilter}
531
                                                            setTreeFilterGraph={setFilterGraph}
532
                                                            updateUsrHelpTipMsgs={updateHelpTipState}
533
                                                            setShowCompactNodes={setShowCompactNodes}
534
                                                            setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
535
                                                            getNodeExpansion={node => this.getNodeExpansion(node)}
536
                                                        />
537
                                                    </>
538
                                                )) ||
539
                                                    (pref.view === 'pods' && (
540
                                                        <PodView
541
                                                            tree={tree}
542
                                                            app={application}
543
                                                            onItemClick={fullName => this.selectNode(fullName)}
544
                                                            nodeMenu={node =>
545
                                                                AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
546
                                                                    this.getApplicationActionMenu(application, false)
547
                                                                )
548
                                                            }
549
                                                            quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
550
                                                        />
551
                                                    )) ||
552
                                                    (this.state.extensionsMap[pref.view] != null && (
553
                                                        <ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
554
                                                    )) || (
555
                                                        <div>
556
                                                            <DataLoader load={() => services.viewPreferences.getPreferences()}>
557
                                                                {viewPref => (
558
                                                                    <ApplicationDetailsFilters
559
                                                                        pref={pref}
560
                                                                        tree={tree}
561
                                                                        onSetFilter={setFilter}
562
                                                                        onClearFilter={clearFilter}
563
                                                                        collapsed={viewPref.hideSidebar}
564
                                                                        resourceNodes={filteredRes}
565
                                                                    />
566
                                                                )}
567
                                                            </DataLoader>
568
                                                            {(filteredRes.length > 0 && (
569
                                                                <Paginate
570
                                                                    page={this.state.page}
571
                                                                    data={filteredRes}
572
                                                                    onPageChange={page => this.setState({page})}
573
                                                                    preferencesKey='application-details'>
574
                                                                    {data => (
575
                                                                        <ApplicationResourceList
576
                                                                            onNodeClick={fullName => this.selectNode(fullName)}
577
                                                                            resources={data}
578
                                                                            nodeMenu={node =>
579
                                                                                AppUtils.renderResourceMenu(
580
                                                                                    {...node, root: node},
581
                                                                                    application,
582
                                                                                    tree,
583
                                                                                    this.appContext.apis,
584
                                                                                    this.appChanged,
585
                                                                                    () => this.getApplicationActionMenu(application, false)
586
                                                                                )
587
                                                                            }
588
                                                                            tree={tree}
589
                                                                        />
590
                                                                    )}
591
                                                                </Paginate>
592
                                                            )) || (
593
                                                                <EmptyState icon='fa fa-search'>
594
                                                                    <h4>No resources found</h4>
595
                                                                    <h5>Try to change filter criteria</h5>
596
                                                                </EmptyState>
597
                                                            )}
598
                                                        </div>
599
                                                    )}
600
                                            </div>
601
                                        </div>
602
                                        <SlidingPanel isShown={this.state.groupedResources.length > 0} onClose={() => this.closeGroupedNodesPanel()}>
603
                                            <div className='application-details__sliding-panel-pagination-wrap'>
604
                                                <Paginate
605
                                                    page={this.state.slidingPanelPage}
606
                                                    data={this.state.groupedResources}
607
                                                    onPageChange={page => this.setState({slidingPanelPage: page})}
608
                                                    preferencesKey='grouped-nodes-details'>
609
                                                    {data => (
610
                                                        <ApplicationResourceList
611
                                                            onNodeClick={fullName => this.selectNode(fullName)}
612
                                                            resources={data}
613
                                                            nodeMenu={node =>
614
                                                                AppUtils.renderResourceMenu({...node, root: node}, application, tree, this.appContext.apis, this.appChanged, () =>
615
                                                                    this.getApplicationActionMenu(application, false)
616
                                                                )
617
                                                            }
618
                                                            tree={tree}
619
                                                        />
620
                                                    )}
621
                                                </Paginate>
622
                                            </div>
623
                                        </SlidingPanel>
624
                                        <SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => this.selectNode('')}>
625
                                            <ResourceDetails
626
                                                tree={tree}
627
                                                application={application}
628
                                                isAppSelected={isAppSelected}
629
                                                updateApp={(app: models.Application, query: {validate?: boolean}) => this.updateApp(app, query)}
630
                                                selectedNode={selectedNode}
631
                                                tab={tab}
632
                                            />
633
                                        </SlidingPanel>
634
                                        <ApplicationSyncPanel
635
                                            application={application}
636
                                            hide={() => AppUtils.showDeploy(null, null, this.appContext.apis)}
637
                                            selectedResource={syncResourceKey}
638
                                        />
639
                                        <SlidingPanel isShown={this.selectedRollbackDeploymentIndex > -1} onClose={() => this.setRollbackPanelVisible(-1)}>
640
                                            {this.selectedRollbackDeploymentIndex > -1 && (
641
                                                <ApplicationDeploymentHistory
642
                                                    app={application}
643
                                                    selectedRollbackDeploymentIndex={this.selectedRollbackDeploymentIndex}
644
                                                    rollbackApp={info => this.rollbackApplication(info, application)}
645
                                                    selectDeployment={i => this.setRollbackPanelVisible(i)}
646
                                                />
647
                                            )}
648
                                        </SlidingPanel>
649
                                        <SlidingPanel isShown={this.showOperationState && !!operationState} onClose={() => this.setOperationStatusVisible(false)}>
650
                                            {operationState && <ApplicationOperationState application={application} operationState={operationState} />}
651
                                        </SlidingPanel>
652
                                        <SlidingPanel isShown={this.showConditions && !!conditions} onClose={() => this.setConditionsStatusVisible(false)}>
653
                                            {conditions && <ApplicationConditions conditions={conditions} />}
654
                                        </SlidingPanel>
655
                                        <SlidingPanel isShown={!!this.state.revision} isMiddle={true} onClose={() => this.setState({revision: null})}>
656
                                            {this.state.revision &&
657
                                                (source.chart ? (
658
                                                    <DataLoader
659
                                                        input={application}
660
                                                        load={input =>
661
                                                            services.applications.revisionChartDetails(input.metadata.name, input.metadata.namespace, this.state.revision)
662
                                                        }>
663
                                                        {(m: ChartDetails) => (
664
                                                            <div className='white-box' style={{marginTop: '1.5em'}}>
665
                                                                <div className='white-box__details'>
666
                                                                    <div className='row white-box__details-row'>
667
                                                                        <div className='columns small-3'>Revision:</div>
668
                                                                        <div className='columns small-9'>{this.state.revision}</div>
669
                                                                    </div>
670
                                                                    <div className='row white-box__details-row'>
671
                                                                        <div className='columns small-3'>Helm Chart:</div>
672
                                                                        <div className='columns small-9'>
673
                                                                            {source.chart}&nbsp;
674
                                                                            {m.home && (
675
                                                                                <a
676
                                                                                    title={m.home}
677
                                                                                    onClick={e => {
678
                                                                                        e.stopPropagation();
679
                                                                                        window.open(m.home);
680
                                                                                    }}>
681
                                                                                    <i className='fa fa-external-link-alt' />
682
                                                                                </a>
683
                                                                            )}
684
                                                                        </div>
685
                                                                    </div>
686
                                                                    {m.description && (
687
                                                                        <div className='row white-box__details-row'>
688
                                                                            <div className='columns small-3'>Description:</div>
689
                                                                            <div className='columns small-9'>{m.description}</div>
690
                                                                        </div>
691
                                                                    )}
692
                                                                    {m.maintainers && m.maintainers.length > 0 && (
693
                                                                        <div className='row white-box__details-row'>
694
                                                                            <div className='columns small-3'>Maintainers:</div>
695
                                                                            <div className='columns small-9'>{m.maintainers.join(', ')}</div>
696
                                                                        </div>
697
                                                                    )}
698
                                                                </div>
699
                                                            </div>
700
                                                        )}
701
                                                    </DataLoader>
702
                                                ) : (
703
                                                    <DataLoader
704
                                                        load={() =>
705
                                                            services.applications.revisionMetadata(application.metadata.name, application.metadata.namespace, this.state.revision)
706
                                                        }>
707
                                                        {metadata => (
708
                                                            <div className='white-box' style={{marginTop: '1.5em'}}>
709
                                                                <div className='white-box__details'>
710
                                                                    <div className='row white-box__details-row'>
711
                                                                        <div className='columns small-3'>SHA:</div>
712
                                                                        <div className='columns small-9'>
713
                                                                            <Revision repoUrl={source.repoURL} revision={this.state.revision} />
714
                                                                        </div>
715
                                                                    </div>
716
                                                                </div>
717
                                                                <div className='white-box__details'>
718
                                                                    <div className='row white-box__details-row'>
719
                                                                        <div className='columns small-3'>Date:</div>
720
                                                                        <div className='columns small-9'>
721
                                                                            <Timestamp date={metadata.date} />
722
                                                                        </div>
723
                                                                    </div>
724
                                                                </div>
725
                                                                <div className='white-box__details'>
726
                                                                    <div className='row white-box__details-row'>
727
                                                                        <div className='columns small-3'>Tags:</div>
728
                                                                        <div className='columns small-9'>
729
                                                                            {((metadata.tags || []).length > 0 && metadata.tags.join(', ')) || 'No tags'}
730
                                                                        </div>
731
                                                                    </div>
732
                                                                </div>
733
                                                                <div className='white-box__details'>
734
                                                                    <div className='row white-box__details-row'>
735
                                                                        <div className='columns small-3'>Author:</div>
736
                                                                        <div className='columns small-9'>{metadata.author}</div>
737
                                                                    </div>
738
                                                                </div>
739
                                                                <div className='white-box__details'>
740
                                                                    <div className='row white-box__details-row'>
741
                                                                        <div className='columns small-3'>Message:</div>
742
                                                                        <div className='columns small-9' style={{display: 'flex', alignItems: 'center'}}>
743
                                                                            <div className='application-details__commit-message'>{renderCommitMessage(metadata.message)}</div>
744
                                                                        </div>
745
                                                                    </div>
746
                                                                </div>
747
                                                            </div>
748
                                                        )}
749
                                                    </DataLoader>
750
                                                ))}
751
                                        </SlidingPanel>
752
                                        <SlidingPanel
753
                                            isShown={this.selectedExtension !== '' && activeExtension != null && activeExtension.flyout != null}
754
                                            onClose={() => this.setExtensionPanelVisible('')}>
755
                                            {this.selectedExtension !== '' && activeExtension && activeExtension.flyout && (
756
                                                <activeExtension.flyout application={application} tree={tree} />
757
                                            )}
758
                                        </SlidingPanel>
759
                                    </Page>
760
                                </div>
761
                            );
762
                        }}
763
                    </DataLoader>
764
                )}
765
            </ObservableQuery>
766
        );
767
    }
768

769
    private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) {
770
        const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey];
771
        const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
772
        const ActionMenuItem = (prop: {actionLabel: string}) => <span className={needOverlapLabelOnNarrowScreen ? 'show-for-large' : ''}>{prop.actionLabel}</span>;
773
        const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0;
774
        return [
775
            {
776
                iconClassName: 'fa fa-info-circle',
777
                title: <ActionMenuItem actionLabel='Details' />,
778
                action: () => this.selectNode(fullName)
779
            },
780
            {
781
                iconClassName: 'fa fa-file-medical',
782
                title: <ActionMenuItem actionLabel='Diff' />,
783
                action: () => this.selectNode(fullName, 0, 'diff'),
784
                disabled: app.status.sync.status === appModels.SyncStatuses.Synced
785
            },
786
            {
787
                iconClassName: 'fa fa-sync',
788
                title: <ActionMenuItem actionLabel='Sync' />,
789
                action: () => AppUtils.showDeploy('all', null, this.appContext.apis)
790
            },
791
            {
792
                iconClassName: 'fa fa-info-circle',
793
                title: <ActionMenuItem actionLabel='Sync Status' />,
794
                action: () => this.setOperationStatusVisible(true),
795
                disabled: !app.status.operationState
796
            },
797
            {
798
                iconClassName: 'fa fa-history',
799
                title: hasMultipleSources ? (
800
                    <React.Fragment>
801
                        <ActionMenuItem actionLabel=' History and rollback' />
802
                        {helpTip('Rollback is not supported for apps with multiple sources')}
803
                    </React.Fragment>
804
                ) : (
805
                    <ActionMenuItem actionLabel='History and rollback' />
806
                ),
807
                action: () => {
808
                    this.setRollbackPanelVisible(0);
809
                },
810
                disabled: !app.status.operationState || hasMultipleSources
811
            },
812
            {
813
                iconClassName: 'fa fa-times-circle',
814
                title: <ActionMenuItem actionLabel='Delete' />,
815
                action: () => this.deleteApplication()
816
            },
817
            {
818
                iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}),
819
                title: (
820
                    <React.Fragment>
821
                        <ActionMenuItem actionLabel='Refresh' />{' '}
822
                        <DropDownMenu
823
                            items={[
824
                                {
825
                                    title: 'Hard Refresh',
826
                                    action: () => !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, 'hard')
827
                                }
828
                            ]}
829
                            anchor={() => <i className='fa fa-caret-down' />}
830
                        />
831
                    </React.Fragment>
832
                ),
833
                disabled: !!refreshing,
834
                action: () => {
835
                    if (!refreshing) {
836
                        services.applications.get(app.metadata.name, app.metadata.namespace, 'normal');
837
                        AppUtils.setAppRefreshing(app);
838
                        this.appChanged.next(app);
839
                    }
840
                }
841
            }
842
        ];
843
    }
844

845
    private filterTreeNode(node: ResourceTreeNode, filterInput: FilterInput): boolean {
846
        const syncStatuses = filterInput.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []);
847

848
        const root = node.root || ({} as ResourceTreeNode);
849
        const hook = root && root.hook;
850
        if (
851
            (filterInput.name.length === 0 || this.nodeNameMatchesWildcardFilters(node.name, filterInput.name)) &&
852
            (filterInput.kind.length === 0 || filterInput.kind.indexOf(node.kind) > -1) &&
853
            // include if node's root sync matches filter
854
            (syncStatuses.length === 0 || hook || (root.status && syncStatuses.indexOf(root.status) > -1)) &&
855
            // include if node or node's root health matches filter
856
            (filterInput.health.length === 0 ||
857
                hook ||
858
                (root.health && filterInput.health.indexOf(root.health.status) > -1) ||
859
                (node.health && filterInput.health.indexOf(node.health.status) > -1)) &&
860
            (filterInput.namespace.length === 0 || filterInput.namespace.includes(node.namespace))
861
        ) {
862
            return true;
863
        }
864

865
        return false;
866
    }
867

868
    private nodeNameMatchesWildcardFilters(nodeName: string, filterInputNames: string[]): boolean {
869
        const regularExpression = new RegExp(
870
            filterInputNames
871
                // Escape any regex input to ensure only * can be used
872
                .map(pattern => '^' + this.escapeRegex(pattern) + '$')
873
                // Replace any escaped * with proper regex
874
                .map(pattern => pattern.replace(/\\\*/g, '.*'))
875
                // Join all filterInputs to a single regular expression
876
                .join('|'),
877
            'gi'
878
        );
879
        return regularExpression.test(nodeName);
880
    }
881

882
    private escapeRegex(input: string): string {
883
        return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
884
    }
885

886
    private loadAppInfo(name: string, appNamespace: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> {
887
        return from(services.applications.get(name, appNamespace))
888
            .pipe(
889
                mergeMap(app => {
890
                    const fallbackTree = {
891
                        nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})),
892
                        orphanedNodes: [],
893
                        hosts: []
894
                    } as appModels.ApplicationTree;
895
                    return combineLatest(
896
                        merge(
897
                            from([app]),
898
                            this.appChanged.pipe(filter(item => !!item)),
899
                            AppUtils.handlePageVisibility(() =>
900
                                services.applications
901
                                    .watch({name, appNamespace})
902
                                    .pipe(
903
                                        map(watchEvent => {
904
                                            if (watchEvent.type === 'DELETED') {
905
                                                this.onAppDeleted();
906
                                            }
907
                                            return watchEvent.application;
908
                                        })
909
                                    )
910
                                    .pipe(repeat())
911
                                    .pipe(retryWhen(errors => errors.pipe(delay(500))))
912
                            )
913
                        ),
914
                        merge(
915
                            from([fallbackTree]),
916
                            services.applications.resourceTree(name, appNamespace).catch(() => fallbackTree),
917
                            AppUtils.handlePageVisibility(() =>
918
                                services.applications
919
                                    .watchResourceTree(name, appNamespace)
920
                                    .pipe(repeat())
921
                                    .pipe(retryWhen(errors => errors.pipe(delay(500))))
922
                            )
923
                        )
924
                    );
925
                })
926
            )
927
            .pipe(filter(([application, tree]) => !!application && !!tree))
928
            .pipe(map(([application, tree]) => ({application, tree})));
929
    }
930

931
    private onAppDeleted() {
932
        this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`});
933
        this.appContext.apis.navigation.goto('/applications');
934
    }
935

936
    private async updateApp(app: appModels.Application, query: {validate?: boolean}) {
937
        const latestApp = await services.applications.get(app.metadata.name, app.metadata.namespace);
938
        latestApp.metadata.labels = app.metadata.labels;
939
        latestApp.metadata.annotations = app.metadata.annotations;
940
        latestApp.spec = app.spec;
941
        const updatedApp = await services.applications.update(latestApp, query);
942
        this.appChanged.next(updatedApp);
943
    }
944

945
    private groupAppNodesByKey(application: appModels.Application, tree: appModels.ApplicationTree) {
946
        const nodeByKey = new Map<string, appModels.ResourceDiff | appModels.ResourceNode | appModels.Application>();
947
        tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node));
948
        nodeByKey.set(AppUtils.nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application);
949
        return nodeByKey;
950
    }
951

952
    private getTreeFilter(filterInput: string[]): FilterInput {
953
        const name = new Array<string>();
954
        const kind = new Array<string>();
955
        const health = new Array<string>();
956
        const sync = new Array<string>();
957
        const namespace = new Array<string>();
958
        for (const item of filterInput || []) {
959
            const [type, val] = item.split(':');
960
            switch (type) {
961
                case 'name':
962
                    name.push(val);
963
                    break;
964
                case 'kind':
965
                    kind.push(val);
966
                    break;
967
                case 'health':
968
                    health.push(val);
969
                    break;
970
                case 'sync':
971
                    sync.push(val);
972
                    break;
973
                case 'namespace':
974
                    namespace.push(val);
975
                    break;
976
            }
977
        }
978
        return {kind, health, sync, namespace, name};
979
    }
980

981
    private setOperationStatusVisible(isVisible: boolean) {
982
        this.appContext.apis.navigation.goto('.', {operation: isVisible}, {replace: true});
983
    }
984

985
    private setConditionsStatusVisible(isVisible: boolean) {
986
        this.appContext.apis.navigation.goto('.', {conditions: isVisible}, {replace: true});
987
    }
988

989
    private setRollbackPanelVisible(selectedDeploymentIndex = 0) {
990
        this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex}, {replace: true});
991
    }
992

993
    private setExtensionPanelVisible(selectedExtension = '') {
994
        this.appContext.apis.navigation.goto('.', {extension: selectedExtension}, {replace: true});
995
    }
996

997
    private selectNode(fullName: string, containerIndex = 0, tab: string = null) {
998
        SelectNode(fullName, containerIndex, tab, this.appContext.apis);
999
    }
1000

1001
    private async rollbackApplication(revisionHistory: appModels.RevisionHistory, application: appModels.Application) {
1002
        try {
1003
            const needDisableRollback = application.spec.syncPolicy && application.spec.syncPolicy.automated;
1004
            let confirmationMessage = `Are you sure you want to rollback application '${this.props.match.params.name}'?`;
1005
            if (needDisableRollback) {
1006
                confirmationMessage = `Auto-Sync needs to be disabled in order for rollback to occur.
1007
Are you sure you want to disable auto-sync and rollback application '${this.props.match.params.name}'?`;
1008
            }
1009

1010
            const confirmed = await this.appContext.apis.popup.confirm('Rollback application', confirmationMessage);
1011
            if (confirmed) {
1012
                if (needDisableRollback) {
1013
                    const update = JSON.parse(JSON.stringify(application)) as appModels.Application;
1014
                    update.spec.syncPolicy = {automated: null};
1015
                    await services.applications.update(update);
1016
                }
1017
                await services.applications.rollback(this.props.match.params.name, this.appNamespace, revisionHistory.id);
1018
                this.appChanged.next(await services.applications.get(this.props.match.params.name, this.appNamespace));
1019
                this.setRollbackPanelVisible(-1);
1020
            }
1021
        } catch (e) {
1022
            this.appContext.apis.notifications.show({
1023
                content: <ErrorNotification title='Unable to rollback application' e={e} />,
1024
                type: NotificationType.Error
1025
            });
1026
        }
1027
    }
1028

1029
    private get appContext(): AppContext {
1030
        return this.context as AppContext;
1031
    }
1032

1033
    private async deleteApplication() {
1034
        await AppUtils.deleteApplication(this.props.match.params.name, this.appNamespace, this.appContext.apis);
1035
    }
1036
}
1037

1038
const ExtensionView = (props: {extension: AppViewExtension; application: models.Application; tree: models.ApplicationTree}) => {
1039
    const {extension, application, tree} = props;
1040
    return <extension.component application={application} tree={tree} />;
1041
};
1042

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

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

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

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