argo-cd

Форк
0
370 строк · 18.6 Кб
1
import {DataLoader, DropDown, Tab, Tabs} from 'argo-ui';
2
import * as React from 'react';
3
import {useState} from 'react';
4
import {EventsList, YamlEditor} from '../../../shared/components';
5
import * as models from '../../../shared/models';
6
import {ErrorBoundary} from '../../../shared/components/error-boundary/error-boundary';
7
import {Context} from '../../../shared/context';
8
import {Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, ResourceNode, State, SyncStatuses} from '../../../shared/models';
9
import {services} from '../../../shared/services';
10
import {ResourceTabExtension} from '../../../shared/services/extensions-service';
11
import {NodeInfo, SelectNode} from '../application-details/application-details';
12
import {ApplicationNodeInfo} from '../application-node-info/application-node-info';
13
import {ApplicationParameters} from '../application-parameters/application-parameters';
14
import {ApplicationResourceEvents} from '../application-resource-events/application-resource-events';
15
import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
16
import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff';
17
import {ApplicationSummary} from '../application-summary/application-summary';
18
import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer';
19
import {PodTerminalViewer} from '../pod-terminal-viewer/pod-terminal-viewer';
20
import {ResourceIcon} from '../resource-icon';
21
import {ResourceLabel} from '../resource-label';
22
import * as AppUtils from '../utils';
23
import './resource-details.scss';
24

25
const jsonMergePatch = require('json-merge-patch');
26

27
interface ResourceDetailsProps {
28
    selectedNode: ResourceNode;
29
    updateApp: (app: Application, query: {validate?: boolean}) => Promise<any>;
30
    application: Application;
31
    isAppSelected: boolean;
32
    tree: ApplicationTree;
33
    tab?: string;
34
}
35

36
export const ResourceDetails = (props: ResourceDetailsProps) => {
37
    const {selectedNode, updateApp, application, isAppSelected, tree} = {...props};
38
    const [activeContainer, setActiveContainer] = useState();
39
    const appContext = React.useContext(Context);
40
    const tab = new URLSearchParams(appContext.history.location.search).get('tab');
41
    const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node'));
42
    const selectedNodeKey = selectedNodeInfo.key;
43

44
    const getResourceTabs = (
45
        node: ResourceNode,
46
        state: State,
47
        podState: State,
48
        events: Event[],
49
        extensionTabs: ResourceTabExtension[],
50
        tabs: Tab[],
51
        execEnabled: boolean,
52
        execAllowed: boolean,
53
        logsAllowed: boolean
54
    ) => {
55
        if (!node || node === undefined) {
56
            return [];
57
        }
58
        if (state) {
59
            const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0);
60
            tabs.push({
61
                title: 'EVENTS',
62
                icon: 'fa fa-calendar-alt',
63
                badge: (numErrors > 0 && numErrors) || null,
64
                key: 'events',
65
                content: (
66
                    <div className='application-resource-events'>
67
                        <EventsList events={events} />
68
                    </div>
69
                )
70
            });
71
        }
72
        if (podState && podState.metadata && podState.spec) {
73
            const containerGroups = [
74
                {
75
                    offset: 0,
76
                    title: 'CONTAINERS',
77
                    containers: podState.spec.containers || []
78
                }
79
            ];
80
            if (podState.spec.initContainers?.length > 0) {
81
                containerGroups.push({
82
                    offset: (podState.spec.containers || []).length,
83
                    title: 'INIT CONTAINERS',
84
                    containers: podState.spec.initContainers || []
85
                });
86
            }
87

88
            const onClickContainer = (group: any, i: number, activeTab: string) => {
89
                setActiveContainer(group.offset + i);
90
                SelectNode(selectedNodeKey, activeContainer, activeTab, appContext);
91
            };
92

93
            if (logsAllowed) {
94
                tabs = tabs.concat([
95
                    {
96
                        key: 'logs',
97
                        icon: 'fa fa-align-left',
98
                        title: 'LOGS',
99
                        content: (
100
                            <div className='application-details__tab-content-full-height'>
101
                                <PodsLogsViewer
102
                                    podName={(state.kind === 'Pod' && state.metadata.name) || ''}
103
                                    group={node.group}
104
                                    kind={node.kind}
105
                                    name={node.name}
106
                                    namespace={podState.metadata.namespace}
107
                                    applicationName={application.metadata.name}
108
                                    applicationNamespace={application.metadata.namespace}
109
                                    containerName={AppUtils.getContainerName(podState, activeContainer)}
110
                                    containerGroups={containerGroups}
111
                                    onClickContainer={onClickContainer}
112
                                />
113
                            </div>
114
                        )
115
                    }
116
                ]);
117
            }
118
            if (selectedNode.kind === 'Pod' && execEnabled && execAllowed) {
119
                tabs = tabs.concat([
120
                    {
121
                        key: 'exec',
122
                        icon: 'fa fa-terminal',
123
                        title: 'Terminal',
124
                        content: (
125
                            <PodTerminalViewer
126
                                applicationName={application.metadata.name}
127
                                applicationNamespace={application.metadata.namespace}
128
                                projectName={application.spec.project}
129
                                podState={podState}
130
                                selectedNode={selectedNode}
131
                                containerName={AppUtils.getContainerName(podState, activeContainer)}
132
                                onClickContainer={onClickContainer}
133
                            />
134
                        )
135
                    }
136
                ]);
137
            }
138
        }
139
        if (state) {
140
            extensionTabs.forEach((tabExtensions, i) => {
141
                tabs.push({
142
                    title: tabExtensions.title,
143
                    key: `extension-${i}`,
144
                    content: (
145
                        <ErrorBoundary message={`Something went wrong with Extension for ${state.kind}`}>
146
                            <tabExtensions.component tree={tree} resource={state} application={application} />
147
                        </ErrorBoundary>
148
                    ),
149
                    icon: tabExtensions.icon
150
                });
151
            });
152
        }
153
        return tabs;
154
    };
155

156
    const getApplicationTabs = () => {
157
        const tabs: Tab[] = [
158
            {
159
                title: 'SUMMARY',
160
                key: 'summary',
161
                content: <ApplicationSummary app={application} updateApp={(app, query: {validate?: boolean}) => updateApp(app, query)} />
162
            },
163
            {
164
                title: 'PARAMETERS',
165
                key: 'parameters',
166
                content: (
167
                    <DataLoader
168
                        key='appDetails'
169
                        input={application}
170
                        load={app =>
171
                            services.repos.appDetails(AppUtils.getAppDefaultSource(app), app.metadata.name, app.spec.project).catch(() => ({
172
                                type: 'Directory' as AppSourceType,
173
                                path: AppUtils.getAppDefaultSource(app).path
174
                            }))
175
                        }>
176
                        {(details: RepoAppDetails) => (
177
                            <ApplicationParameters
178
                                save={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
179
                                application={application}
180
                                details={details}
181
                            />
182
                        )}
183
                    </DataLoader>
184
                )
185
            },
186
            {
187
                title: 'MANIFEST',
188
                key: 'manifest',
189
                content: (
190
                    <YamlEditor
191
                        minHeight={800}
192
                        input={application.spec}
193
                        onSave={async patch => {
194
                            const spec = JSON.parse(JSON.stringify(application.spec));
195
                            return services.applications.updateSpec(application.metadata.name, application.metadata.namespace, jsonMergePatch.apply(spec, JSON.parse(patch)));
196
                        }}
197
                    />
198
                )
199
            }
200
        ];
201

202
        if (application.status.sync.status !== SyncStatuses.Synced) {
203
            tabs.push({
204
                icon: 'fa fa-file-medical',
205
                title: 'DIFF',
206
                key: 'diff',
207
                content: (
208
                    <DataLoader
209
                        key='diff'
210
                        load={async () =>
211
                            await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
212
                                fields: ['items.normalizedLiveState', 'items.predictedLiveState', 'items.group', 'items.kind', 'items.namespace', 'items.name']
213
                            })
214
                        }>
215
                        {managedResources => <ApplicationResourcesDiff states={managedResources} />}
216
                    </DataLoader>
217
                )
218
            });
219
        }
220

221
        tabs.push({
222
            title: 'EVENTS',
223
            key: 'event',
224
            content: <ApplicationResourceEvents applicationName={application.metadata.name} applicationNamespace={application.metadata.namespace} />
225
        });
226

227
        const extensionTabs = services.extensions.getResourceTabs('argoproj.io', 'Application').map((ext, i) => ({
228
            title: ext.title,
229
            key: `extension-${i}`,
230
            content: <ext.component resource={application} tree={tree} application={application} />,
231
            icon: ext.icon
232
        }));
233

234
        return tabs.concat(extensionTabs);
235
    };
236

237
    const extensions = selectedNode?.kind ? services.extensions.getResourceTabs(selectedNode?.group || '', selectedNode?.kind) : [];
238

239
    return (
240
        <div style={{width: '100%', height: '100%'}}>
241
            {selectedNode && (
242
                <DataLoader
243
                    noLoaderOnInputChange={true}
244
                    input={selectedNode.resourceVersion}
245
                    load={async () => {
246
                        const managedResources = await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
247
                            id: {
248
                                name: selectedNode.name,
249
                                namespace: selectedNode.namespace,
250
                                kind: selectedNode.kind,
251
                                group: selectedNode.group
252
                            }
253
                        });
254
                        const controlled = managedResources.find(item => AppUtils.isSameNode(selectedNode, item));
255
                        const summary = application.status.resources.find(item => AppUtils.isSameNode(selectedNode, item));
256
                        const controlledState = (controlled && summary && {summary, state: controlled}) || null;
257
                        const resQuery = {...selectedNode};
258
                        if (controlled && controlled.targetState) {
259
                            resQuery.version = AppUtils.parseApiVersion(controlled.targetState.apiVersion).version;
260
                        }
261
                        const liveState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, resQuery).catch(() => null);
262
                        const events =
263
                            (liveState &&
264
                                (await services.applications.resourceEvents(application.metadata.name, application.metadata.namespace, {
265
                                    name: liveState.metadata.name,
266
                                    namespace: liveState.metadata.namespace,
267
                                    uid: liveState.metadata.uid
268
                                }))) ||
269
                            [];
270
                        let podState: State;
271
                        if (selectedNode.kind === 'Pod') {
272
                            podState = liveState;
273
                        } else {
274
                            const childPod = AppUtils.findChildPod(selectedNode, tree);
275
                            if (childPod) {
276
                                podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null);
277
                            }
278
                        }
279

280
                        const settings = await services.authService.settings();
281
                        const execEnabled = settings.execEnabled;
282
                        const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
283
                        const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
284
                        const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
285
                        return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links};
286
                    }}>
287
                    {data => (
288
                        <React.Fragment>
289
                            <div className='resource-details__header'>
290
                                <div style={{display: 'flex', flexDirection: 'column', marginRight: '15px', alignItems: 'center', fontSize: '12px'}}>
291
                                    <ResourceIcon kind={selectedNode.kind} />
292
                                    {ResourceLabel({kind: selectedNode.kind})}
293
                                </div>
294
                                <h1>{selectedNode.name}</h1>
295
                                {data.controlledState && (
296
                                    <React.Fragment>
297
                                        <span style={{marginRight: '5px'}}>
298
                                            <AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} />
299
                                        </span>
300
                                    </React.Fragment>
301
                                )}
302
                                {(selectedNode as ResourceTreeNode).health && <AppUtils.HealthStatusIcon state={(selectedNode as ResourceTreeNode).health} />}
303
                                <button
304
                                    onClick={() => appContext.navigation.goto('.', {deploy: AppUtils.nodeKey(selectedNode)}, {replace: true})}
305
                                    style={{marginLeft: 'auto', marginRight: '5px'}}
306
                                    className='argo-button argo-button--base'>
307
                                    <i className='fa fa-sync-alt' /> <span className='show-for-large'>SYNC</span>
308
                                </button>
309
                                <button
310
                                    onClick={() => AppUtils.deletePopup(appContext, selectedNode, application)}
311
                                    style={{marginRight: '5px'}}
312
                                    className='argo-button argo-button--base'>
313
                                    <i className='fa fa-trash' /> <span className='show-for-large'>DELETE</span>
314
                                </button>
315
                                <DropDown
316
                                    isMenu={true}
317
                                    anchor={() => (
318
                                        <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
319
                                            <i className='fa fa-ellipsis-v' />
320
                                        </button>
321
                                    )}>
322
                                    {() => AppUtils.renderResourceActionMenu(selectedNode, application, appContext)}
323
                                </DropDown>
324
                            </div>
325
                            <Tabs
326
                                navTransparent={true}
327
                                tabs={getResourceTabs(
328
                                    selectedNode,
329
                                    data.liveState,
330
                                    data.podState,
331
                                    data.events,
332
                                    extensions,
333
                                    [
334
                                        {
335
                                            title: 'SUMMARY',
336
                                            icon: 'fa fa-file-alt',
337
                                            key: 'summary',
338
                                            content: (
339
                                                <ApplicationNodeInfo
340
                                                    application={application}
341
                                                    live={data.liveState}
342
                                                    controlled={data.controlledState}
343
                                                    node={selectedNode}
344
                                                    links={data.links}
345
                                                />
346
                                            )
347
                                        }
348
                                    ],
349
                                    data.execEnabled,
350
                                    data.execAllowed,
351
                                    data.logsAllowed
352
                                )}
353
                                selectedTabKey={props.tab}
354
                                onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
355
                            />
356
                        </React.Fragment>
357
                    )}
358
                </DataLoader>
359
            )}
360
            {isAppSelected && (
361
                <Tabs
362
                    navTransparent={true}
363
                    tabs={getApplicationTabs()}
364
                    selectedTabKey={tab}
365
                    onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
366
                />
367
            )}
368
        </div>
369
    );
370
};
371

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

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

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

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