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