argo-cd
370 строк · 18.6 Кб
1import {DataLoader, DropDown, Tab, Tabs} from 'argo-ui';
2import * as React from 'react';
3import {useState} from 'react';
4import {EventsList, YamlEditor} from '../../../shared/components';
5import * as models from '../../../shared/models';
6import {ErrorBoundary} from '../../../shared/components/error-boundary/error-boundary';
7import {Context} from '../../../shared/context';
8import {Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, ResourceNode, State, SyncStatuses} from '../../../shared/models';
9import {services} from '../../../shared/services';
10import {ResourceTabExtension} from '../../../shared/services/extensions-service';
11import {NodeInfo, SelectNode} from '../application-details/application-details';
12import {ApplicationNodeInfo} from '../application-node-info/application-node-info';
13import {ApplicationParameters} from '../application-parameters/application-parameters';
14import {ApplicationResourceEvents} from '../application-resource-events/application-resource-events';
15import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
16import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff';
17import {ApplicationSummary} from '../application-summary/application-summary';
18import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer';
19import {PodTerminalViewer} from '../pod-terminal-viewer/pod-terminal-viewer';
20import {ResourceIcon} from '../resource-icon';
21import {ResourceLabel} from '../resource-label';
22import * as AppUtils from '../utils';
23import './resource-details.scss';
24
25const jsonMergePatch = require('json-merge-patch');
26
27interface ResourceDetailsProps {
28selectedNode: ResourceNode;
29updateApp: (app: Application, query: {validate?: boolean}) => Promise<any>;
30application: Application;
31isAppSelected: boolean;
32tree: ApplicationTree;
33tab?: string;
34}
35
36export const ResourceDetails = (props: ResourceDetailsProps) => {
37const {selectedNode, updateApp, application, isAppSelected, tree} = {...props};
38const [activeContainer, setActiveContainer] = useState();
39const appContext = React.useContext(Context);
40const tab = new URLSearchParams(appContext.history.location.search).get('tab');
41const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node'));
42const selectedNodeKey = selectedNodeInfo.key;
43
44const getResourceTabs = (
45node: ResourceNode,
46state: State,
47podState: State,
48events: Event[],
49extensionTabs: ResourceTabExtension[],
50tabs: Tab[],
51execEnabled: boolean,
52execAllowed: boolean,
53logsAllowed: boolean
54) => {
55if (!node || node === undefined) {
56return [];
57}
58if (state) {
59const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0);
60tabs.push({
61title: 'EVENTS',
62icon: 'fa fa-calendar-alt',
63badge: (numErrors > 0 && numErrors) || null,
64key: 'events',
65content: (
66<div className='application-resource-events'>
67<EventsList events={events} />
68</div>
69)
70});
71}
72if (podState && podState.metadata && podState.spec) {
73const containerGroups = [
74{
75offset: 0,
76title: 'CONTAINERS',
77containers: podState.spec.containers || []
78}
79];
80if (podState.spec.initContainers?.length > 0) {
81containerGroups.push({
82offset: (podState.spec.containers || []).length,
83title: 'INIT CONTAINERS',
84containers: podState.spec.initContainers || []
85});
86}
87
88const onClickContainer = (group: any, i: number, activeTab: string) => {
89setActiveContainer(group.offset + i);
90SelectNode(selectedNodeKey, activeContainer, activeTab, appContext);
91};
92
93if (logsAllowed) {
94tabs = tabs.concat([
95{
96key: 'logs',
97icon: 'fa fa-align-left',
98title: 'LOGS',
99content: (
100<div className='application-details__tab-content-full-height'>
101<PodsLogsViewer
102podName={(state.kind === 'Pod' && state.metadata.name) || ''}
103group={node.group}
104kind={node.kind}
105name={node.name}
106namespace={podState.metadata.namespace}
107applicationName={application.metadata.name}
108applicationNamespace={application.metadata.namespace}
109containerName={AppUtils.getContainerName(podState, activeContainer)}
110containerGroups={containerGroups}
111onClickContainer={onClickContainer}
112/>
113</div>
114)
115}
116]);
117}
118if (selectedNode.kind === 'Pod' && execEnabled && execAllowed) {
119tabs = tabs.concat([
120{
121key: 'exec',
122icon: 'fa fa-terminal',
123title: 'Terminal',
124content: (
125<PodTerminalViewer
126applicationName={application.metadata.name}
127applicationNamespace={application.metadata.namespace}
128projectName={application.spec.project}
129podState={podState}
130selectedNode={selectedNode}
131containerName={AppUtils.getContainerName(podState, activeContainer)}
132onClickContainer={onClickContainer}
133/>
134)
135}
136]);
137}
138}
139if (state) {
140extensionTabs.forEach((tabExtensions, i) => {
141tabs.push({
142title: tabExtensions.title,
143key: `extension-${i}`,
144content: (
145<ErrorBoundary message={`Something went wrong with Extension for ${state.kind}`}>
146<tabExtensions.component tree={tree} resource={state} application={application} />
147</ErrorBoundary>
148),
149icon: tabExtensions.icon
150});
151});
152}
153return tabs;
154};
155
156const getApplicationTabs = () => {
157const tabs: Tab[] = [
158{
159title: 'SUMMARY',
160key: 'summary',
161content: <ApplicationSummary app={application} updateApp={(app, query: {validate?: boolean}) => updateApp(app, query)} />
162},
163{
164title: 'PARAMETERS',
165key: 'parameters',
166content: (
167<DataLoader
168key='appDetails'
169input={application}
170load={app =>
171services.repos.appDetails(AppUtils.getAppDefaultSource(app), app.metadata.name, app.spec.project).catch(() => ({
172type: 'Directory' as AppSourceType,
173path: AppUtils.getAppDefaultSource(app).path
174}))
175}>
176{(details: RepoAppDetails) => (
177<ApplicationParameters
178save={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
179application={application}
180details={details}
181/>
182)}
183</DataLoader>
184)
185},
186{
187title: 'MANIFEST',
188key: 'manifest',
189content: (
190<YamlEditor
191minHeight={800}
192input={application.spec}
193onSave={async patch => {
194const spec = JSON.parse(JSON.stringify(application.spec));
195return services.applications.updateSpec(application.metadata.name, application.metadata.namespace, jsonMergePatch.apply(spec, JSON.parse(patch)));
196}}
197/>
198)
199}
200];
201
202if (application.status.sync.status !== SyncStatuses.Synced) {
203tabs.push({
204icon: 'fa fa-file-medical',
205title: 'DIFF',
206key: 'diff',
207content: (
208<DataLoader
209key='diff'
210load={async () =>
211await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
212fields: ['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
221tabs.push({
222title: 'EVENTS',
223key: 'event',
224content: <ApplicationResourceEvents applicationName={application.metadata.name} applicationNamespace={application.metadata.namespace} />
225});
226
227const extensionTabs = services.extensions.getResourceTabs('argoproj.io', 'Application').map((ext, i) => ({
228title: ext.title,
229key: `extension-${i}`,
230content: <ext.component resource={application} tree={tree} application={application} />,
231icon: ext.icon
232}));
233
234return tabs.concat(extensionTabs);
235};
236
237const extensions = selectedNode?.kind ? services.extensions.getResourceTabs(selectedNode?.group || '', selectedNode?.kind) : [];
238
239return (
240<div style={{width: '100%', height: '100%'}}>
241{selectedNode && (
242<DataLoader
243noLoaderOnInputChange={true}
244input={selectedNode.resourceVersion}
245load={async () => {
246const managedResources = await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
247id: {
248name: selectedNode.name,
249namespace: selectedNode.namespace,
250kind: selectedNode.kind,
251group: selectedNode.group
252}
253});
254const controlled = managedResources.find(item => AppUtils.isSameNode(selectedNode, item));
255const summary = application.status.resources.find(item => AppUtils.isSameNode(selectedNode, item));
256const controlledState = (controlled && summary && {summary, state: controlled}) || null;
257const resQuery = {...selectedNode};
258if (controlled && controlled.targetState) {
259resQuery.version = AppUtils.parseApiVersion(controlled.targetState.apiVersion).version;
260}
261const liveState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, resQuery).catch(() => null);
262const events =
263(liveState &&
264(await services.applications.resourceEvents(application.metadata.name, application.metadata.namespace, {
265name: liveState.metadata.name,
266namespace: liveState.metadata.namespace,
267uid: liveState.metadata.uid
268}))) ||
269[];
270let podState: State;
271if (selectedNode.kind === 'Pod') {
272podState = liveState;
273} else {
274const childPod = AppUtils.findChildPod(selectedNode, tree);
275if (childPod) {
276podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null);
277}
278}
279
280const settings = await services.authService.settings();
281const execEnabled = settings.execEnabled;
282const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
283const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
284const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
285return {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
304onClick={() => appContext.navigation.goto('.', {deploy: AppUtils.nodeKey(selectedNode)}, {replace: true})}
305style={{marginLeft: 'auto', marginRight: '5px'}}
306className='argo-button argo-button--base'>
307<i className='fa fa-sync-alt' /> <span className='show-for-large'>SYNC</span>
308</button>
309<button
310onClick={() => AppUtils.deletePopup(appContext, selectedNode, application)}
311style={{marginRight: '5px'}}
312className='argo-button argo-button--base'>
313<i className='fa fa-trash' /> <span className='show-for-large'>DELETE</span>
314</button>
315<DropDown
316isMenu={true}
317anchor={() => (
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
326navTransparent={true}
327tabs={getResourceTabs(
328selectedNode,
329data.liveState,
330data.podState,
331data.events,
332extensions,
333[
334{
335title: 'SUMMARY',
336icon: 'fa fa-file-alt',
337key: 'summary',
338content: (
339<ApplicationNodeInfo
340application={application}
341live={data.liveState}
342controlled={data.controlledState}
343node={selectedNode}
344links={data.links}
345/>
346)
347}
348],
349data.execEnabled,
350data.execAllowed,
351data.logsAllowed
352)}
353selectedTabKey={props.tab}
354onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
355/>
356</React.Fragment>
357)}
358</DataLoader>
359)}
360{isAppSelected && (
361<Tabs
362navTransparent={true}
363tabs={getApplicationTabs()}
364selectedTabKey={tab}
365onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
366/>
367)}
368</div>
369);
370};
371