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:&nbsp;
    81                                      <DropDownMenu
    82                                          anchor={() => (
    83                                              <button className='argo-button argo-button--base-o'>
    84                                                  {labelForSortMode[podPrefs.sortMode]}&nbsp;&nbsp;
    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:&nbsp;
    94                                          <DropDownMenu
    95                                              anchor={() => (
    96                                                  <button className='argo-button argo-button--base-o'>
    97                                                      {labelForSortOrder[podPrefs.sortOrder || 'desc']}&nbsp;&nbsp;
    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                                                                      &nbsp;
   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  }