github.com/argoproj/argo-cd/v3@v3.2.1/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 {AppContext, Context} from '../../../shared/context'; 8 import {Application, ApplicationTree, Event, 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 appCxt: AppContext; 35 } 36 37 export const ResourceDetails = (props: ResourceDetailsProps) => { 38 const {selectedNode, updateApp, application, isAppSelected, tree} = {...props}; 39 const [activeContainer, setActiveContainer] = useState(); 40 const appContext = React.useContext(Context); 41 const tab = new URLSearchParams(appContext.history.location.search).get('tab'); 42 const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node')); 43 const selectedNodeKey = selectedNodeInfo.key; 44 const [pageNumber, setPageNumber] = React.useState(0); 45 const [collapsedSources, setCollapsedSources] = React.useState(new Array<boolean>()); // For Sources tab to save collapse states 46 const handleCollapse = (i: number, isCollapsed: boolean) => { 47 const v = collapsedSources.slice(); 48 v[i] = isCollapsed; 49 setCollapsedSources(v); 50 }; 51 52 const getResourceTabs = ( 53 node: ResourceNode, 54 state: State, 55 podState: State, 56 events: Event[], 57 extensionTabs: ResourceTabExtension[], 58 tabs: Tab[], 59 execEnabled: boolean, 60 execAllowed: boolean, 61 logsAllowed: boolean 62 ) => { 63 if (!node || node === undefined) { 64 return []; 65 } 66 if (state) { 67 const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0); 68 tabs.push({ 69 title: 'EVENTS', 70 icon: 'fa fa-calendar-alt', 71 badge: (numErrors > 0 && numErrors) || null, 72 key: 'events', 73 content: ( 74 <div className='application-resource-events'> 75 <EventsList events={events} /> 76 </div> 77 ) 78 }); 79 } 80 if (podState && podState.metadata && podState.spec) { 81 const containerGroups = [ 82 { 83 offset: 0, 84 title: 'CONTAINERS', 85 containers: podState.spec.containers || [] 86 } 87 ]; 88 if (podState.spec.initContainers?.length > 0) { 89 containerGroups.push({ 90 offset: (podState.spec.containers || []).length, 91 title: 'INIT CONTAINERS', 92 containers: podState.spec.initContainers || [] 93 }); 94 } 95 96 const onClickContainer = (group: any, i: number, activeTab: string) => { 97 setActiveContainer(group.offset + i); 98 SelectNode(selectedNodeKey, activeContainer, activeTab, appContext); 99 }; 100 101 if (logsAllowed) { 102 tabs = tabs.concat([ 103 { 104 key: 'logs', 105 icon: 'fa fa-align-left', 106 title: 'LOGS', 107 content: ( 108 <div className='application-details__tab-content-full-height'> 109 <PodsLogsViewer 110 podName={(state.kind === 'Pod' && state.metadata.name) || ''} 111 group={node.group} 112 kind={node.kind} 113 name={node.name} 114 namespace={podState.metadata.namespace} 115 applicationName={application.metadata.name} 116 applicationNamespace={application.metadata.namespace} 117 containerName={AppUtils.getContainerName(podState, activeContainer)} 118 containerGroups={containerGroups} 119 onClickContainer={onClickContainer} 120 /> 121 </div> 122 ) 123 } 124 ]); 125 } 126 if (selectedNode?.kind === 'Pod' && execEnabled && execAllowed) { 127 tabs = tabs.concat([ 128 { 129 key: 'exec', 130 icon: 'fa fa-terminal', 131 title: 'Terminal', 132 content: ( 133 <PodTerminalViewer 134 applicationName={application.metadata.name} 135 applicationNamespace={application.metadata.namespace} 136 projectName={application.spec.project} 137 podState={podState} 138 selectedNode={selectedNode} 139 containerName={AppUtils.getContainerName(podState, activeContainer)} 140 onClickContainer={onClickContainer} 141 /> 142 ) 143 } 144 ]); 145 } 146 } 147 if (state) { 148 extensionTabs.forEach((tabExtensions, i) => { 149 tabs.push({ 150 title: tabExtensions.title, 151 key: `extension-${i}`, 152 content: ( 153 <ErrorBoundary message={`Something went wrong with Extension for ${state?.kind || 'resource of unknown kind'}`}> 154 <tabExtensions.component tree={tree} resource={state} application={application} /> 155 </ErrorBoundary> 156 ), 157 icon: tabExtensions.icon 158 }); 159 }); 160 } 161 return tabs; 162 }; 163 164 const getApplicationTabs = () => { 165 const tabs: Tab[] = [ 166 { 167 title: 'SUMMARY', 168 key: 'summary', 169 content: <ApplicationSummary app={application} updateApp={(app, query: {validate?: boolean}) => updateApp(app, query)} /> 170 }, 171 { 172 title: application.spec.sources === undefined ? 'PARAMETERS' : 'SOURCES', 173 key: 'parameters', 174 content: ( 175 <ApplicationParameters 176 save={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)} 177 application={application} 178 pageNumber={pageNumber} 179 setPageNumber={setPageNumber} 180 collapsedSources={collapsedSources} 181 handleCollapse={handleCollapse} 182 appContext={props.appCxt} 183 /> 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 let childResources: models.ResourceNode[] = []; 272 if (selectedNode.kind === 'Pod') { 273 podState = liveState; 274 } else { 275 const childPod = AppUtils.findChildPod(selectedNode, tree); 276 if (childPod) { 277 podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null); 278 } 279 childResources = AppUtils.findChildResources(selectedNode, tree); 280 } 281 282 const settings = await services.authService.settings(); 283 const execEnabled = settings.execEnabled; 284 const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name); 285 const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name)); 286 const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null); 287 const resourceActionsMenuItems = await AppUtils.getResourceActionsMenuItems(selectedNode, application.metadata, appContext); 288 return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links, childResources, resourceActionsMenuItems}; 289 }}> 290 {data => ( 291 <React.Fragment> 292 <div className='resource-details__header'> 293 <div style={{display: 'flex', flexDirection: 'column', marginRight: '15px', alignItems: 'center', fontSize: '12px'}}> 294 <ResourceIcon kind={selectedNode.kind} /> 295 {ResourceLabel({kind: selectedNode.kind})} 296 </div> 297 <h1>{selectedNode.name}</h1> 298 {data.controlledState && ( 299 <span style={{marginRight: '5px'}}> 300 <AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} /> 301 </span> 302 )} 303 {(selectedNode as ResourceTreeNode).health && <AppUtils.HealthStatusIcon state={(selectedNode as ResourceTreeNode).health} />} 304 <button 305 onClick={() => appContext.navigation.goto('.', {deploy: AppUtils.nodeKey(selectedNode)}, {replace: true})} 306 style={{marginLeft: 'auto', marginRight: '5px'}} 307 className='argo-button argo-button--base'> 308 <i className='fa fa-sync-alt' /> <span className='show-for-large'>SYNC</span> 309 </button> 310 <button 311 onClick={() => AppUtils.deletePopup(appContext, selectedNode, application, !!data.controlledState, data.childResources)} 312 style={{marginRight: '5px'}} 313 className='argo-button argo-button--base'> 314 <i className='fa fa-trash' /> <span className='show-for-large'>DELETE</span> 315 </button> 316 {data.resourceActionsMenuItems?.length > 0 && ( 317 <DropDown 318 isMenu={true} 319 anchor={() => ( 320 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 321 <i className='fa fa-ellipsis-v' /> 322 </button> 323 )}> 324 {() => AppUtils.renderResourceActionMenu(data.resourceActionsMenuItems)} 325 </DropDown> 326 )} 327 </div> 328 <Tabs 329 navTransparent={true} 330 tabs={getResourceTabs( 331 selectedNode, 332 data.liveState, 333 data.podState, 334 data.events, 335 extensions, 336 [ 337 { 338 title: 'SUMMARY', 339 icon: 'fa fa-file-alt', 340 key: 'summary', 341 content: ( 342 <ApplicationNodeInfo 343 application={application} 344 live={data.liveState} 345 controlled={data.controlledState} 346 node={selectedNode} 347 links={data.links} 348 /> 349 ) 350 } 351 ], 352 data.execEnabled, 353 data.execAllowed, 354 data.logsAllowed 355 )} 356 selectedTabKey={props.tab} 357 onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})} 358 /> 359 </React.Fragment> 360 )} 361 </DataLoader> 362 )} 363 {isAppSelected && ( 364 <Tabs 365 navTransparent={true} 366 tabs={getApplicationTabs()} 367 selectedTabKey={tab} 368 onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})} 369 /> 370 )} 371 </div> 372 ); 373 };