github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/resource-details/resource-details.tsx (about) 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 };