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