github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/application-pod-view/pod-view.tsx (about) 1 import {DataLoader, DropDown, DropDownMenu, MenuItem, Tooltip} from 'argo-ui'; 2 import * as PropTypes from 'prop-types'; 3 import * as React from 'react'; 4 import Moment from 'react-moment'; 5 6 import {AppContext} from '../../../shared/context'; 7 import {EmptyState} from '../../../shared/components'; 8 import {Application, ApplicationTree, HostResourceInfo, InfoItem, Node, Pod, ResourceName, ResourceNode, ResourceStatus} from '../../../shared/models'; 9 import {PodViewPreferences, services, ViewPreferences} from '../../../shared/services'; 10 11 import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; 12 import {ResourceIcon} from '../resource-icon'; 13 import {ResourceLabel} from '../resource-label'; 14 import {ComparisonStatusIcon, isYoungerThanXMinutes, HealthStatusIcon, nodeKey, PodHealthIcon, deletePodAction} from '../utils'; 15 16 import './pod-view.scss'; 17 import {PodTooltip} from './pod-tooltip'; 18 19 interface PodViewProps { 20 tree: ApplicationTree; 21 onItemClick: (fullName: string) => void; 22 app: Application; 23 nodeMenu?: (node: ResourceNode) => React.ReactNode; 24 quickStarts?: (node: ResourceNode) => React.ReactNode; 25 } 26 27 export type PodGroupType = 'topLevelResource' | 'parentResource' | 'node'; 28 29 export interface PodGroup extends Partial<ResourceNode> { 30 type: PodGroupType; 31 pods: Pod[]; 32 info?: InfoItem[]; 33 hostResourcesInfo?: HostResourceInfo[]; 34 resourceStatus?: Partial<ResourceStatus>; 35 renderMenu?: () => React.ReactNode; 36 renderQuickStarts?: () => React.ReactNode; 37 fullName?: string; 38 } 39 40 export class PodView extends React.Component<PodViewProps> { 41 private get appContext(): AppContext { 42 return this.context as AppContext; 43 } 44 45 public static contextTypes = { 46 apis: PropTypes.object 47 }; 48 49 public render() { 50 return ( 51 <DataLoader load={() => services.viewPreferences.getPreferences()}> 52 {prefs => { 53 const podPrefs = prefs.appDetails.podView || ({} as PodViewPreferences); 54 const groups = this.processTree(podPrefs.sortMode, this.props.tree.hosts || []) || []; 55 56 return ( 57 <React.Fragment> 58 <div className='pod-view__settings'> 59 <div className='pod-view__settings__section'> 60 GROUP BY: 61 <DropDownMenu 62 anchor={() => ( 63 <button className='argo-button argo-button--base-o'> 64 {labelForSortMode[podPrefs.sortMode]} 65 <i className='fa fa-chevron-circle-down' /> 66 </button> 67 )} 68 items={this.menuItemsFor(['node', 'parentResource', 'topLevelResource'], prefs)} 69 /> 70 </div> 71 {podPrefs.sortMode === 'node' && ( 72 <div className='pod-view__settings__section'> 73 <button 74 className={`argo-button argo-button--base${podPrefs.hideUnschedulable ? '-o' : ''}`} 75 style={{border: 'none', width: '170px'}} 76 onClick={() => 77 services.viewPreferences.updatePreferences({ 78 appDetails: {...prefs.appDetails, podView: {...podPrefs, hideUnschedulable: !podPrefs.hideUnschedulable}} 79 }) 80 }> 81 <i className={`fa fa-${podPrefs.hideUnschedulable ? 'eye-slash' : 'eye'}`} style={{width: '15px', marginRight: '5px'}} /> 82 UNSCHEDULABLE 83 </button> 84 </div> 85 )} 86 </div> 87 {groups.length > 0 ? ( 88 <div className='pod-view__nodes-container'> 89 {groups.map(group => { 90 if (group.type === 'node' && group.name === 'Unschedulable' && podPrefs.hideUnschedulable) { 91 return <React.Fragment />; 92 } 93 return ( 94 <div className={`pod-view__node white-box ${group.kind === 'node' && 'pod-view__node--large'}`} key={group.fullName || group.name}> 95 <div 96 className='pod-view__node__container--header' 97 onClick={() => this.props.onItemClick(group.fullName)} 98 style={group.kind === 'node' ? {} : {cursor: 'pointer'}}> 99 <div style={{display: 'flex', alignItems: 'center'}}> 100 <div style={{marginRight: '10px'}}> 101 <ResourceIcon kind={group.kind || 'Unknown'} /> 102 <br /> 103 {<div style={{textAlign: 'center'}}>{ResourceLabel({kind: group.kind})}</div>} 104 </div> 105 <div style={{lineHeight: '15px'}}> 106 <b style={{wordWrap: 'break-word'}}>{group.name || 'Unknown'}</b> 107 {group.resourceStatus && ( 108 <div> 109 {group.resourceStatus.health && <HealthStatusIcon state={group.resourceStatus.health} />} 110 111 {group.resourceStatus.status && ( 112 <ComparisonStatusIcon status={group.resourceStatus.status} resource={group.resourceStatus} /> 113 )} 114 </div> 115 )} 116 </div> 117 <div style={{marginLeft: 'auto'}}> 118 {group.renderMenu && ( 119 <DropDown 120 isMenu={true} 121 anchor={() => ( 122 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 123 <i className='fa fa-ellipsis-v' /> 124 </button> 125 )}> 126 {() => group.renderMenu()} 127 </DropDown> 128 )} 129 </div> 130 </div> 131 {group.type === 'node' ? ( 132 <div className='pod-view__node__info--large'> 133 {(group.info || []).map(item => ( 134 <div key={item.name}> 135 {item.name}: <div>{item.value}</div> 136 </div> 137 ))} 138 </div> 139 ) : ( 140 <div className='pod-view__node__info'> 141 {group.createdAt ? ( 142 <div> 143 <Moment fromNow={true} ago={true}> 144 {group.createdAt} 145 </Moment> 146 </div> 147 ) : null} 148 {group.info?.map(infoItem => ( 149 <div key={infoItem.name}>{infoItem.value}</div> 150 ))} 151 </div> 152 )} 153 </div> 154 <div className='pod-view__node__container'> 155 {(group.hostResourcesInfo || []).length > 0 && ( 156 <div className='pod-view__node__container pod-view__node__container--stats'> 157 {group.hostResourcesInfo.map(info => renderStats(info))} 158 </div> 159 )} 160 <div className='pod-view__node__pod-container pod-view__node__container'> 161 <div className='pod-view__node__pod-container__pods'> 162 {group.pods.map(pod => ( 163 <DropDownMenu 164 key={pod.uid} 165 anchor={() => ( 166 <Tooltip 167 content={<PodTooltip pod={pod} />} 168 popperOptions={{ 169 modifiers: { 170 preventOverflow: { 171 enabled: true 172 }, 173 hide: { 174 enabled: false 175 }, 176 flip: { 177 enabled: false 178 } 179 } 180 }} 181 key={pod.metadata.name}> 182 <div style={{position: 'relative'}}> 183 {isYoungerThanXMinutes(pod, 30) && ( 184 <i className='fas fa-circle pod-view__node__pod pod-view__node__pod__new-pod-icon' /> 185 )} 186 <div className={`pod-view__node__pod pod-view__node__pod--${pod.health.toLowerCase()}`}> 187 <PodHealthIcon state={{status: pod.health, message: ''}} /> 188 </div> 189 </div> 190 </Tooltip> 191 )} 192 items={[ 193 { 194 title: ( 195 <React.Fragment> 196 <i className='fa fa-info-circle' /> Info 197 </React.Fragment> 198 ), 199 action: () => this.props.onItemClick(pod.fullName) 200 }, 201 { 202 title: ( 203 <React.Fragment> 204 <i className='fa fa-align-left' /> Logs 205 </React.Fragment> 206 ), 207 action: () => { 208 this.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'logs'}, {replace: true}); 209 } 210 }, 211 { 212 title: ( 213 <React.Fragment> 214 <i className='fa fa-terminal' /> Exec 215 </React.Fragment> 216 ), 217 action: () => { 218 this.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'exec'}, {replace: true}); 219 } 220 }, 221 { 222 title: ( 223 <React.Fragment> 224 <i className='fa fa-times-circle' /> Delete 225 </React.Fragment> 226 ), 227 action: () => { 228 deletePodAction( 229 pod, 230 this.appContext, 231 this.props.app.metadata.name, 232 this.props.app.metadata.namespace 233 ); 234 } 235 } 236 ]} 237 /> 238 ))} 239 </div> 240 <div className='pod-view__node__label'>PODS</div> 241 {(podPrefs.sortMode === 'parentResource' || podPrefs.sortMode === 'topLevelResource') && ( 242 <div key={group.uid}>{group.renderQuickStarts()}</div> 243 )} 244 </div> 245 </div> 246 </div> 247 ); 248 })} 249 </div> 250 ) : ( 251 <EmptyState icon=' fa fa-th'> 252 <h4>Your application has no pod groups</h4> 253 <h5>Try switching to tree or list view</h5> 254 </EmptyState> 255 )} 256 </React.Fragment> 257 ); 258 }} 259 </DataLoader> 260 ); 261 } 262 263 private menuItemsFor(modes: PodGroupType[], prefs: ViewPreferences): MenuItem[] { 264 const podPrefs = prefs.appDetails.podView || ({} as PodViewPreferences); 265 return modes.map(mode => ({ 266 title: ( 267 <React.Fragment> 268 {podPrefs.sortMode === mode && <i className='fa fa-check' />} {labelForSortMode[mode]}{' '} 269 </React.Fragment> 270 ), 271 action: () => { 272 this.appContext.apis.navigation.goto('.', {podSortMode: mode}); 273 services.viewPreferences.updatePreferences({appDetails: {...prefs.appDetails, podView: {...podPrefs, sortMode: mode}}}); 274 } 275 })); 276 } 277 278 private processTree(sortMode: PodGroupType, initNodes: Node[]): PodGroup[] { 279 const tree = this.props.tree; 280 if (!tree) { 281 return []; 282 } 283 const groupRefs: {[key: string]: PodGroup} = {}; 284 const parentsFor: {[key: string]: PodGroup[]} = {}; 285 286 if (sortMode === 'node' && initNodes) { 287 initNodes.forEach(infraNode => { 288 const nodeName = infraNode.name; 289 groupRefs[nodeName] = { 290 ...infraNode, 291 type: 'node', 292 kind: 'node', 293 name: nodeName, 294 pods: [], 295 info: [ 296 {name: 'Kernel Version', value: infraNode.systemInfo.kernelVersion}, 297 {name: 'OS/Arch', value: `${infraNode.systemInfo.operatingSystem}/${infraNode.systemInfo.architecture}`} 298 ], 299 hostResourcesInfo: infraNode.resourcesInfo 300 }; 301 }); 302 } 303 304 const statusByKey = new Map<string, ResourceStatus>(); 305 this.props.app.status?.resources?.forEach(res => statusByKey.set(nodeKey(res), res)); 306 307 (tree.nodes || []).forEach((rnode: ResourceTreeNode) => { 308 // make sure each node has not null/undefined parentRefs field 309 rnode.parentRefs = rnode.parentRefs || []; 310 311 if (sortMode !== 'node') { 312 parentsFor[rnode.uid] = rnode.parentRefs as PodGroup[]; 313 const fullName = nodeKey(rnode); 314 const status = statusByKey.get(fullName); 315 316 if ((rnode.parentRefs || []).length === 0) { 317 rnode.root = rnode; 318 } 319 groupRefs[rnode.uid] = { 320 pods: [] as Pod[], 321 fullName, 322 ...groupRefs[rnode.uid], 323 ...rnode, 324 info: (rnode.info || []).filter(i => !i.name.includes('Resource.')), 325 createdAt: rnode.createdAt, 326 resourceStatus: {health: rnode.health, status: status ? status.status : null, requiresPruning: status && status.requiresPruning ? true : false}, 327 renderMenu: () => this.props.nodeMenu(rnode), 328 renderQuickStarts: () => this.props.quickStarts(rnode) 329 }; 330 } 331 }); 332 (tree.nodes || []).forEach((rnode: ResourceTreeNode) => { 333 if (rnode.kind !== 'Pod') { 334 return; 335 } 336 337 const p: Pod = { 338 ...rnode, 339 fullName: nodeKey(rnode), 340 metadata: {name: rnode.name}, 341 spec: {nodeName: 'Unknown'}, 342 health: rnode.health ? rnode.health.status : 'Unknown' 343 } as Pod; 344 345 // Get node name for Pod 346 rnode.info?.forEach(i => { 347 if (i.name === 'Node') { 348 p.spec.nodeName = i.value; 349 } 350 }); 351 352 if (sortMode === 'node') { 353 if (groupRefs[p.spec.nodeName]) { 354 const curNode = groupRefs[p.spec.nodeName]; 355 curNode.pods.push(p); 356 } else { 357 if (groupRefs.Unschedulable) { 358 groupRefs.Unschedulable.pods.push(p); 359 } else { 360 groupRefs.Unschedulable = { 361 type: 'node', 362 kind: 'node', 363 name: 'Unschedulable', 364 pods: [p], 365 info: [ 366 {name: 'Kernel Version', value: 'N/A'}, 367 {name: 'OS/Arch', value: 'N/A'} 368 ], 369 hostResourcesInfo: [] 370 }; 371 } 372 } 373 } else if (sortMode === 'parentResource') { 374 rnode.parentRefs.forEach(parentRef => { 375 if (!groupRefs[parentRef.uid]) { 376 groupRefs[parentRef.uid] = { 377 kind: parentRef.kind, 378 type: sortMode, 379 name: parentRef.name, 380 pods: [p] 381 }; 382 } else { 383 groupRefs[parentRef.uid].pods.push(p); 384 } 385 }); 386 } else if (sortMode === 'topLevelResource') { 387 let cur = rnode.uid; 388 let parents = parentsFor[rnode.uid]; 389 while ((parents || []).length > 0) { 390 cur = parents[0].uid; 391 parents = parentsFor[cur]; 392 } 393 if (groupRefs[cur]) { 394 groupRefs[cur].pods.push(p); 395 } 396 } 397 }); 398 399 Object.values(groupRefs).forEach(group => group.pods.sort((first, second) => nodeKey(first).localeCompare(nodeKey(second)))); 400 401 return Object.values(groupRefs) 402 .sort((a, b) => (a.name > b.name ? 1 : a.name === b.name ? 0 : -1)) // sort by name 403 .filter(i => (i.pods || []).length > 0); // filter out groups with no pods 404 } 405 } 406 407 const labelForSortMode = { 408 node: 'Node', 409 parentResource: 'Parent Resource', 410 topLevelResource: 'Top Level Resource' 411 }; 412 413 const sizes = ['Bytes', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; 414 function formatSize(bytes: number) { 415 if (!bytes) { 416 return '0 Bytes'; 417 } 418 const k = 1024; 419 const dm = 2; 420 const i = Math.floor(Math.log(bytes) / Math.log(k)); 421 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 422 } 423 424 function formatMetric(name: ResourceName, val: number) { 425 if (name === ResourceName.ResourceStorage || name === ResourceName.ResourceMemory) { 426 // divide by 1000 to convert "milli bytes" to bytes 427 return formatSize(val / 1000); 428 } 429 // cpu millicores 430 return (val || '0') + 'm'; 431 } 432 433 function renderStats(info: HostResourceInfo) { 434 const neighborsHeight = 100 * (info.requestedByNeighbors / info.capacity); 435 const appHeight = 100 * (info.requestedByApp / info.capacity); 436 return ( 437 <div className='pod-view__node__pod__stat' key={info.resourceName}> 438 <Tooltip 439 key={info.resourceName} 440 content={ 441 <React.Fragment> 442 <div>{info.resourceName.toUpperCase()}:</div> 443 <div className='pod-view__node__pod__stat-tooltip'> 444 <div>Requests:</div> 445 <div> 446 {' '} 447 <i className='pod-view__node__pod__stat-icon-app' /> {formatMetric(info.resourceName, info.requestedByApp)} (App) 448 </div> 449 <div> 450 {' '} 451 <i className='pod-view__node__pod__stat-icon-neighbors' /> {formatMetric(info.resourceName, info.requestedByNeighbors)} (Neighbors) 452 </div> 453 <div>Capacity: {formatMetric(info.resourceName, info.capacity)}</div> 454 </div> 455 </React.Fragment> 456 }> 457 <div className='pod-view__node__pod__stat__bar'> 458 <div className='pod-view__node__pod__stat__bar--fill pod-view__node__pod__stat__bar--neighbors' style={{height: `${neighborsHeight}%`}} /> 459 <div className='pod-view__node__pod__stat__bar--fill' style={{bottom: `${neighborsHeight}%`, height: `${appHeight}%`}} /> 460 </div> 461 </Tooltip> 462 <div className='pod-view__node__label'>{info.resourceName.slice(0, 3).toUpperCase()}</div> 463 </div> 464 ); 465 }