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:&nbsp;
    61                                      <DropDownMenu
    62                                          anchor={() => (
    63                                              <button className='argo-button argo-button--base-o'>
    64                                                  {labelForSortMode[podPrefs.sortMode]}&nbsp;&nbsp;
    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                                                                      &nbsp;
   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  }