github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/utils.tsx (about)

     1  import {models, DataLoader, FormField, MenuItem, NotificationType, Tooltip} from 'argo-ui';
     2  import {ActionButton} from 'argo-ui/v2';
     3  import * as classNames from 'classnames';
     4  import * as React from 'react';
     5  import * as ReactForm from 'react-form';
     6  import {FormApi, Text} from 'react-form';
     7  import * as moment from 'moment';
     8  import {BehaviorSubject, combineLatest, concat, from, fromEvent, Observable, Observer, Subscription} from 'rxjs';
     9  import {debounceTime, map} from 'rxjs/operators';
    10  import {AppContext, Context, ContextApis} from '../../shared/context';
    11  import {ResourceTreeNode} from './application-resource-tree/application-resource-tree';
    12  
    13  import {CheckboxField, COLORS, ErrorNotification, Revision} from '../../shared/components';
    14  import * as appModels from '../../shared/models';
    15  import {services} from '../../shared/services';
    16  
    17  require('./utils.scss');
    18  
    19  export interface NodeId {
    20      kind: string;
    21      namespace: string;
    22      name: string;
    23      group: string;
    24      createdAt?: models.Time;
    25  }
    26  
    27  type ActionMenuItem = MenuItem & {disabled?: boolean; tooltip?: string};
    28  
    29  export function nodeKey(node: NodeId) {
    30      return [node.group, node.kind, node.namespace, node.name].join('/');
    31  }
    32  
    33  export function createdOrNodeKey(node: NodeId) {
    34      return node?.createdAt || nodeKey(node);
    35  }
    36  
    37  export function isSameNode(first: NodeId, second: NodeId) {
    38      return nodeKey(first) === nodeKey(second);
    39  }
    40  
    41  export function helpTip(text: string) {
    42      return (
    43          <Tooltip content={text}>
    44              <span style={{fontSize: 'smaller'}}>
    45                  {' '}
    46                  <i className='fas fa-info-circle' />
    47              </span>
    48          </Tooltip>
    49      );
    50  }
    51  export async function deleteApplication(appName: string, appNamespace: string, apis: ContextApis): Promise<boolean> {
    52      let confirmed = false;
    53      const propagationPolicies: {name: string; message: string}[] = [
    54          {
    55              name: 'Foreground',
    56              message: `Cascade delete the application's resources using foreground propagation policy`
    57          },
    58          {
    59              name: 'Background',
    60              message: `Cascade delete the application's resources using background propagation policy`
    61          },
    62          {
    63              name: 'Non-cascading',
    64              message: `Only delete the application, but do not cascade delete its resources`
    65          }
    66      ];
    67      await apis.popup.prompt(
    68          'Delete application',
    69          api => (
    70              <div>
    71                  <p>
    72                      Are you sure you want to delete the application <kbd>{appName}</kbd>?
    73                  </p>
    74                  <div className='argo-form-row'>
    75                      <FormField
    76                          label={`Please type '${appName}' to confirm the deletion of the resource`}
    77                          formApi={api}
    78                          field='applicationName'
    79                          qeId='name-field-delete-confirmation'
    80                          component={Text}
    81                      />
    82                  </div>
    83                  <p>Select propagation policy for application deletion</p>
    84                  <div className='propagation-policy-list'>
    85                      {propagationPolicies.map(policy => {
    86                          return (
    87                              <FormField
    88                                  formApi={api}
    89                                  key={policy.name}
    90                                  field='propagationPolicy'
    91                                  component={PropagationPolicyOption}
    92                                  componentProps={{
    93                                      policy: policy.name,
    94                                      message: policy.message
    95                                  }}
    96                              />
    97                          );
    98                      })}
    99                  </div>
   100              </div>
   101          ),
   102          {
   103              validate: vals => ({
   104                  applicationName: vals.applicationName !== appName && 'Enter the application name to confirm the deletion'
   105              }),
   106              submit: async (vals, _, close) => {
   107                  try {
   108                      await services.applications.delete(appName, appNamespace, vals.propagationPolicy);
   109                      confirmed = true;
   110                      close();
   111                  } catch (e) {
   112                      apis.notifications.show({
   113                          content: <ErrorNotification title='Unable to delete application' e={e} />,
   114                          type: NotificationType.Error
   115                      });
   116                  }
   117              }
   118          },
   119          {name: 'argo-icon-warning', color: 'warning'},
   120          'yellow',
   121          {propagationPolicy: 'foreground'}
   122      );
   123      return confirmed;
   124  }
   125  
   126  export async function confirmSyncingAppOfApps(apps: appModels.Application[], apis: ContextApis, form: FormApi): Promise<boolean> {
   127      let confirmed = false;
   128      const appNames: string[] = apps.map(app => app.metadata.name);
   129      const appNameList = appNames.join(', ');
   130      await apis.popup.prompt(
   131          'Warning: Synchronize App of Multiple Apps using replace?',
   132          api => (
   133              <div>
   134                  <p>
   135                      Are you sure you want to sync the application '{appNameList}' which contain(s) multiple apps with 'replace' option? This action will delete and recreate all
   136                      apps linked to '{appNameList}'.
   137                  </p>
   138                  <div className='argo-form-row'>
   139                      <FormField
   140                          label={`Please type '${appNameList}' to confirm the Syncing of the resource`}
   141                          formApi={api}
   142                          field='applicationName'
   143                          qeId='name-field-delete-confirmation'
   144                          component={Text}
   145                      />
   146                  </div>
   147              </div>
   148          ),
   149          {
   150              validate: vals => ({
   151                  applicationName: vals.applicationName !== appNameList && 'Enter the application name(s) to confirm syncing'
   152              }),
   153              submit: async (_vals, _, close) => {
   154                  try {
   155                      await form.submitForm(null);
   156                      confirmed = true;
   157                      close();
   158                  } catch (e) {
   159                      apis.notifications.show({
   160                          content: <ErrorNotification title='Unable to sync application' e={e} />,
   161                          type: NotificationType.Error
   162                      });
   163                  }
   164              }
   165          },
   166          {name: 'argo-icon-warning', color: 'warning'},
   167          'yellow'
   168      );
   169      return confirmed;
   170  }
   171  
   172  const PropagationPolicyOption = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi; policy: string; message: string}) => {
   173      const {
   174          fieldApi: {setValue}
   175      } = props;
   176      return (
   177          <div className='propagation-policy-option'>
   178              <input
   179                  className='radio-button'
   180                  key={props.policy}
   181                  type='radio'
   182                  name='propagation-policy'
   183                  value={props.policy}
   184                  id={props.policy}
   185                  defaultChecked={props.policy === 'Foreground'}
   186                  onChange={() => setValue(props.policy.toLowerCase())}
   187              />
   188              <label htmlFor={props.policy}>
   189                  {props.policy} {helpTip(props.message)}
   190              </label>
   191          </div>
   192      );
   193  });
   194  
   195  export const OperationPhaseIcon = ({app}: {app: appModels.Application}) => {
   196      const operationState = getAppOperationState(app);
   197      if (operationState === undefined) {
   198          return <React.Fragment />;
   199      }
   200      let className = '';
   201      let color = '';
   202      switch (operationState.phase) {
   203          case appModels.OperationPhases.Succeeded:
   204              className = 'fa fa-check-circle';
   205              color = COLORS.operation.success;
   206              break;
   207          case appModels.OperationPhases.Error:
   208              className = 'fa fa-times-circle';
   209              color = COLORS.operation.error;
   210              break;
   211          case appModels.OperationPhases.Failed:
   212              className = 'fa fa-times-circle';
   213              color = COLORS.operation.failed;
   214              break;
   215          default:
   216              className = 'fa fa-circle-notch fa-spin';
   217              color = COLORS.operation.running;
   218              break;
   219      }
   220      return <i title={getOperationStateTitle(app)} qe-id='utils-operations-status-title' className={className} style={{color}} />;
   221  };
   222  
   223  export const ComparisonStatusIcon = ({
   224      status,
   225      resource,
   226      label,
   227      noSpin
   228  }: {
   229      status: appModels.SyncStatusCode;
   230      resource?: {requiresPruning?: boolean};
   231      label?: boolean;
   232      noSpin?: boolean;
   233  }) => {
   234      let className = 'fas fa-question-circle';
   235      let color = COLORS.sync.unknown;
   236      let title: string = 'Unknown';
   237  
   238      switch (status) {
   239          case appModels.SyncStatuses.Synced:
   240              className = 'fa fa-check-circle';
   241              color = COLORS.sync.synced;
   242              title = 'Synced';
   243              break;
   244          case appModels.SyncStatuses.OutOfSync:
   245              const requiresPruning = resource && resource.requiresPruning;
   246              className = requiresPruning ? 'fa fa-trash' : 'fa fa-arrow-alt-circle-up';
   247              title = 'OutOfSync';
   248              if (requiresPruning) {
   249                  title = `${title} (This resource is not present in the application's source. It will be deleted from Kubernetes if the prune option is enabled during sync.)`;
   250              }
   251              color = COLORS.sync.out_of_sync;
   252              break;
   253          case appModels.SyncStatuses.Unknown:
   254              className = `fa fa-circle-notch ${noSpin ? '' : 'fa-spin'}`;
   255              break;
   256      }
   257      return (
   258          <React.Fragment>
   259              <i qe-id='utils-sync-status-title' title={title} className={className} style={{color}} /> {label && title}
   260          </React.Fragment>
   261      );
   262  };
   263  
   264  export function showDeploy(resource: string, revision: string, apis: ContextApis) {
   265      apis.navigation.goto('.', {deploy: resource, revision}, {replace: true});
   266  }
   267  
   268  export function findChildPod(node: appModels.ResourceNode, tree: appModels.ApplicationTree): appModels.ResourceNode {
   269      const key = nodeKey(node);
   270  
   271      const allNodes = tree.nodes.concat(tree.orphanedNodes || []);
   272      const nodeByKey = new Map<string, appModels.ResourceNode>();
   273      allNodes.forEach(item => nodeByKey.set(nodeKey(item), item));
   274  
   275      const pods = tree.nodes.concat(tree.orphanedNodes || []).filter(item => item.kind === 'Pod');
   276      return pods.find(pod => {
   277          const items: Array<appModels.ResourceNode> = [pod];
   278          while (items.length > 0) {
   279              const next = items.pop();
   280              const parentKeys = (next.parentRefs || []).map(nodeKey);
   281              if (parentKeys.includes(key)) {
   282                  return true;
   283              }
   284              parentKeys.forEach(item => {
   285                  const parent = nodeByKey.get(item);
   286                  if (parent) {
   287                      items.push(parent);
   288                  }
   289              });
   290          }
   291  
   292          return false;
   293      });
   294  }
   295  
   296  export const deletePodAction = async (pod: appModels.Pod, appContext: AppContext, appName: string, appNamespace: string) => {
   297      appContext.apis.popup.prompt(
   298          'Delete pod',
   299          () => (
   300              <div>
   301                  <p>
   302                      Are you sure you want to delete Pod <kbd>{pod.name}</kbd>?
   303                  </p>
   304                  <div className='argo-form-row' style={{paddingLeft: '30px'}}>
   305                      <CheckboxField id='force-delete-checkbox' field='force'>
   306                          <label htmlFor='force-delete-checkbox'>Force delete</label>
   307                      </CheckboxField>
   308                  </div>
   309              </div>
   310          ),
   311          {
   312              submit: async (vals, _, close) => {
   313                  try {
   314                      await services.applications.deleteResource(appName, appNamespace, pod, !!vals.force, false);
   315                      close();
   316                  } catch (e) {
   317                      appContext.apis.notifications.show({
   318                          content: <ErrorNotification title='Unable to delete resource' e={e} />,
   319                          type: NotificationType.Error
   320                      });
   321                  }
   322              }
   323          }
   324      );
   325  };
   326  
   327  export const deletePopup = async (ctx: ContextApis, resource: ResourceTreeNode, application: appModels.Application, appChanged?: BehaviorSubject<appModels.Application>) => {
   328      function isTopLevelResource(res: ResourceTreeNode, app: appModels.Application): boolean {
   329          const uniqRes = `/${res.namespace}/${res.group}/${res.kind}/${res.name}`;
   330          return app.status.resources.some(resStatus => `/${resStatus.namespace}/${resStatus.group}/${resStatus.kind}/${resStatus.name}` === uniqRes);
   331      }
   332  
   333      const isManaged = isTopLevelResource(resource, application);
   334      const deleteOptions = {
   335          option: 'foreground'
   336      };
   337      function handleStateChange(option: string) {
   338          deleteOptions.option = option;
   339      }
   340      return ctx.popup.prompt(
   341          'Delete resource',
   342          api => (
   343              <div>
   344                  <p>
   345                      Are you sure you want to delete {resource.kind} <kbd>{resource.name}</kbd>?
   346                  </p>
   347                  {isManaged ? (
   348                      <div className='argo-form-row'>
   349                          <FormField label={`Please type '${resource.name}' to confirm the deletion of the resource`} formApi={api} field='resourceName' component={Text} />
   350                      </div>
   351                  ) : (
   352                      ''
   353                  )}
   354                  <div className='argo-form-row'>
   355                      <input
   356                          type='radio'
   357                          name='deleteOptions'
   358                          value='foreground'
   359                          onChange={() => handleStateChange('foreground')}
   360                          defaultChecked={true}
   361                          style={{marginRight: '5px'}}
   362                          id='foreground-delete-radio'
   363                      />
   364                      <label htmlFor='foreground-delete-radio' style={{paddingRight: '30px'}}>
   365                          Foreground Delete {helpTip('Deletes the resource and dependent resources using the cascading policy in the foreground')}
   366                      </label>
   367                      <input type='radio' name='deleteOptions' value='force' onChange={() => handleStateChange('force')} style={{marginRight: '5px'}} id='force-delete-radio' />
   368                      <label htmlFor='force-delete-radio' style={{paddingRight: '30px'}}>
   369                          Background Delete {helpTip('Performs a forceful "background cascading deletion" of the resource and its dependent resources')}
   370                      </label>
   371                      <input type='radio' name='deleteOptions' value='orphan' onChange={() => handleStateChange('orphan')} style={{marginRight: '5px'}} id='cascade-delete-radio' />
   372                      <label htmlFor='cascade-delete-radio'>Non-cascading (Orphan) Delete {helpTip('Deletes the resource and orphans the dependent resources')}</label>
   373                  </div>
   374              </div>
   375          ),
   376          {
   377              validate: vals =>
   378                  isManaged && {
   379                      resourceName: vals.resourceName !== resource.name && 'Enter the resource name to confirm the deletion'
   380                  },
   381              submit: async (vals, _, close) => {
   382                  const force = deleteOptions.option === 'force';
   383                  const orphan = deleteOptions.option === 'orphan';
   384                  try {
   385                      await services.applications.deleteResource(application.metadata.name, application.metadata.namespace, resource, !!force, !!orphan);
   386                      if (appChanged) {
   387                          appChanged.next(await services.applications.get(application.metadata.name, application.metadata.namespace));
   388                      }
   389                      close();
   390                  } catch (e) {
   391                      ctx.notifications.show({
   392                          content: <ErrorNotification title='Unable to delete resource' e={e} />,
   393                          type: NotificationType.Error
   394                      });
   395                  }
   396              }
   397          },
   398          {name: 'argo-icon-warning', color: 'warning'},
   399          'yellow'
   400      );
   401  };
   402  
   403  function getResourceActionsMenuItems(resource: ResourceTreeNode, metadata: models.ObjectMeta, apis: ContextApis): Promise<ActionMenuItem[]> {
   404      return services.applications
   405          .getResourceActions(metadata.name, metadata.namespace, resource)
   406          .then(actions => {
   407              return actions.map(
   408                  action =>
   409                      ({
   410                          title: action.displayName ?? action.name,
   411                          disabled: !!action.disabled,
   412                          iconClassName: action.iconClass,
   413                          action: async () => {
   414                              try {
   415                                  const confirmed = await apis.popup.confirm(`Execute '${action.name}' action?`, `Are you sure you want to execute '${action.name}' action?`);
   416                                  if (confirmed) {
   417                                      await services.applications.runResourceAction(metadata.name, metadata.namespace, resource, action.name);
   418                                  }
   419                              } catch (e) {
   420                                  apis.notifications.show({
   421                                      content: <ErrorNotification title='Unable to execute resource action' e={e} />,
   422                                      type: NotificationType.Error
   423                                  });
   424                              }
   425                          }
   426                      } as MenuItem)
   427              );
   428          })
   429          .catch(() => [] as MenuItem[]);
   430  }
   431  
   432  function getActionItems(
   433      resource: ResourceTreeNode,
   434      application: appModels.Application,
   435      tree: appModels.ApplicationTree,
   436      apis: ContextApis,
   437      appChanged: BehaviorSubject<appModels.Application>,
   438      isQuickStart: boolean
   439  ): Observable<ActionMenuItem[]> {
   440      const isRoot = resource.root && nodeKey(resource.root) === nodeKey(resource);
   441      const items: MenuItem[] = [
   442          ...((isRoot && [
   443              {
   444                  title: 'Sync',
   445                  iconClassName: 'fa fa-fw fa-sync',
   446                  action: () => showDeploy(nodeKey(resource), null, apis)
   447              }
   448          ]) ||
   449              []),
   450          {
   451              title: 'Delete',
   452              iconClassName: 'fa fa-fw fa-times-circle',
   453              action: async () => {
   454                  return deletePopup(apis, resource, application, appChanged);
   455              }
   456          }
   457      ];
   458      if (!isQuickStart) {
   459          items.unshift({
   460              title: 'Details',
   461              iconClassName: 'fa fa-fw fa-info-circle',
   462              action: () => apis.navigation.goto('.', {node: nodeKey(resource)})
   463          });
   464      }
   465  
   466      if (findChildPod(resource, tree)) {
   467          items.push({
   468              title: 'Logs',
   469              iconClassName: 'fa fa-fw fa-align-left',
   470              action: () => apis.navigation.goto('.', {node: nodeKey(resource), tab: 'logs'}, {replace: true})
   471          });
   472      }
   473  
   474      if (isQuickStart) {
   475          return from([items]);
   476      }
   477  
   478      const execAction = services.authService
   479          .settings()
   480          .then(async settings => {
   481              const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
   482              if (resource.kind === 'Pod' && execAllowed) {
   483                  return [
   484                      {
   485                          title: 'Exec',
   486                          iconClassName: 'fa fa-fw fa-terminal',
   487                          action: async () => apis.navigation.goto('.', {node: nodeKey(resource), tab: 'exec'}, {replace: true})
   488                      } as MenuItem
   489                  ];
   490              }
   491              return [] as MenuItem[];
   492          })
   493          .catch(() => [] as MenuItem[]);
   494  
   495      const resourceActions = getResourceActionsMenuItems(resource, application.metadata, apis);
   496  
   497      const links = services.applications
   498          .getResourceLinks(application.metadata.name, application.metadata.namespace, resource)
   499          .then(data => {
   500              return (data.items || []).map(
   501                  link =>
   502                      ({
   503                          title: link.title,
   504                          iconClassName: `fa fa-fw ${link.iconClass ? link.iconClass : 'fa-external-link'}`,
   505                          action: () => window.open(link.url, '_blank'),
   506                          tooltip: link.description
   507                      } as MenuItem)
   508              );
   509          })
   510          .catch(() => [] as MenuItem[]);
   511  
   512      return combineLatest(
   513          from([items]), // this resolves immediately
   514          concat([[] as MenuItem[]], resourceActions), // this resolves at first to [] and then whatever the API returns
   515          concat([[] as MenuItem[]], execAction), // this resolves at first to [] and then whatever the API returns
   516          concat([[] as MenuItem[]], links) // this resolves at first to [] and then whatever the API returns
   517      ).pipe(map(res => ([] as MenuItem[]).concat(...res)));
   518  }
   519  
   520  export function renderResourceMenu(
   521      resource: ResourceTreeNode,
   522      application: appModels.Application,
   523      tree: appModels.ApplicationTree,
   524      apis: ContextApis,
   525      appChanged: BehaviorSubject<appModels.Application>,
   526      getApplicationActionMenu: () => any
   527  ): React.ReactNode {
   528      let menuItems: Observable<ActionMenuItem[]>;
   529  
   530      if (isAppNode(resource) && resource.name === application.metadata.name) {
   531          menuItems = from([getApplicationActionMenu()]);
   532      } else {
   533          menuItems = getActionItems(resource, application, tree, apis, appChanged, false);
   534      }
   535      return (
   536          <DataLoader load={() => menuItems}>
   537              {items => (
   538                  <ul>
   539                      {items.map((item, i) => (
   540                          <li
   541                              className={classNames('application-details__action-menu', {disabled: item.disabled})}
   542                              key={i}
   543                              onClick={e => {
   544                                  e.stopPropagation();
   545                                  if (!item.disabled) {
   546                                      item.action();
   547                                      document.body.click();
   548                                  }
   549                              }}>
   550                              {item.tooltip ? (
   551                                  <Tooltip content={item.tooltip || ''}>
   552                                      <div>
   553                                          {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   554                                      </div>
   555                                  </Tooltip>
   556                              ) : (
   557                                  <>
   558                                      {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   559                                  </>
   560                              )}
   561                          </li>
   562                      ))}
   563                  </ul>
   564              )}
   565          </DataLoader>
   566      );
   567  }
   568  
   569  export function renderResourceActionMenu(resource: ResourceTreeNode, application: appModels.Application, apis: ContextApis): React.ReactNode {
   570      const menuItems = getResourceActionsMenuItems(resource, application.metadata, apis);
   571  
   572      return (
   573          <DataLoader load={() => menuItems}>
   574              {items => (
   575                  <ul>
   576                      {items.map((item, i) => (
   577                          <li
   578                              className={classNames('application-details__action-menu', {disabled: item.disabled})}
   579                              key={i}
   580                              onClick={e => {
   581                                  e.stopPropagation();
   582                                  if (!item.disabled) {
   583                                      item.action();
   584                                      document.body.click();
   585                                  }
   586                              }}>
   587                              {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   588                          </li>
   589                      ))}
   590                  </ul>
   591              )}
   592          </DataLoader>
   593      );
   594  }
   595  
   596  export function renderResourceButtons(
   597      resource: ResourceTreeNode,
   598      application: appModels.Application,
   599      tree: appModels.ApplicationTree,
   600      apis: ContextApis,
   601      appChanged: BehaviorSubject<appModels.Application>
   602  ): React.ReactNode {
   603      let menuItems: Observable<ActionMenuItem[]>;
   604      menuItems = getActionItems(resource, application, tree, apis, appChanged, true);
   605      return (
   606          <DataLoader load={() => menuItems}>
   607              {items => (
   608                  <div className='pod-view__node__quick-start-actions'>
   609                      {items.map((item, i) => (
   610                          <ActionButton
   611                              disabled={item.disabled}
   612                              key={i}
   613                              action={(e: React.MouseEvent) => {
   614                                  e.stopPropagation();
   615                                  if (!item.disabled) {
   616                                      item.action();
   617                                      document.body.click();
   618                                  }
   619                              }}
   620                              icon={item.iconClassName}
   621                              tooltip={
   622                                  item.title
   623                                      .toString()
   624                                      .charAt(0)
   625                                      .toUpperCase() + item.title.toString().slice(1)
   626                              }
   627                          />
   628                      ))}
   629                  </div>
   630              )}
   631          </DataLoader>
   632      );
   633  }
   634  
   635  export function syncStatusMessage(app: appModels.Application) {
   636      const source = getAppDefaultSource(app);
   637      const rev = app.status.sync.revision || source.targetRevision || 'HEAD';
   638      let message = source.targetRevision || 'HEAD';
   639  
   640      if (app.status.sync.revision) {
   641          if (source.chart) {
   642              message += ' (' + app.status.sync.revision + ')';
   643          } else if (app.status.sync.revision.length >= 7 && !app.status.sync.revision.startsWith(source.targetRevision)) {
   644              message += ' (' + app.status.sync.revision.substr(0, 7) + ')';
   645          }
   646      }
   647      switch (app.status.sync.status) {
   648          case appModels.SyncStatuses.Synced:
   649              return (
   650                  <span>
   651                      to{' '}
   652                      <Revision repoUrl={source.repoURL} revision={rev}>
   653                          {message}
   654                      </Revision>{' '}
   655                  </span>
   656              );
   657          case appModels.SyncStatuses.OutOfSync:
   658              return (
   659                  <span>
   660                      from{' '}
   661                      <Revision repoUrl={source.repoURL} revision={rev}>
   662                          {message}
   663                      </Revision>{' '}
   664                  </span>
   665              );
   666          default:
   667              return <span>{message}</span>;
   668      }
   669  }
   670  
   671  export const HealthStatusIcon = ({state, noSpin}: {state: appModels.HealthStatus; noSpin?: boolean}) => {
   672      let color = COLORS.health.unknown;
   673      let icon = 'fa-question-circle';
   674  
   675      switch (state.status) {
   676          case appModels.HealthStatuses.Healthy:
   677              color = COLORS.health.healthy;
   678              icon = 'fa-heart';
   679              break;
   680          case appModels.HealthStatuses.Suspended:
   681              color = COLORS.health.suspended;
   682              icon = 'fa-pause-circle';
   683              break;
   684          case appModels.HealthStatuses.Degraded:
   685              color = COLORS.health.degraded;
   686              icon = 'fa-heart-broken';
   687              break;
   688          case appModels.HealthStatuses.Progressing:
   689              color = COLORS.health.progressing;
   690              icon = `fa fa-circle-notch ${noSpin ? '' : 'fa-spin'}`;
   691              break;
   692          case appModels.HealthStatuses.Missing:
   693              color = COLORS.health.missing;
   694              icon = 'fa-ghost';
   695              break;
   696      }
   697      let title: string = state.status;
   698      if (state.message) {
   699          title = `${state.status}: ${state.message}`;
   700      }
   701      return <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon} style={{color}} />;
   702  };
   703  
   704  export const PodHealthIcon = ({state}: {state: appModels.HealthStatus}) => {
   705      let icon = 'fa-question-circle';
   706  
   707      switch (state.status) {
   708          case appModels.HealthStatuses.Healthy:
   709              icon = 'fa-check';
   710              break;
   711          case appModels.HealthStatuses.Suspended:
   712              icon = 'fa-check';
   713              break;
   714          case appModels.HealthStatuses.Degraded:
   715              icon = 'fa-times';
   716              break;
   717          case appModels.HealthStatuses.Progressing:
   718              icon = 'fa fa-circle-notch fa-spin';
   719              break;
   720      }
   721      let title: string = state.status;
   722      if (state.message) {
   723          title = `${state.status}: ${state.message}`;
   724      }
   725      return <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon} />;
   726  };
   727  
   728  export const PodPhaseIcon = ({state}: {state: appModels.PodPhase}) => {
   729      let className = '';
   730      switch (state) {
   731          case appModels.PodPhase.PodSucceeded:
   732              className = 'fa fa-check';
   733              break;
   734          case appModels.PodPhase.PodRunning:
   735              className = 'fa fa-circle-notch fa-spin';
   736              break;
   737          case appModels.PodPhase.PodPending:
   738              className = 'fa fa-circle-notch fa-spin';
   739              break;
   740          case appModels.PodPhase.PodFailed:
   741              className = 'fa fa-times';
   742              break;
   743          default:
   744              className = 'fa fa-question-circle';
   745              break;
   746      }
   747      return <i qe-id='utils-pod-phase-icon' className={className} />;
   748  };
   749  
   750  export const ResourceResultIcon = ({resource}: {resource: appModels.ResourceResult}) => {
   751      let color = COLORS.sync_result.unknown;
   752      let icon = 'fas fa-question-circle';
   753  
   754      if (!resource.hookType && resource.status) {
   755          switch (resource.status) {
   756              case appModels.ResultCodes.Synced:
   757                  color = COLORS.sync_result.synced;
   758                  icon = 'fa-heart';
   759                  break;
   760              case appModels.ResultCodes.Pruned:
   761                  color = COLORS.sync_result.pruned;
   762                  icon = 'fa-heart';
   763                  break;
   764              case appModels.ResultCodes.SyncFailed:
   765                  color = COLORS.sync_result.failed;
   766                  icon = 'fa-heart-broken';
   767                  break;
   768              case appModels.ResultCodes.PruneSkipped:
   769                  icon = 'fa-heart';
   770                  break;
   771          }
   772          let title: string = resource.message;
   773          if (resource.message) {
   774              title = `${resource.status}: ${resource.message}`;
   775          }
   776          return <i title={title} className={'fa ' + icon} style={{color}} />;
   777      }
   778      if (resource.hookType && resource.hookPhase) {
   779          let className = '';
   780          switch (resource.hookPhase) {
   781              case appModels.OperationPhases.Running:
   782                  color = COLORS.operation.running;
   783                  className = 'fa fa-circle-notch fa-spin';
   784                  break;
   785              case appModels.OperationPhases.Failed:
   786                  color = COLORS.operation.failed;
   787                  className = 'fa fa-heart-broken';
   788                  break;
   789              case appModels.OperationPhases.Error:
   790                  color = COLORS.operation.error;
   791                  className = 'fa fa-heart-broken';
   792                  break;
   793              case appModels.OperationPhases.Succeeded:
   794                  color = COLORS.operation.success;
   795                  className = 'fa fa-heart';
   796                  break;
   797              case appModels.OperationPhases.Terminating:
   798                  color = COLORS.operation.terminating;
   799                  className = 'fa fa-circle-notch fa-spin';
   800                  break;
   801          }
   802          let title: string = resource.message;
   803          if (resource.message) {
   804              title = `${resource.hookPhase}: ${resource.message}`;
   805          }
   806          return <i title={title} className={className} style={{color}} />;
   807      }
   808      return null;
   809  };
   810  
   811  export const getAppOperationState = (app: appModels.Application): appModels.OperationState => {
   812      if (app.operation) {
   813          return {
   814              phase: appModels.OperationPhases.Running,
   815              message: (app.status && app.status.operationState && app.status.operationState.message) || 'waiting to start',
   816              startedAt: new Date().toISOString(),
   817              operation: {
   818                  sync: {}
   819              }
   820          } as appModels.OperationState;
   821      } else if (app.metadata.deletionTimestamp) {
   822          return {
   823              phase: appModels.OperationPhases.Running,
   824              startedAt: app.metadata.deletionTimestamp
   825          } as appModels.OperationState;
   826      } else {
   827          return app.status.operationState;
   828      }
   829  };
   830  
   831  export function getOperationType(application: appModels.Application) {
   832      const operation = application.operation || (application.status && application.status.operationState && application.status.operationState.operation);
   833      if (application.metadata.deletionTimestamp && !application.operation) {
   834          return 'Delete';
   835      }
   836      if (operation && operation.sync) {
   837          return 'Sync';
   838      }
   839      return 'Unknown';
   840  }
   841  
   842  const getOperationStateTitle = (app: appModels.Application) => {
   843      const appOperationState = getAppOperationState(app);
   844      const operationType = getOperationType(app);
   845      switch (operationType) {
   846          case 'Delete':
   847              return 'Deleting';
   848          case 'Sync':
   849              switch (appOperationState.phase) {
   850                  case 'Running':
   851                      return 'Syncing';
   852                  case 'Error':
   853                      return 'Sync error';
   854                  case 'Failed':
   855                      return 'Sync failed';
   856                  case 'Succeeded':
   857                      return 'Sync OK';
   858                  case 'Terminating':
   859                      return 'Terminated';
   860              }
   861      }
   862      return 'Unknown';
   863  };
   864  
   865  export const OperationState = ({app, quiet}: {app: appModels.Application; quiet?: boolean}) => {
   866      const appOperationState = getAppOperationState(app);
   867      if (appOperationState === undefined) {
   868          return <React.Fragment />;
   869      }
   870      if (quiet && [appModels.OperationPhases.Running, appModels.OperationPhases.Failed, appModels.OperationPhases.Error].indexOf(appOperationState.phase) === -1) {
   871          return <React.Fragment />;
   872      }
   873  
   874      return (
   875          <React.Fragment>
   876              <OperationPhaseIcon app={app} /> {getOperationStateTitle(app)}
   877          </React.Fragment>
   878      );
   879  };
   880  
   881  export function getPodStateReason(pod: appModels.State): {message: string; reason: string; netContainerStatuses: any[]} {
   882      let reason = pod.status.phase;
   883      let message = '';
   884      if (pod.status.reason) {
   885          reason = pod.status.reason;
   886      }
   887  
   888      let initializing = false;
   889  
   890      let netContainerStatuses = pod.status.initContainerStatuses || [];
   891      netContainerStatuses = netContainerStatuses.concat(pod.status.containerStatuses || []);
   892  
   893      for (const container of (pod.status.initContainerStatuses || []).slice().reverse()) {
   894          if (container.state.terminated && container.state.terminated.exitCode === 0) {
   895              continue;
   896          }
   897  
   898          if (container.state.terminated) {
   899              if (container.state.terminated.reason) {
   900                  reason = `Init:ExitCode:${container.state.terminated.exitCode}`;
   901              } else {
   902                  reason = `Init:${container.state.terminated.reason}`;
   903                  message = container.state.terminated.message;
   904              }
   905          } else if (container.state.waiting && container.state.waiting.reason && container.state.waiting.reason !== 'PodInitializing') {
   906              reason = `Init:${container.state.waiting.reason}`;
   907              message = `Init:${container.state.waiting.message}`;
   908          } else {
   909              reason = `Init: ${(pod.spec.initContainers || []).length})`;
   910          }
   911          initializing = true;
   912          break;
   913      }
   914  
   915      if (!initializing) {
   916          let hasRunning = false;
   917          for (const container of pod.status.containerStatuses || []) {
   918              if (container.state.waiting && container.state.waiting.reason) {
   919                  reason = container.state.waiting.reason;
   920                  message = container.state.waiting.message;
   921              } else if (container.state.terminated && container.state.terminated.reason) {
   922                  reason = container.state.terminated.reason;
   923                  message = container.state.terminated.message;
   924              } else if (container.state.terminated && !container.state.terminated.reason) {
   925                  if (container.state.terminated.signal !== 0) {
   926                      reason = `Signal:${container.state.terminated.signal}`;
   927                      message = '';
   928                  } else {
   929                      reason = `ExitCode:${container.state.terminated.exitCode}`;
   930                      message = '';
   931                  }
   932              } else if (container.ready && container.state.running) {
   933                  hasRunning = true;
   934              }
   935          }
   936  
   937          // change pod status back to 'Running' if there is at least one container still reporting as 'Running' status
   938          if (reason === 'Completed' && hasRunning) {
   939              reason = 'Running';
   940              message = '';
   941          }
   942      }
   943  
   944      if ((pod as any).metadata.deletionTimestamp && pod.status.reason === 'NodeLost') {
   945          reason = 'Unknown';
   946          message = '';
   947      } else if ((pod as any).metadata.deletionTimestamp) {
   948          reason = 'Terminating';
   949          message = '';
   950      }
   951  
   952      return {reason, message, netContainerStatuses};
   953  }
   954  
   955  export const getPodReadinessGatesState = (pod: appModels.State): {nonExistingConditions: string[]; notPassedConditions: string[]} => {
   956      // if pod does not have readiness gates then return empty status
   957      if (!pod.spec?.readinessGates?.length) {
   958          return {
   959              nonExistingConditions: [],
   960              notPassedConditions: []
   961          };
   962      }
   963  
   964      const existingConditions = new Map<string, boolean>();
   965      const podConditions = new Map<string, boolean>();
   966  
   967      const podStatusConditions = pod.status?.conditions || [];
   968  
   969      for (const condition of podStatusConditions) {
   970          existingConditions.set(condition.type, true);
   971          // priority order of conditions
   972          // eg. if there are multiple conditions set with same name then the one which comes first is evaluated
   973          if (podConditions.has(condition.type)) {
   974              continue;
   975          }
   976  
   977          if (condition.status === 'False') {
   978              podConditions.set(condition.type, false);
   979          } else if (condition.status === 'True') {
   980              podConditions.set(condition.type, true);
   981          }
   982      }
   983  
   984      const nonExistingConditions: string[] = [];
   985      const failedConditions: string[] = [];
   986  
   987      const readinessGates: appModels.ReadinessGate[] = pod.spec?.readinessGates || [];
   988  
   989      for (const readinessGate of readinessGates) {
   990          if (!existingConditions.has(readinessGate.conditionType)) {
   991              nonExistingConditions.push(readinessGate.conditionType);
   992          } else if (podConditions.get(readinessGate.conditionType) === false) {
   993              failedConditions.push(readinessGate.conditionType);
   994          }
   995      }
   996  
   997      return {
   998          nonExistingConditions,
   999          notPassedConditions: failedConditions
  1000      };
  1001  };
  1002  
  1003  export function getConditionCategory(condition: appModels.ApplicationCondition): 'error' | 'warning' | 'info' {
  1004      if (condition.type.endsWith('Error')) {
  1005          return 'error';
  1006      } else if (condition.type.endsWith('Warning')) {
  1007          return 'warning';
  1008      } else {
  1009          return 'info';
  1010      }
  1011  }
  1012  
  1013  export function isAppNode(node: appModels.ResourceNode) {
  1014      return node.kind === 'Application' && node.group === 'argoproj.io';
  1015  }
  1016  
  1017  export function getAppOverridesCount(app: appModels.Application) {
  1018      const source = getAppDefaultSource(app);
  1019      if (source.kustomize && source.kustomize.images) {
  1020          return source.kustomize.images.length;
  1021      }
  1022      if (source.helm && source.helm.parameters) {
  1023          return source.helm.parameters.length;
  1024      }
  1025      return 0;
  1026  }
  1027  
  1028  // getAppDefaultSource gets the first app source from `sources` or, if that list is missing or empty, the `source`
  1029  // field.
  1030  export function getAppDefaultSource(app?: appModels.Application) {
  1031      if (!app) {
  1032          return null;
  1033      }
  1034      return app.spec.sources && app.spec.sources.length > 0 ? app.spec.sources[0] : app.spec.source;
  1035  }
  1036  
  1037  export function getAppSpecDefaultSource(spec: appModels.ApplicationSpec) {
  1038      return spec.sources && spec.sources.length > 0 ? spec.sources[0] : spec.source;
  1039  }
  1040  
  1041  export function isAppRefreshing(app: appModels.Application) {
  1042      return !!(app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]);
  1043  }
  1044  
  1045  export function setAppRefreshing(app: appModels.Application) {
  1046      if (!app.metadata.annotations) {
  1047          app.metadata.annotations = {};
  1048      }
  1049      if (!app.metadata.annotations[appModels.AnnotationRefreshKey]) {
  1050          app.metadata.annotations[appModels.AnnotationRefreshKey] = 'refreshing';
  1051      }
  1052  }
  1053  
  1054  export function refreshLinkAttrs(app: appModels.Application) {
  1055      return {disabled: isAppRefreshing(app)};
  1056  }
  1057  
  1058  export const SyncWindowStatusIcon = ({state, window}: {state: appModels.SyncWindowsState; window: appModels.SyncWindow}) => {
  1059      let className = '';
  1060      let color = '';
  1061      let current = '';
  1062  
  1063      if (state.windows === undefined) {
  1064          current = 'Inactive';
  1065      } else {
  1066          for (const w of state.windows) {
  1067              if (w.kind === window.kind && w.schedule === window.schedule && w.duration === window.duration && w.timeZone === window.timeZone) {
  1068                  current = 'Active';
  1069                  break;
  1070              } else {
  1071                  current = 'Inactive';
  1072              }
  1073          }
  1074      }
  1075  
  1076      switch (current + ':' + window.kind) {
  1077          case 'Active:deny':
  1078          case 'Inactive:allow':
  1079              className = 'fa fa-stop-circle';
  1080              if (window.manualSync) {
  1081                  color = COLORS.sync_window.manual;
  1082              } else {
  1083                  color = COLORS.sync_window.deny;
  1084              }
  1085              break;
  1086          case 'Active:allow':
  1087          case 'Inactive:deny':
  1088              className = 'fa fa-check-circle';
  1089              color = COLORS.sync_window.allow;
  1090              break;
  1091          default:
  1092              className = 'fas fa-question-circle';
  1093              color = COLORS.sync_window.unknown;
  1094              current = 'Unknown';
  1095              break;
  1096      }
  1097  
  1098      return (
  1099          <React.Fragment>
  1100              <i title={current} className={className} style={{color}} /> {current}
  1101          </React.Fragment>
  1102      );
  1103  };
  1104  
  1105  export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state: appModels.ApplicationSyncWindowState}) => {
  1106      let className = '';
  1107      let color = '';
  1108      let deny = false;
  1109      let allow = false;
  1110      let inactiveAllow = false;
  1111      if (state.assignedWindows !== undefined && state.assignedWindows.length > 0) {
  1112          if (state.activeWindows !== undefined && state.activeWindows.length > 0) {
  1113              for (const w of state.activeWindows) {
  1114                  if (w.kind === 'deny') {
  1115                      deny = true;
  1116                  } else if (w.kind === 'allow') {
  1117                      allow = true;
  1118                  }
  1119              }
  1120          }
  1121          for (const a of state.assignedWindows) {
  1122              if (a.kind === 'allow') {
  1123                  inactiveAllow = true;
  1124              }
  1125          }
  1126      } else {
  1127          allow = true;
  1128      }
  1129  
  1130      if (deny || (!deny && !allow && inactiveAllow)) {
  1131          className = 'fa fa-stop-circle';
  1132          if (state.canSync) {
  1133              color = COLORS.sync_window.manual;
  1134          } else {
  1135              color = COLORS.sync_window.deny;
  1136          }
  1137      } else {
  1138          className = 'fa fa-check-circle';
  1139          color = COLORS.sync_window.allow;
  1140      }
  1141  
  1142      const ctx = React.useContext(Context);
  1143  
  1144      return (
  1145          <a href={`${ctx.baseHref}settings/projects/${project}?tab=windows`} style={{color}}>
  1146              <i className={className} style={{color}} /> SyncWindow
  1147          </a>
  1148      );
  1149  };
  1150  
  1151  /**
  1152   * Automatically stops and restarts the given observable when page visibility changes.
  1153   */
  1154  export function handlePageVisibility<T>(src: () => Observable<T>): Observable<T> {
  1155      return new Observable<T>((observer: Observer<T>) => {
  1156          let subscription: Subscription;
  1157          const ensureUnsubscribed = () => {
  1158              if (subscription) {
  1159                  subscription.unsubscribe();
  1160                  subscription = null;
  1161              }
  1162          };
  1163          const start = () => {
  1164              ensureUnsubscribed();
  1165              subscription = src().subscribe(
  1166                  (item: T) => observer.next(item),
  1167                  err => observer.error(err),
  1168                  () => observer.complete()
  1169              );
  1170          };
  1171  
  1172          if (!document.hidden) {
  1173              start();
  1174          }
  1175  
  1176          const visibilityChangeSubscription = fromEvent(document, 'visibilitychange')
  1177              // wait until user stop clicking back and forth to avoid restarting observable too often
  1178              .pipe(debounceTime(500))
  1179              .subscribe(() => {
  1180                  if (document.hidden && subscription) {
  1181                      ensureUnsubscribed();
  1182                  } else if (!document.hidden && !subscription) {
  1183                      start();
  1184                  }
  1185              });
  1186  
  1187          return () => {
  1188              visibilityChangeSubscription.unsubscribe();
  1189              ensureUnsubscribed();
  1190          };
  1191      });
  1192  }
  1193  
  1194  export function parseApiVersion(apiVersion: string): {group: string; version: string} {
  1195      const parts = apiVersion.split('/');
  1196      if (parts.length > 1) {
  1197          return {group: parts[0], version: parts[1]};
  1198      }
  1199      return {version: parts[0], group: ''};
  1200  }
  1201  
  1202  export function getContainerName(pod: any, containerIndex: number | null): string {
  1203      if (containerIndex == null && pod.metadata?.annotations?.['kubectl.kubernetes.io/default-container']) {
  1204          return pod.metadata?.annotations?.['kubectl.kubernetes.io/default-container'];
  1205      }
  1206      const containers = (pod.spec.containers || []).concat(pod.spec.initContainers || []);
  1207      const container = containers[containerIndex || 0];
  1208      return container.name;
  1209  }
  1210  
  1211  export function isYoungerThanXMinutes(pod: any, x: number): boolean {
  1212      const createdAt = moment(pod.createdAt, 'YYYY-MM-DDTHH:mm:ssZ');
  1213      const xMinutesAgo = moment().subtract(x, 'minutes');
  1214      return createdAt.isAfter(xMinutesAgo);
  1215  }
  1216  
  1217  export const BASE_COLORS = [
  1218      '#0DADEA', // blue
  1219      '#DE7EAE', // pink
  1220      '#FF9500', // orange
  1221      '#4B0082', // purple
  1222      '#F5d905', // yellow
  1223      '#964B00' // brown
  1224  ];
  1225  
  1226  export const urlPattern = new RegExp(
  1227      new RegExp(
  1228          // tslint:disable-next-line:max-line-length
  1229          /^(https?:\/\/(?:www\.|(?!www))[a-z0-9][a-z0-9-]+[a-z0-9]\.[^\s]{2,}|www\.[a-z0-9][a-z0-9-]+[a-z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-z0-9]+\.[^\s]{2,}|www\.[a-z0-9]+\.[^\s]{2,})$/,
  1230          'gi'
  1231      )
  1232  );
  1233  
  1234  export function appQualifiedName(app: appModels.Application, nsEnabled: boolean): string {
  1235      return (nsEnabled ? app.metadata.namespace + '/' : '') + app.metadata.name;
  1236  }
  1237  
  1238  export function appInstanceName(app: appModels.Application): string {
  1239      return app.metadata.namespace + '_' + app.metadata.name;
  1240  }
  1241  
  1242  export function formatCreationTimestamp(creationTimestamp: string) {
  1243      const createdAt = moment
  1244          .utc(creationTimestamp)
  1245          .local()
  1246          .format('MM/DD/YYYY HH:mm:ss');
  1247      const fromNow = moment
  1248          .utc(creationTimestamp)
  1249          .local()
  1250          .fromNow();
  1251      return (
  1252          <span>
  1253              {createdAt}
  1254              <i style={{padding: '2px'}} /> ({fromNow})
  1255          </span>
  1256      );
  1257  }
  1258  
  1259  export const selectPostfix = (arr: string[], singular: string, plural: string) => (arr.length > 1 ? plural : singular);
  1260  
  1261  export function getUsrMsgKeyToDisplay(appName: string, msgKey: string, usrMessages: appModels.UserMessages[]) {
  1262      const usrMsg = usrMessages?.find((msg: appModels.UserMessages) => msg.appName === appName && msg.msgKey === msgKey);
  1263      if (usrMsg !== undefined) {
  1264          return {...usrMsg, display: true};
  1265      } else {
  1266          return {appName, msgKey, display: false, duration: 1} as appModels.UserMessages;
  1267      }
  1268  }
  1269  
  1270  export const userMsgsList: {[key: string]: string} = {
  1271      groupNodes: `Since the number of pods has surpassed the threshold pod count of 15, you will now be switched to the group node view.
  1272                   If you prefer the tree view, you can simply click on the Group Nodes toolbar button to deselect the current view.`
  1273  };