github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-node-info/application-node-info.tsx (about) 1 import {Checkbox, DataLoader, Tab, Tabs} from 'argo-ui'; 2 import classNames from 'classnames'; 3 import * as deepMerge from 'deepmerge'; 4 import * as React from 'react'; 5 6 import {YamlEditor, ClipboardText} from '../../../shared/components'; 7 import {DeepLinks} from '../../../shared/components/deep-links'; 8 import * as models from '../../../shared/models'; 9 import {services} from '../../../shared/services'; 10 import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; 11 import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff'; 12 import {ComparisonStatusIcon, formatCreationTimestamp, getPodReadinessGatesState, getPodStateReason, HealthStatusIcon} from '../utils'; 13 import './application-node-info.scss'; 14 import {ReadinessGatesNotPassedWarning} from './readiness-gates-not-passed-warning'; 15 import Moment from 'react-moment'; 16 17 const RenderContainerState = (props: {container: any}) => { 18 const state = (props.container.state?.waiting && 'waiting') || (props.container.state?.terminated && 'terminated') || (props.container.state?.running && 'running'); 19 const status = props.container.state.waiting?.reason || props.container.state.terminated?.reason || props.container.state.running?.reason; 20 const lastState = props.container.lastState?.terminated; 21 const msg = props.container.state.waiting?.message || props.container.state.terminated?.message || props.container.state.running?.message; 22 23 return ( 24 <div className='application-node-info__container'> 25 <div className='application-node-info__container--name'> 26 {props.container.state?.running ? ( 27 <span style={{marginRight: '4px'}}> 28 <i className='fa fa-check-circle' style={{color: 'rgb(24, 190, 148)'}} /> 29 </span> 30 ) : ( 31 (props.container.state.terminated && props.container.state.terminated?.exitCode !== 0) || 32 (lastState && lastState?.exitCode !== 0 && ( 33 <span style={{marginRight: '4px'}}> 34 <i className='fa fa-times-circle' style={{color: 'red'}} /> 35 </span> 36 )) 37 )} 38 {props.container.name} 39 </div> 40 <div> 41 {state && ( 42 <> 43 Container is <span className='application-node-info__container--highlight'>{state}</span> 44 {status && ' because of '} 45 </> 46 )} 47 <span title={msg || ''}> 48 {status && ( 49 <span 50 className={classNames('application-node-info__container--highlight', { 51 'application-node-info__container--hint': !!msg 52 })}> 53 {status} 54 </span> 55 )} 56 </span> 57 {'.'} 58 {(props.container.state.terminated?.exitCode === 0 || props.container.state.terminated?.exitCode) && ( 59 <> 60 {' '} 61 It exited with <span className='application-node-info__container--highlight'>exit code {props.container.state.terminated.exitCode}.</span> 62 </> 63 )}{' '} 64 It is <span className='application-node-info__container--highlight'>{props.container?.started ? 'started' : 'not started'}</span> 65 <span className='application-node-info__container--highlight'>{status === 'Completed' ? '.' : props.container?.ready ? ' and ready.' : ' and not ready.'}</span> 66 <br /> 67 {lastState && ( 68 <> 69 <> 70 The container last terminated{' '} 71 <span className='application-node-info__container--highlight'> 72 <Moment fromNow={true} ago={true}> 73 {lastState.finishedAt} 74 </Moment>{' '} 75 ago with exit code {lastState?.exitCode} 76 </span> 77 </> 78 {lastState?.reason && ' because of '} 79 <span title={props.container.lastState?.message || ''}> 80 {lastState?.reason && ( 81 <span 82 className={classNames('application-node-info__container--highlight', { 83 'application-node-info__container--hint': !!props.container.lastState?.message 84 })}> 85 {lastState?.reason} 86 </span> 87 )} 88 </span> 89 {'.'} 90 </> 91 )} 92 </div> 93 </div> 94 ); 95 }; 96 97 export const ApplicationNodeInfo = (props: { 98 application: models.Application; 99 node: models.ResourceNode; 100 live: models.State; 101 links: models.LinksResponse; 102 controlled: {summary: models.ResourceStatus; state: models.ResourceDiff}; 103 }) => { 104 const attributes: {title: string; value: any}[] = [ 105 {title: 'KIND', value: props.node.kind}, 106 {title: 'NAME', value: <ClipboardText text={props.node.name} />}, 107 {title: 'NAMESPACE', value: <ClipboardText text={props.node.namespace} />} 108 ]; 109 if (props.node.createdAt) { 110 attributes.push({ 111 title: 'CREATED AT', 112 value: formatCreationTimestamp(props.node.createdAt) 113 }); 114 } 115 if ((props.node.images || []).length) { 116 attributes.push({ 117 title: 'IMAGES', 118 value: ( 119 <div className='application-node-info__labels'> 120 {(props.node.images || []).sort().map(image => ( 121 <span className='application-node-info__label' key={image}> 122 {image} 123 </span> 124 ))} 125 </div> 126 ) 127 }); 128 } 129 130 if (props.live) { 131 if (props.node.kind === 'Pod') { 132 const {reason, message, netContainerStatuses} = getPodStateReason(props.live); 133 attributes.push({title: 'STATE', value: reason}); 134 if (message) { 135 attributes.push({title: 'STATE DETAILS', value: message}); 136 } 137 if (netContainerStatuses.length > 0) { 138 attributes.push({ 139 title: 'CONTAINER STATE', 140 value: ( 141 <div className='application-node-info__labels'> 142 {netContainerStatuses.map((container, i) => { 143 return <RenderContainerState key={i} container={container} />; 144 })} 145 </div> 146 ) 147 }); 148 } 149 } else if (props.node.kind === 'Service') { 150 attributes.push({title: 'TYPE', value: props.live.spec.type}); 151 let hostNames = ''; 152 const status = props.live.status; 153 if (status && status.loadBalancer && status.loadBalancer.ingress) { 154 hostNames = (status.loadBalancer.ingress || []).map((item: any) => item.hostname || item.ip).join(', '); 155 } 156 attributes.push({title: 'HOSTNAMES', value: hostNames}); 157 } else if (props.node.kind === 'ReplicaSet') { 158 attributes.push({title: 'REPLICAS', value: `${props.live.spec?.replicas || 0}/${props.live.status?.readyReplicas || 0}/${props.live.status?.replicas || 0}`}); 159 } 160 } 161 162 if (props.controlled) { 163 if (!props.controlled.summary.hook) { 164 attributes.push({ 165 title: 'STATUS', 166 value: ( 167 <span> 168 <ComparisonStatusIcon status={props.controlled.summary.status} resource={props.controlled.summary} label={true} /> 169 </span> 170 ) 171 } as any); 172 } 173 if (props.controlled.summary.health !== undefined) { 174 attributes.push({ 175 title: 'HEALTH', 176 value: ( 177 <span> 178 <HealthStatusIcon state={props.controlled.summary.health} /> {props.controlled.summary.health.status} 179 </span> 180 ) 181 } as any); 182 if (props.controlled.summary.health.message) { 183 attributes.push({title: 'HEALTH DETAILS', value: props.controlled.summary.health.message}); 184 } 185 } 186 } else if (props.node && (props.node as ResourceTreeNode).health) { 187 const treeNode = props.node as ResourceTreeNode; 188 if (treeNode && treeNode.health) { 189 attributes.push({ 190 title: 'HEALTH', 191 value: ( 192 <span> 193 <HealthStatusIcon state={treeNode.health} /> {treeNode.health.message || treeNode.health.status} 194 </span> 195 ) 196 } as any); 197 } 198 } 199 let showLiveState = true; 200 if (props.links) { 201 attributes.push({ 202 title: 'LINKS', 203 value: <DeepLinks links={props.links.items} /> 204 }); 205 } 206 207 const tabs: Tab[] = [ 208 { 209 key: 'manifest', 210 title: 'Live Manifest', 211 content: ( 212 <DataLoader load={() => services.viewPreferences.getPreferences()}> 213 {pref => { 214 const live = deepMerge(props.live, {}) as any; 215 if (Object.keys(live).length === 0) { 216 showLiveState = false; 217 } 218 219 if (live?.metadata?.managedFields && pref.appDetails.hideManagedFields) { 220 delete live.metadata.managedFields; 221 } 222 return ( 223 <React.Fragment> 224 {showLiveState ? ( 225 <React.Fragment> 226 <div className='application-node-info__checkboxes'> 227 <Checkbox 228 id='hideManagedFields' 229 checked={!!pref.appDetails.hideManagedFields} 230 onChange={() => 231 services.viewPreferences.updatePreferences({ 232 appDetails: { 233 ...pref.appDetails, 234 hideManagedFields: !pref.appDetails.hideManagedFields 235 } 236 }) 237 } 238 /> 239 <label htmlFor='hideManagedFields'>Hide Managed Fields</label> 240 <Checkbox 241 id='enableWordWrap' 242 checked={!!pref.appDetails.enableWordWrap} 243 onChange={() => 244 services.viewPreferences.updatePreferences({ 245 appDetails: { 246 ...pref.appDetails, 247 enableWordWrap: !pref.appDetails.enableWordWrap 248 } 249 }) 250 } 251 /> 252 <label htmlFor='enableWordWrap'>Enable Word Wrap</label> 253 </div> 254 <YamlEditor 255 input={live} 256 hideModeButtons={!live} 257 vScrollbar={live} 258 enableWordWrap={pref.appDetails.enableWordWrap} 259 onSave={(patch, patchType) => 260 services.applications.patchResource( 261 props.application.metadata.name, 262 props.application.metadata.namespace, 263 props.node, 264 patch, 265 patchType 266 ) 267 } 268 /> 269 </React.Fragment> 270 ) : ( 271 <div className='application-node-info__err_msg'> 272 Resource not found in cluster:{' '} 273 {`${props?.controlled?.state?.targetState?.apiVersion}/${props?.controlled?.state?.targetState?.kind}:${props.node.name}`} 274 <br /> 275 {props?.controlled?.state?.normalizedLiveState?.apiVersion && ( 276 <span> 277 Please update your resource specification to use the latest Kubernetes API resources supported by the target cluster. The 278 recommended syntax is{' '} 279 {`${props.controlled.state.normalizedLiveState.apiVersion}/${props?.controlled.state.normalizedLiveState?.kind}:${props.node.name}`} 280 </span> 281 )} 282 </div> 283 )} 284 </React.Fragment> 285 ); 286 }} 287 </DataLoader> 288 ) 289 } 290 ]; 291 if (props.controlled && !props.controlled.summary.hook) { 292 tabs.push({ 293 key: 'diff', 294 icon: 'fa fa-file-medical', 295 title: 'Diff', 296 content: <ApplicationResourcesDiff states={[props.controlled.state]} /> 297 }); 298 tabs.push({ 299 key: 'desiredManifest', 300 title: 'Desired Manifest', 301 content: ( 302 <DataLoader load={() => services.viewPreferences.getPreferences()}> 303 {pref => ( 304 <React.Fragment> 305 <div className='application-node-info__checkboxes'> 306 <Checkbox 307 id='enableWordWrap' 308 checked={!!pref.appDetails.enableWordWrap} 309 onChange={() => 310 services.viewPreferences.updatePreferences({ 311 appDetails: { 312 ...pref.appDetails, 313 enableWordWrap: !pref.appDetails.enableWordWrap 314 } 315 }) 316 } 317 /> 318 <label htmlFor='enableWordWrap'>Enable Word Wrap</label> 319 </div> 320 <YamlEditor enableWordWrap={pref.appDetails.enableWordWrap} input={props.controlled.state.targetState} hideModeButtons={true} /> 321 </React.Fragment> 322 )} 323 </DataLoader> 324 ) 325 }); 326 } 327 328 const readinessGatesState = React.useMemo(() => { 329 // If containers are not ready then readiness gate status is not important. 330 if (!props.live?.status?.containerStatuses?.length) { 331 return null; 332 } 333 if (props.live?.status?.containerStatuses?.some((containerStatus: {ready: boolean}) => !containerStatus.ready)) { 334 return null; 335 } 336 337 if (props.live && props.node?.kind === 'Pod') { 338 return getPodReadinessGatesState(props.live); 339 } 340 341 return null; 342 }, [props.live, props.node]); 343 344 return ( 345 <div> 346 {Boolean(readinessGatesState) && <ReadinessGatesNotPassedWarning readinessGatesState={readinessGatesState} />} 347 <div className='white-box'> 348 <div className='white-box__details'> 349 {attributes.map(attr => ( 350 <div className='row white-box__details-row' key={attr.title}> 351 <div className='columns small-3'>{attr.title}</div> 352 <div className='columns small-9'>{attr.value}</div> 353 </div> 354 ))} 355 </div> 356 </div> 357 358 <div className='application-node-info__manifest'> 359 <DataLoader load={() => services.viewPreferences.getPreferences()}> 360 {pref => ( 361 <Tabs 362 selectedTabKey={(tabs.length > 1 && pref.appDetails.resourceView) || 'manifest'} 363 tabs={tabs} 364 onTabSelected={selected => { 365 services.viewPreferences.updatePreferences({appDetails: {...pref.appDetails, resourceView: selected as any}}); 366 }} 367 /> 368 )} 369 </DataLoader> 370 </div> 371 </div> 372 ); 373 };