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

     1  import {models, DataLoader, FormField, MenuItem, NotificationType, Tooltip, HelpIcon} 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  import {ApplicationSource} from '../../shared/models';
    17  
    18  require('./utils.scss');
    19  
    20  export interface NodeId {
    21      kind: string;
    22      namespace: string;
    23      name: string;
    24      group: string;
    25      createdAt?: models.Time;
    26  }
    27  
    28  type ActionMenuItem = MenuItem & {disabled?: boolean; tooltip?: string};
    29  
    30  export function nodeKey(node: NodeId) {
    31      return [node.group, node.kind, node.namespace, node.name].join('/');
    32  }
    33  
    34  export function createdOrNodeKey(node: NodeId) {
    35      return node?.createdAt || nodeKey(node);
    36  }
    37  
    38  export function isSameNode(first: NodeId, second: NodeId) {
    39      return nodeKey(first) === nodeKey(second);
    40  }
    41  
    42  export function helpTip(text: string) {
    43      return (
    44          <Tooltip content={text}>
    45              <span style={{fontSize: 'smaller'}}>
    46                  {' '}
    47                  <i className='fas fa-info-circle' />
    48              </span>
    49          </Tooltip>
    50      );
    51  }
    52  
    53  //CLassic Solid circle-notch icon
    54  //<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
    55  //this will replace all <i> fa-spin </i> icons as they are currently misbehaving with no fix available.
    56  
    57  export const SpinningIcon = ({color, qeId}: {color: string; qeId: string}) => {
    58      return (
    59          <svg className='icon spin' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' style={{color}} qe-id={qeId}>
    60              <path
    61                  fill={color}
    62                  d='M222.7 32.1c5 16.9-4.6 34.8-21.5 39.8C121.8 95.6 64 169.1 64 256c0 106 86 192 192 192s192-86 192-192c0-86.9-57.8-160.4-137.1-184.1c-16.9-5-26.6-22.9-21.5-39.8s22.9-26.6 39.8-21.5C434.9 42.1 512 140 512 256c0 141.4-114.6 256-256 256S0 397.4 0 256C0 140 77.1 42.1 182.9 10.6c16.9-5 34.8 4.6 39.8 21.5z'
    63              />
    64          </svg>
    65      );
    66  };
    67  
    68  export async function deleteApplication(appName: string, appNamespace: string, apis: ContextApis, application?: appModels.Application): Promise<boolean> {
    69      let confirmed = false;
    70  
    71      // Use common child application detection logic if application object is provided
    72      const isChildApp = application ? isChildApplication(application) : false;
    73      const dialogTitle = isChildApp ? 'Delete child application' : 'Delete application';
    74      const appType = isChildApp ? 'child Application' : 'Application';
    75      const confirmLabel = isChildApp ? 'child application' : 'application';
    76  
    77      // Check if this is being called from resource tree context
    78      const isFromResourceTree = application !== undefined;
    79  
    80      const propagationPolicies: {name: string; message: string}[] = [
    81          {
    82              name: 'Foreground',
    83              message: `Cascade delete the application's resources using foreground propagation policy`
    84          },
    85          {
    86              name: 'Background',
    87              message: `Cascade delete the application's resources using background propagation policy`
    88          },
    89          {
    90              name: 'Non-cascading',
    91              message: `Only delete the application, but do not cascade delete its resources`
    92          }
    93      ];
    94      await apis.popup.prompt(
    95          dialogTitle,
    96          api => (
    97              <div>
    98                  <p>
    99                      Are you sure you want to delete the <strong>{appType}</strong> <kbd>{appName}</kbd>?
   100                  </p>
   101                  {isFromResourceTree && (
   102                      <p>
   103                          <strong>
   104                              <i className='fa fa-warning delete-dialog-icon warning' /> Note:
   105                          </strong>{' '}
   106                          You are about to delete an Application from the resource tree. This uses the same deletion behavior as the Applications list page.
   107                      </p>
   108                  )}
   109                  <p>
   110                      Deleting the application in <kbd>foreground</kbd> or <kbd>background</kbd> mode will delete all the application's managed resources, which can be{' '}
   111                      <strong>dangerous</strong>. Be sure you understand the effects of deleting this resource before continuing. Consider asking someone to review the change first.
   112                  </p>
   113                  <div className='argo-form-row'>
   114                      <FormField
   115                          label={`Please type '${appName}' to confirm the deletion of the ${confirmLabel}`}
   116                          formApi={api}
   117                          field='applicationName'
   118                          qeId='name-field-delete-confirmation'
   119                          component={Text}
   120                      />
   121                  </div>
   122                  <p>Select propagation policy for application deletion</p>
   123                  <div className='propagation-policy-list'>
   124                      {propagationPolicies.map(policy => {
   125                          return (
   126                              <FormField
   127                                  formApi={api}
   128                                  key={policy.name}
   129                                  field='propagationPolicy'
   130                                  component={PropagationPolicyOption}
   131                                  componentProps={{
   132                                      policy: policy.name,
   133                                      message: policy.message
   134                                  }}
   135                              />
   136                          );
   137                      })}
   138                  </div>
   139              </div>
   140          ),
   141          {
   142              validate: vals => ({
   143                  applicationName: vals.applicationName !== appName && 'Enter the application name to confirm the deletion'
   144              }),
   145              submit: async (vals, _, close) => {
   146                  try {
   147                      await services.applications.delete(appName, appNamespace, vals.propagationPolicy);
   148                      confirmed = true;
   149                      close();
   150                  } catch (e) {
   151                      apis.notifications.show({
   152                          content: <ErrorNotification title='Unable to delete application' e={e} />,
   153                          type: NotificationType.Error
   154                      });
   155                  }
   156              }
   157          },
   158          {name: 'argo-icon-warning', color: 'failed'},
   159          'red',
   160          {propagationPolicy: 'foreground'}
   161      );
   162      return confirmed;
   163  }
   164  
   165  export async function confirmSyncingAppOfApps(apps: appModels.Application[], apis: ContextApis, form: FormApi): Promise<boolean> {
   166      let confirmed = false;
   167      const appNames: string[] = apps.map(app => app.metadata.name);
   168      const appNameList = appNames.join(', ');
   169      await apis.popup.prompt(
   170          'Warning: Synchronize App of Multiple Apps using replace?',
   171          api => (
   172              <div>
   173                  <p>
   174                      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
   175                      apps linked to '{appNameList}'.
   176                  </p>
   177                  <div className='argo-form-row'>
   178                      <FormField
   179                          label={`Please type '${appNameList}' to confirm the Syncing of the resource`}
   180                          formApi={api}
   181                          field='applicationName'
   182                          qeId='name-field-delete-confirmation'
   183                          component={Text}
   184                      />
   185                  </div>
   186              </div>
   187          ),
   188          {
   189              validate: vals => ({
   190                  applicationName: vals.applicationName !== appNameList && 'Enter the application name(s) to confirm syncing'
   191              }),
   192              submit: async (_vals, _, close) => {
   193                  try {
   194                      await form.submitForm(null);
   195                      confirmed = true;
   196                      close();
   197                  } catch (e) {
   198                      apis.notifications.show({
   199                          content: <ErrorNotification title='Unable to sync application' e={e} />,
   200                          type: NotificationType.Error
   201                      });
   202                  }
   203              }
   204          },
   205          {name: 'argo-icon-warning', color: 'warning'},
   206          'yellow'
   207      );
   208      return confirmed;
   209  }
   210  
   211  const PropagationPolicyOption = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi; policy: string; message: string}) => {
   212      const {
   213          fieldApi: {setValue}
   214      } = props;
   215      return (
   216          <div className='propagation-policy-option'>
   217              <input
   218                  className='radio-button'
   219                  key={props.policy}
   220                  type='radio'
   221                  name='propagation-policy'
   222                  value={props.policy}
   223                  id={props.policy}
   224                  defaultChecked={props.policy === 'Foreground'}
   225                  onChange={() => setValue(props.policy.toLowerCase())}
   226              />
   227              <label htmlFor={props.policy}>
   228                  {props.policy} {helpTip(props.message)}
   229              </label>
   230          </div>
   231      );
   232  });
   233  
   234  export const OperationPhaseIcon = ({app, isButton}: {app: appModels.Application; isButton?: boolean}) => {
   235      const operationState = getAppOperationState(app);
   236      if (operationState === undefined) {
   237          return null;
   238      }
   239      let className = '';
   240      let color = '';
   241      switch (operationState.phase) {
   242          case appModels.OperationPhases.Succeeded:
   243              className = `fa fa-check-circle${isButton ? ' status-button' : ''}`;
   244              color = COLORS.operation.success;
   245              break;
   246          case appModels.OperationPhases.Error:
   247              className = `fa fa-times-circle${isButton ? ' status-button' : ''}`;
   248              color = COLORS.operation.error;
   249              break;
   250          case appModels.OperationPhases.Failed:
   251              className = `fa fa-times-circle${isButton ? ' status-button' : ''}`;
   252              color = COLORS.operation.failed;
   253              break;
   254          default:
   255              className = 'fa fa-circle-notch fa-spin';
   256              color = COLORS.operation.running;
   257              break;
   258      }
   259      return className.includes('fa-spin') ? (
   260          <SpinningIcon color={color} qeId='utils-operations-status-title' />
   261      ) : (
   262          <i title={getOperationStateTitle(app)} qe-id='utils-operations-status-title' className={className} style={{color}} />
   263      );
   264  };
   265  
   266  export const HydrateOperationPhaseIcon = ({operationState, isButton}: {operationState?: appModels.HydrateOperation; isButton?: boolean}) => {
   267      if (operationState === undefined) {
   268          return null;
   269      }
   270      let className = '';
   271      let color = '';
   272      switch (operationState.phase) {
   273          case appModels.HydrateOperationPhases.Hydrated:
   274              className = `fa fa-check-circle${isButton ? ' status-button' : ''}`;
   275              color = COLORS.operation.success;
   276              break;
   277          case appModels.HydrateOperationPhases.Failed:
   278              className = `fa fa-times-circle${isButton ? ' status-button' : ''}`;
   279              color = COLORS.operation.failed;
   280              break;
   281          default:
   282              className = 'fa fa-circle-notch fa-spin';
   283              color = COLORS.operation.running;
   284              break;
   285      }
   286      return className.includes('fa-spin') ? (
   287          <SpinningIcon color={color} qeId='utils-operations-status-title' />
   288      ) : (
   289          <i title={operationState.phase} qe-id='utils-operations-status-title' className={className} style={{color}} />
   290      );
   291  };
   292  
   293  export const ComparisonStatusIcon = ({
   294      status,
   295      resource,
   296      label,
   297      noSpin,
   298      isButton
   299  }: {
   300      status: appModels.SyncStatusCode;
   301      resource?: {requiresPruning?: boolean};
   302      label?: boolean;
   303      noSpin?: boolean;
   304      isButton?: boolean;
   305  }) => {
   306      let className = 'fas fa-question-circle';
   307      let color = COLORS.sync.unknown;
   308      let title: string = 'Unknown';
   309      switch (status) {
   310          case appModels.SyncStatuses.Synced:
   311              className = `fa fa-check-circle${isButton ? ' status-button' : ''}`;
   312              color = COLORS.sync.synced;
   313              title = 'Synced';
   314              break;
   315          case appModels.SyncStatuses.OutOfSync:
   316              // eslint-disable-next-line no-case-declarations
   317              const requiresPruning = resource && resource.requiresPruning;
   318              className = requiresPruning ? `fa fa-trash${isButton ? ' status-button' : ''}` : `fa fa-arrow-alt-circle-up${isButton ? ' status-button' : ''}`;
   319              title = 'OutOfSync';
   320              if (requiresPruning) {
   321                  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.)`;
   322              }
   323              color = COLORS.sync.out_of_sync;
   324              break;
   325          case appModels.SyncStatuses.Unknown:
   326              className = `fa fa-circle-notch ${noSpin ? '' : 'fa-spin'}${isButton ? ' status-button' : ''}`;
   327              break;
   328      }
   329      return className.includes('fa-spin') ? (
   330          <SpinningIcon color={color} qeId='utils-sync-status-title' />
   331      ) : (
   332          <React.Fragment>
   333              <i qe-id='utils-sync-status-title' title={title} className={className} style={{color}} /> {label && title}
   334          </React.Fragment>
   335      );
   336  };
   337  
   338  export function showDeploy(resource: string, revision: string, apis: ContextApis) {
   339      apis.navigation.goto('.', {deploy: resource, revision}, {replace: true});
   340  }
   341  
   342  export function findChildPod(node: appModels.ResourceNode, tree: appModels.ApplicationTree): appModels.ResourceNode {
   343      const key = nodeKey(node);
   344  
   345      const allNodes = tree.nodes.concat(tree.orphanedNodes || []);
   346      const nodeByKey = new Map<string, appModels.ResourceNode>();
   347      allNodes.forEach(item => nodeByKey.set(nodeKey(item), item));
   348  
   349      const pods = tree.nodes.concat(tree.orphanedNodes || []).filter(item => item.kind === 'Pod');
   350      return pods.find(pod => {
   351          const items: Array<appModels.ResourceNode> = [pod];
   352          while (items.length > 0) {
   353              const next = items.pop();
   354              const parentKeys = (next.parentRefs || []).map(nodeKey);
   355              if (parentKeys.includes(key)) {
   356                  return true;
   357              }
   358              parentKeys.forEach(item => {
   359                  const parent = nodeByKey.get(item);
   360                  if (parent) {
   361                      items.push(parent);
   362                  }
   363              });
   364          }
   365  
   366          return false;
   367      });
   368  }
   369  
   370  export function findChildResources(node: appModels.ResourceNode, tree: appModels.ApplicationTree): appModels.ResourceNode[] {
   371      const key = nodeKey(node);
   372  
   373      const children: appModels.ResourceNode[] = [];
   374      tree.nodes.forEach(item => {
   375          (item.parentRefs || []).forEach(parent => {
   376              if (key === nodeKey(parent)) {
   377                  children.push(item);
   378              }
   379          });
   380      });
   381  
   382      return children;
   383  }
   384  
   385  const deletePodAction = async (ctx: ContextApis, pod: appModels.ResourceNode, app: appModels.Application) => {
   386      ctx.popup.prompt(
   387          'Delete pod',
   388          () => (
   389              <div>
   390                  <p>
   391                      Are you sure you want to delete <strong>Pod</strong> <kbd>{pod.name}</kbd>?
   392                      <span style={{display: 'block', marginBottom: '10px'}} />
   393                      Deleting resources can be <strong>dangerous</strong>. Be sure you understand the effects of deleting this resource before continuing. Consider asking someone to
   394                      review the change first.
   395                  </p>
   396                  <div className='argo-form-row' style={{paddingLeft: '30px'}}>
   397                      <CheckboxField id='force-delete-checkbox' field='force' />
   398                      <label htmlFor='force-delete-checkbox'>Force delete</label>
   399                      <HelpIcon title='If checked, Argo will ignore any configured grace period and delete the resource immediately' />
   400                  </div>
   401              </div>
   402          ),
   403          {
   404              submit: async (vals, _, close) => {
   405                  try {
   406                      await services.applications.deleteResource(app.metadata.name, app.metadata.namespace, pod, !!vals.force, false);
   407                      close();
   408                  } catch (e) {
   409                      ctx.notifications.show({
   410                          content: <ErrorNotification title='Unable to delete resource' e={e} />,
   411                          type: NotificationType.Error
   412                      });
   413                  }
   414              }
   415          }
   416      );
   417  };
   418  
   419  export const deleteSourceAction = (app: appModels.Application, source: appModels.ApplicationSource, appContext: AppContext) => {
   420      appContext.apis.popup.prompt(
   421          'Delete source',
   422          () => (
   423              <div>
   424                  <p>
   425                      Are you sure you want to delete the source with URL: <kbd>{source.repoURL}</kbd>
   426                      {source.path ? (
   427                          <>
   428                              {' '}
   429                              and path: <kbd>{source.path}</kbd>?
   430                          </>
   431                      ) : (
   432                          <>?</>
   433                      )}
   434                  </p>
   435              </div>
   436          ),
   437          {
   438              submit: async (vals, _, close) => {
   439                  try {
   440                      const i = app.spec.sources.indexOf(source);
   441                      app.spec.sources.splice(i, 1);
   442                      await services.applications.update(app);
   443                      close();
   444                  } catch (e) {
   445                      appContext.apis.notifications.show({
   446                          content: <ErrorNotification title='Unable to delete source' e={e} />,
   447                          type: NotificationType.Error
   448                      });
   449                  }
   450              }
   451          },
   452          {name: 'argo-icon-warning', color: 'warning'},
   453          'yellow'
   454      );
   455  };
   456  
   457  // Detect if a resource is an Application
   458  const isApplicationResource = (resource: ResourceTreeNode): boolean => {
   459      return resource.kind === 'Application' && resource.group === 'argoproj.io';
   460  };
   461  
   462  // Detect if an application is a child application
   463  const isChildApplication = (application: appModels.Application): boolean => {
   464      const partOfLabel = application.metadata.labels?.['app.kubernetes.io/part-of'];
   465      return partOfLabel && partOfLabel.trim() !== '';
   466  };
   467  
   468  export const deletePopup = async (
   469      ctx: ContextApis,
   470      resource: ResourceTreeNode,
   471      application: appModels.Application,
   472      isManaged: boolean,
   473      childResources: appModels.ResourceNode[],
   474      appChanged?: BehaviorSubject<appModels.Application>
   475  ) => {
   476      // Detect if this is an Application resource
   477      const isApplication = isApplicationResource(resource);
   478  
   479      // Check if we're in a parent-child context (used for both Application and non-Application resources)
   480      const isInParentContext = isChildApplication(application);
   481  
   482      // For Application resources, use the deleteApplication function with resource tree context
   483      if (isApplication) {
   484          return deleteApplication(resource.name, resource.namespace || '', ctx, application);
   485      }
   486  
   487      const deleteOptions = {
   488          option: 'foreground'
   489      };
   490      function handleStateChange(option: string) {
   491          deleteOptions.option = option;
   492      }
   493  
   494      if (resource.kind === 'Pod' && !isManaged) {
   495          return deletePodAction(ctx, resource, application);
   496      }
   497  
   498      // Determine dialog title and add custom messaging
   499      const dialogTitle = 'Delete resource';
   500      let customMessage: React.ReactNode = null;
   501  
   502      if (isInParentContext) {
   503          customMessage = (
   504              <div>
   505                  <p>
   506                      <strong>
   507                          <i className='fa fa-exclamation-triangle delete-dialog-icon info' /> Note:
   508                      </strong>{' '}
   509                      You are about to delete a resource from a parent application's resource tree.
   510                  </p>
   511              </div>
   512          );
   513      }
   514  
   515      return ctx.popup.prompt(
   516          dialogTitle,
   517          api => (
   518              <div>
   519                  <p>
   520                      Are you sure you want to delete <strong>{resource.kind}</strong> <kbd>{resource.name}</kbd>?
   521                  </p>
   522                  {customMessage}
   523                  <p>
   524                      Deleting resources can be <strong>dangerous</strong>. Be sure you understand the effects of deleting this resource before continuing. Consider asking someone to
   525                      review the change first.
   526                  </p>
   527  
   528                  {(childResources || []).length > 0 ? (
   529                      <React.Fragment>
   530                          <p>Dependent resources:</p>
   531                          <ul>
   532                              {childResources.slice(0, 4).map((child, i) => (
   533                                  <li key={i}>
   534                                      <kbd>{[child.kind, child.name].join('/')}</kbd>
   535                                  </li>
   536                              ))}
   537                              {childResources.length === 5 ? (
   538                                  <li key='4'>
   539                                      <kbd>{[childResources[4].kind, childResources[4].name].join('/')}</kbd>
   540                                  </li>
   541                              ) : (
   542                                  ''
   543                              )}
   544                              {childResources.length > 5 ? <li key='N'>and {childResources.slice(4).length} more.</li> : ''}
   545                          </ul>
   546                      </React.Fragment>
   547                  ) : (
   548                      ''
   549                  )}
   550  
   551                  {isManaged ? (
   552                      <div className='argo-form-row'>
   553                          <FormField label={`Please type '${resource.name}' to confirm the deletion of the resource`} formApi={api} field='resourceName' component={Text} />
   554                      </div>
   555                  ) : (
   556                      ''
   557                  )}
   558                  <div className='argo-form-row'>
   559                      <input
   560                          type='radio'
   561                          name='deleteOptions'
   562                          value='foreground'
   563                          onChange={() => handleStateChange('foreground')}
   564                          defaultChecked={true}
   565                          style={{marginRight: '5px'}}
   566                          id='foreground-delete-radio'
   567                      />
   568                      <label htmlFor='foreground-delete-radio' style={{paddingRight: '30px'}}>
   569                          Foreground Delete {helpTip('Deletes the resource and dependent resources using the cascading policy in the foreground')}
   570                      </label>
   571                      <input type='radio' name='deleteOptions' value='force' onChange={() => handleStateChange('force')} style={{marginRight: '5px'}} id='force-delete-radio' />
   572                      <label htmlFor='force-delete-radio' style={{paddingRight: '30px'}}>
   573                          Background Delete {helpTip('Performs a forceful "background cascading deletion" of the resource and its dependent resources')}
   574                      </label>
   575                      <input type='radio' name='deleteOptions' value='orphan' onChange={() => handleStateChange('orphan')} style={{marginRight: '5px'}} id='cascade-delete-radio' />
   576                      <label htmlFor='cascade-delete-radio'>Non-cascading (Orphan) Delete {helpTip('Deletes the resource and orphans the dependent resources')}</label>
   577                  </div>
   578              </div>
   579          ),
   580          {
   581              validate: vals =>
   582                  isManaged && {
   583                      resourceName: vals.resourceName !== resource.name && 'Enter the resource name to confirm the deletion'
   584                  },
   585              submit: async (vals, _, close) => {
   586                  const force = deleteOptions.option === 'force';
   587                  const orphan = deleteOptions.option === 'orphan';
   588                  try {
   589                      await services.applications.deleteResource(application.metadata.name, application.metadata.namespace, resource, !!force, !!orphan);
   590                      if (appChanged) {
   591                          appChanged.next(await services.applications.get(application.metadata.name, application.metadata.namespace));
   592                      }
   593                      close();
   594                  } catch (e) {
   595                      ctx.notifications.show({
   596                          content: <ErrorNotification title='Unable to delete resource' e={e} />,
   597                          type: NotificationType.Error
   598                      });
   599                  }
   600              }
   601          },
   602          {name: 'argo-icon-warning', color: 'warning'},
   603          'yellow'
   604      );
   605  };
   606  
   607  export async function getResourceActionsMenuItems(resource: ResourceTreeNode, metadata: models.ObjectMeta, apis: ContextApis): Promise<ActionMenuItem[]> {
   608      // Don't call API for missing resources
   609      if (!resource.uid) {
   610          return [];
   611      }
   612  
   613      return services.applications
   614          .getResourceActions(metadata.name, metadata.namespace, resource)
   615          .then(actions => {
   616              return actions.map(action => ({
   617                  title: action.displayName ?? action.name,
   618                  disabled: !!action.disabled,
   619                  iconClassName: action.iconClass,
   620                  action: async () => {
   621                      const confirmed = false;
   622                      const title = action.params ? `Enter input parameters for action: ${action.name}` : `Perform ${action.name} action?`;
   623                      await apis.popup.prompt(
   624                          title,
   625                          api => (
   626                              <div>
   627                                  {!action.params && (
   628                                      <div className='argo-form-row'>
   629                                          <div> Are you sure you want to perform {action.name} action?</div>
   630                                      </div>
   631                                  )}
   632                                  {action.params &&
   633                                      action.params.map((param, index) => (
   634                                          <div className='argo-form-row' key={index}>
   635                                              <FormField label={param.name} field={param.name} formApi={api} component={Text} />
   636                                          </div>
   637                                      ))}
   638                              </div>
   639                          ),
   640                          {
   641                              submit: async (vals, _, close) => {
   642                                  try {
   643                                      const resourceActionParameters = action.params
   644                                          ? action.params.map(param => ({
   645                                                name: param.name,
   646                                                value: vals[param.name] || param.default,
   647                                                type: param.type,
   648                                                default: param.default
   649                                            }))
   650                                          : [];
   651                                      await services.applications.runResourceAction(metadata.name, metadata.namespace, resource, action.name, resourceActionParameters);
   652                                      close();
   653                                  } catch (e) {
   654                                      apis.notifications.show({
   655                                          content: <ErrorNotification title='Unable to execute resource action' e={e} />,
   656                                          type: NotificationType.Error
   657                                      });
   658                                  }
   659                              }
   660                          },
   661                          null,
   662                          null,
   663                          action.params
   664                              ? action.params.reduce((acc, res) => {
   665                                    acc[res.name] = res.default;
   666                                    return acc;
   667                                }, {} as any)
   668                              : {}
   669                      );
   670                      return confirmed;
   671                  }
   672              }));
   673          })
   674          .catch(() => [] as ActionMenuItem[]);
   675  }
   676  
   677  function getActionItems(
   678      resource: ResourceTreeNode,
   679      application: appModels.Application,
   680      tree: appModels.ApplicationTree,
   681      apis: ContextApis,
   682      appChanged: BehaviorSubject<appModels.Application>,
   683      isQuickStart: boolean
   684  ): Observable<ActionMenuItem[]> {
   685      function isTopLevelResource(res: ResourceTreeNode, app: appModels.Application): boolean {
   686          const uniqRes = `/${res.namespace}/${res.group}/${res.kind}/${res.name}`;
   687          return app.status.resources.some(resStatus => `/${resStatus.namespace}/${resStatus.group}/${resStatus.kind}/${resStatus.name}` === uniqRes);
   688      }
   689  
   690      const isPod = resource.kind === 'Pod';
   691      const isManaged = isTopLevelResource(resource, application);
   692      const childResources = findChildResources(resource, tree);
   693  
   694      const items: MenuItem[] = [
   695          ...((isManaged && [
   696              {
   697                  title: 'Sync',
   698                  iconClassName: 'fa fa-fw fa-sync',
   699                  action: () => showDeploy(nodeKey(resource), null, apis)
   700              }
   701          ]) ||
   702              []),
   703          {
   704              title: 'Delete',
   705              iconClassName: 'fa fa-fw fa-times-circle',
   706              action: async () => {
   707                  return deletePopup(apis, resource, application, isManaged, childResources, appChanged);
   708              }
   709          }
   710      ];
   711  
   712      if (!isQuickStart) {
   713          items.unshift({
   714              title: 'Details',
   715              iconClassName: 'fa fa-fw fa-info-circle',
   716              action: () => apis.navigation.goto('.', {node: nodeKey(resource)})
   717          });
   718      }
   719  
   720      const logsAction = services.accounts
   721          .canI('logs', 'get', application.spec.project + '/' + application.metadata.name)
   722          .then(async allowed => {
   723              if (allowed && (isPod || findChildPod(resource, tree))) {
   724                  return [
   725                      {
   726                          title: 'Logs',
   727                          iconClassName: 'fa fa-fw fa-align-left',
   728                          action: () => apis.navigation.goto('.', {node: nodeKey(resource), tab: 'logs'}, {replace: true})
   729                      } as MenuItem
   730                  ];
   731              }
   732              return [] as MenuItem[];
   733          })
   734          .catch(() => [] as MenuItem[]);
   735  
   736      if (isQuickStart) {
   737          return combineLatest(
   738              from([items]), // this resolves immediately
   739              concat([[] as MenuItem[]], logsAction) // this resolves at first to [] and then whatever the API returns
   740          ).pipe(map(res => ([] as MenuItem[]).concat(...res)));
   741      }
   742  
   743      const execAction = services.authService
   744          .settings()
   745          .then(async settings => {
   746              const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
   747              if (isPod && execAllowed) {
   748                  return [
   749                      {
   750                          title: 'Exec',
   751                          iconClassName: 'fa fa-fw fa-terminal',
   752                          action: async () => apis.navigation.goto('.', {node: nodeKey(resource), tab: 'exec'}, {replace: true})
   753                      } as MenuItem
   754                  ];
   755              }
   756              return [] as MenuItem[];
   757          })
   758          .catch(() => [] as MenuItem[]);
   759  
   760      const resourceActions = getResourceActionsMenuItems(resource, application.metadata, apis);
   761  
   762      const links = !resource.uid
   763          ? Promise.resolve([])
   764          : services.applications
   765                .getResourceLinks(application.metadata.name, application.metadata.namespace, resource)
   766                .then(data => {
   767                    return (data.items || []).map(
   768                        link =>
   769                            ({
   770                                title: link.title,
   771                                iconClassName: `fa fa-fw ${link.iconClass ? link.iconClass : 'fa-external-link'}`,
   772                                action: () => window.open(link.url, '_blank'),
   773                                tooltip: link.description
   774                            }) as MenuItem
   775                    );
   776                })
   777                .catch(() => [] as MenuItem[]);
   778  
   779      return combineLatest(
   780          from([items]), // this resolves immediately
   781          concat([[] as MenuItem[]], logsAction), // this resolves at first to [] and then whatever the API returns
   782          concat([[] as MenuItem[]], resourceActions), // this resolves at first to [] and then whatever the API returns
   783          concat([[] as MenuItem[]], execAction), // this resolves at first to [] and then whatever the API returns
   784          concat([[] as MenuItem[]], links) // this resolves at first to [] and then whatever the API returns
   785      ).pipe(map(res => ([] as MenuItem[]).concat(...res)));
   786  }
   787  
   788  export function renderResourceMenu(
   789      resource: ResourceTreeNode,
   790      application: appModels.Application,
   791      tree: appModels.ApplicationTree,
   792      apis: ContextApis,
   793      appChanged: BehaviorSubject<appModels.Application>,
   794      getApplicationActionMenu: () => any
   795  ): React.ReactNode {
   796      let menuItems: Observable<ActionMenuItem[]>;
   797  
   798      if (isAppNode(resource) && resource.name === application.metadata.name) {
   799          menuItems = from([getApplicationActionMenu()]);
   800      } else {
   801          menuItems = getActionItems(resource, application, tree, apis, appChanged, false);
   802      }
   803      return (
   804          <DataLoader load={() => menuItems}>
   805              {items => (
   806                  <ul>
   807                      {items.map((item, i) => (
   808                          <li
   809                              className={classNames('application-details__action-menu', {disabled: item.disabled})}
   810                              tabIndex={item.disabled ? undefined : 0}
   811                              key={i}
   812                              onClick={e => {
   813                                  e.stopPropagation();
   814                                  if (!item.disabled) {
   815                                      item.action();
   816                                      document.body.click();
   817                                  }
   818                              }}
   819                              onKeyDown={e => {
   820                                  if (e.keyCode === 13 || e.key === 'Enter') {
   821                                      e.stopPropagation();
   822                                      setTimeout(() => {
   823                                          item.action();
   824                                          document.body.click();
   825                                      });
   826                                  }
   827                              }}>
   828                              {item.tooltip ? (
   829                                  <Tooltip content={item.tooltip || ''}>
   830                                      <div>
   831                                          {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   832                                      </div>
   833                                  </Tooltip>
   834                              ) : (
   835                                  <>
   836                                      {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   837                                  </>
   838                              )}
   839                          </li>
   840                      ))}
   841                  </ul>
   842              )}
   843          </DataLoader>
   844      );
   845  }
   846  
   847  export function renderResourceActionMenu(menuItems: ActionMenuItem[]): React.ReactNode {
   848      return (
   849          <ul>
   850              {menuItems.map((item, i) => (
   851                  <li
   852                      className={classNames('application-details__action-menu', {disabled: item.disabled})}
   853                      key={i}
   854                      onClick={e => {
   855                          e.stopPropagation();
   856                          if (!item.disabled) {
   857                              item.action();
   858                              document.body.click();
   859                          }
   860                      }}>
   861                      {item.iconClassName && <i className={item.iconClassName} />} {item.title}
   862                  </li>
   863              ))}
   864          </ul>
   865      );
   866  }
   867  
   868  export function renderResourceButtons(
   869      resource: ResourceTreeNode,
   870      application: appModels.Application,
   871      tree: appModels.ApplicationTree,
   872      apis: ContextApis,
   873      appChanged: BehaviorSubject<appModels.Application>
   874  ): React.ReactNode {
   875      const menuItems: Observable<ActionMenuItem[]> = getActionItems(resource, application, tree, apis, appChanged, true);
   876      return (
   877          <DataLoader load={() => menuItems}>
   878              {items => (
   879                  <div className='pod-view__node__quick-start-actions'>
   880                      {items.map((item, i) => (
   881                          <ActionButton
   882                              disabled={item.disabled}
   883                              key={i}
   884                              action={(e: React.MouseEvent) => {
   885                                  e.stopPropagation();
   886                                  if (!item.disabled) {
   887                                      item.action();
   888                                      document.body.click();
   889                                  }
   890                              }}
   891                              icon={item.iconClassName}
   892                              tooltip={item.title.toString().charAt(0).toUpperCase() + item.title.toString().slice(1)}
   893                          />
   894                      ))}
   895                  </div>
   896              )}
   897          </DataLoader>
   898      );
   899  }
   900  
   901  export function syncStatusMessage(app: appModels.Application) {
   902      const source = getAppDefaultSource(app);
   903      const revision = getAppDefaultSyncRevision(app);
   904      const rev = app.status.sync.revision || (source ? source.targetRevision || 'HEAD' : 'Unknown');
   905      let message = source ? source?.targetRevision || 'HEAD' : 'Unknown';
   906  
   907      if (revision && source) {
   908          if (source.chart) {
   909              message += ' (' + revision + ')';
   910          } else if (revision.length >= 7 && !revision.startsWith(source.targetRevision)) {
   911              if (source.repoURL.startsWith('oci://')) {
   912                  // Show "sha256: " plus the first 7 actual characters of the digest.
   913                  if (revision.startsWith('sha256:')) {
   914                      message += ' (' + revision.substring(0, 14) + ')';
   915                  } else {
   916                      message += ' (' + revision.substring(0, 7) + ')';
   917                  }
   918              } else {
   919                  message += ' (' + revision.substring(0, 7) + ')';
   920              }
   921          }
   922      }
   923  
   924      switch (app.status.sync.status) {
   925          case appModels.SyncStatuses.Synced:
   926              return (
   927                  <span>
   928                      to{' '}
   929                      <Revision repoUrl={source.repoURL} revision={rev}>
   930                          {message}
   931                      </Revision>
   932                      {getAppDefaultSyncRevisionExtra(app)}{' '}
   933                  </span>
   934              );
   935          case appModels.SyncStatuses.OutOfSync:
   936              return (
   937                  <span>
   938                      from{' '}
   939                      <Revision repoUrl={source.repoURL} revision={rev}>
   940                          {message}
   941                      </Revision>
   942                      {getAppDefaultSyncRevisionExtra(app)}{' '}
   943                  </span>
   944              );
   945          default:
   946              return <span>{message}</span>;
   947      }
   948  }
   949  
   950  export function hydrationStatusMessage(app: appModels.Application) {
   951      const drySource = app.status.sourceHydrator.currentOperation.sourceHydrator.drySource;
   952      const dryCommit = app.status.sourceHydrator.currentOperation.drySHA;
   953      const syncSource: ApplicationSource = {
   954          repoURL: drySource.repoURL,
   955          targetRevision:
   956              app.status.sourceHydrator.currentOperation.sourceHydrator.hydrateTo?.targetBranch || app.status.sourceHydrator.currentOperation.sourceHydrator.syncSource.targetBranch,
   957          path: app.status.sourceHydrator.currentOperation.sourceHydrator.syncSource.path
   958      };
   959      const hydratedCommit = app.status.sourceHydrator.currentOperation.hydratedSHA || '';
   960  
   961      switch (app.status.sourceHydrator.currentOperation.phase) {
   962          case appModels.HydrateOperationPhases.Hydrated:
   963              return (
   964                  <span>
   965                      from{' '}
   966                      <Revision repoUrl={drySource.repoURL} revision={dryCommit}>
   967                          {drySource.targetRevision + ' (' + dryCommit.substr(0, 7) + ')'}
   968                      </Revision>
   969                      <br />
   970                      to{' '}
   971                      <Revision repoUrl={syncSource.repoURL} revision={hydratedCommit}>
   972                          {syncSource.targetRevision + ' (' + hydratedCommit.substr(0, 7) + ')'}
   973                      </Revision>
   974                  </span>
   975              );
   976          case appModels.HydrateOperationPhases.Hydrating:
   977              return (
   978                  <span>
   979                      from{' '}
   980                      <Revision repoUrl={drySource.repoURL} revision={drySource.targetRevision}>
   981                          {drySource.targetRevision}
   982                      </Revision>
   983                      <br />
   984                      to{' '}
   985                      <Revision repoUrl={syncSource.repoURL} revision={syncSource.targetRevision}>
   986                          {syncSource.targetRevision}
   987                      </Revision>
   988                  </span>
   989              );
   990          case appModels.HydrateOperationPhases.Failed:
   991              return (
   992                  <span>
   993                      from{' '}
   994                      <Revision repoUrl={drySource.repoURL} revision={dryCommit || drySource.targetRevision}>
   995                          {drySource.targetRevision}
   996                          {dryCommit && ' (' + dryCommit.substr(0, 7) + ')'}
   997                      </Revision>
   998                      <br />
   999                      to{' '}
  1000                      <Revision repoUrl={syncSource.repoURL} revision={syncSource.targetRevision}>
  1001                          {syncSource.targetRevision}
  1002                      </Revision>
  1003                  </span>
  1004              );
  1005          default:
  1006              return <span>{}</span>;
  1007      }
  1008  }
  1009  
  1010  export const HealthStatusIcon = ({state, noSpin}: {state: appModels.HealthStatus; noSpin?: boolean}) => {
  1011      let color = COLORS.health.unknown;
  1012      let icon = 'fa-question-circle';
  1013  
  1014      switch (state.status) {
  1015          case appModels.HealthStatuses.Healthy:
  1016              color = COLORS.health.healthy;
  1017              icon = 'fa-heart';
  1018              break;
  1019          case appModels.HealthStatuses.Suspended:
  1020              color = COLORS.health.suspended;
  1021              icon = 'fa-pause-circle';
  1022              break;
  1023          case appModels.HealthStatuses.Degraded:
  1024              color = COLORS.health.degraded;
  1025              icon = 'fa-heart-broken';
  1026              break;
  1027          case appModels.HealthStatuses.Progressing:
  1028              color = COLORS.health.progressing;
  1029              icon = `fa fa-circle-notch ${noSpin ? '' : 'fa-spin'}`;
  1030              break;
  1031          case appModels.HealthStatuses.Missing:
  1032              color = COLORS.health.missing;
  1033              icon = 'fa-ghost';
  1034              break;
  1035      }
  1036      let title: string = state.status;
  1037      if (state.message) {
  1038          title = `${state.status}: ${state.message}`;
  1039      }
  1040      return icon.includes('fa-spin') ? (
  1041          <SpinningIcon color={color} qeId='utils-health-status-title' />
  1042      ) : (
  1043          <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon + ' utils-health-status-icon'} style={{color}} />
  1044      );
  1045  };
  1046  
  1047  export const PodHealthIcon = ({state}: {state: appModels.HealthStatus}) => {
  1048      let icon = 'fa-question-circle';
  1049  
  1050      switch (state.status) {
  1051          case appModels.HealthStatuses.Healthy:
  1052              icon = 'fa-check';
  1053              break;
  1054          case appModels.HealthStatuses.Suspended:
  1055              icon = 'fa-check';
  1056              break;
  1057          case appModels.HealthStatuses.Degraded:
  1058              icon = 'fa-times';
  1059              break;
  1060          case appModels.HealthStatuses.Progressing:
  1061              icon = 'fa fa-circle-notch fa-spin';
  1062              break;
  1063      }
  1064      let title: string = state.status;
  1065      if (state.message) {
  1066          title = `${state.status}: ${state.message}`;
  1067      }
  1068      return icon.includes('fa-spin') ? (
  1069          <SpinningIcon color={'white'} qeId='utils-health-status-title' />
  1070      ) : (
  1071          <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon} />
  1072      );
  1073  };
  1074  
  1075  export const PodPhaseIcon = ({state}: {state: appModels.PodPhase}) => {
  1076      let className = '';
  1077      switch (state) {
  1078          case appModels.PodPhase.PodSucceeded:
  1079              className = 'fa fa-check';
  1080              break;
  1081          case appModels.PodPhase.PodRunning:
  1082              className = 'fa fa-circle-notch fa-spin';
  1083              break;
  1084          case appModels.PodPhase.PodPending:
  1085              className = 'fa fa-circle-notch fa-spin';
  1086              break;
  1087          case appModels.PodPhase.PodFailed:
  1088              className = 'fa fa-times';
  1089              break;
  1090          default:
  1091              className = 'fa fa-question-circle';
  1092              break;
  1093      }
  1094      return className.includes('fa-spin') ? <SpinningIcon color={'white'} qeId='utils-pod-phase-icon' /> : <i qe-id='utils-pod-phase-icon' className={className} />;
  1095  };
  1096  
  1097  export const ResourceResultIcon = ({resource}: {resource: appModels.ResourceResult}) => {
  1098      let color = COLORS.sync_result.unknown;
  1099      let icon = 'fas fa-question-circle';
  1100  
  1101      if (!resource.hookType && resource.status) {
  1102          switch (resource.status) {
  1103              case appModels.ResultCodes.Synced:
  1104                  color = COLORS.sync_result.synced;
  1105                  icon = 'fa-heart';
  1106                  break;
  1107              case appModels.ResultCodes.Pruned:
  1108                  color = COLORS.sync_result.pruned;
  1109                  icon = 'fa-trash';
  1110                  break;
  1111              case appModels.ResultCodes.SyncFailed:
  1112                  color = COLORS.sync_result.failed;
  1113                  icon = 'fa-heart-broken';
  1114                  break;
  1115              case appModels.ResultCodes.PruneSkipped:
  1116                  icon = 'fa-heart';
  1117                  break;
  1118          }
  1119          let title: string = resource.message;
  1120          if (resource.message) {
  1121              title = `${resource.status}: ${resource.message}`;
  1122          }
  1123          return <i title={title} className={'fa ' + icon} style={{color}} />;
  1124      }
  1125      if (resource.hookType && resource.hookPhase) {
  1126          let className = '';
  1127          switch (resource.hookPhase) {
  1128              case appModels.OperationPhases.Running:
  1129                  color = COLORS.operation.running;
  1130                  className = 'fa fa-circle-notch fa-spin';
  1131                  break;
  1132              case appModels.OperationPhases.Failed:
  1133                  color = COLORS.operation.failed;
  1134                  className = 'fa fa-heart-broken';
  1135                  break;
  1136              case appModels.OperationPhases.Error:
  1137                  color = COLORS.operation.error;
  1138                  className = 'fa fa-heart-broken';
  1139                  break;
  1140              case appModels.OperationPhases.Succeeded:
  1141                  color = COLORS.operation.success;
  1142                  className = 'fa fa-heart';
  1143                  break;
  1144              case appModels.OperationPhases.Terminating:
  1145                  color = COLORS.operation.terminating;
  1146                  className = 'fa fa-circle-notch fa-spin';
  1147                  break;
  1148          }
  1149          let title: string = resource.message;
  1150          if (resource.message) {
  1151              title = `${resource.hookPhase}: ${resource.message}`;
  1152          }
  1153          return className.includes('fa-spin') ? <SpinningIcon color={color} qeId='utils-resource-result-icon' /> : <i title={title} className={className} style={{color}} />;
  1154      }
  1155      return null;
  1156  };
  1157  
  1158  export const getAppOperationState = (app: appModels.Application): appModels.OperationState => {
  1159      if (app.operation) {
  1160          return {
  1161              phase: appModels.OperationPhases.Running,
  1162              message: (app.status && app.status.operationState && app.status.operationState.message) || 'waiting to start',
  1163              startedAt: new Date().toISOString(),
  1164              operation: {
  1165                  sync: {}
  1166              }
  1167          } as appModels.OperationState;
  1168      } else if (app.metadata.deletionTimestamp) {
  1169          return {
  1170              phase: appModels.OperationPhases.Running,
  1171              startedAt: app.metadata.deletionTimestamp
  1172          } as appModels.OperationState;
  1173      } else {
  1174          return app.status.operationState;
  1175      }
  1176  };
  1177  
  1178  export function getOperationType(application: appModels.Application) {
  1179      const operation = application.operation || (application.status && application.status.operationState && application.status.operationState.operation);
  1180      if (application.metadata.deletionTimestamp && !application.operation) {
  1181          return 'Delete';
  1182      }
  1183      if (operation && operation.sync) {
  1184          return 'Sync';
  1185      }
  1186      return 'Unknown';
  1187  }
  1188  
  1189  const getOperationStateTitle = (app: appModels.Application) => {
  1190      const appOperationState = getAppOperationState(app);
  1191      const operationType = getOperationType(app);
  1192      switch (operationType) {
  1193          case 'Delete':
  1194              return 'Deleting';
  1195          case 'Sync':
  1196              switch (appOperationState.phase) {
  1197                  case 'Running':
  1198                      return 'Syncing';
  1199                  case 'Error':
  1200                      return 'Sync error';
  1201                  case 'Failed':
  1202                      return 'Sync failed';
  1203                  case 'Succeeded':
  1204                      return 'Sync OK';
  1205                  case 'Terminating':
  1206                      return 'Terminated';
  1207              }
  1208      }
  1209      return 'Unknown';
  1210  };
  1211  
  1212  export const OperationState = ({app, quiet, isButton}: {app: appModels.Application; quiet?: boolean; isButton?: boolean}) => {
  1213      const appOperationState = getAppOperationState(app);
  1214      if (appOperationState === undefined) {
  1215          return null;
  1216      }
  1217      if (quiet && [appModels.OperationPhases.Running, appModels.OperationPhases.Failed, appModels.OperationPhases.Error].indexOf(appOperationState.phase) === -1) {
  1218          return null;
  1219      }
  1220  
  1221      return (
  1222          <React.Fragment>
  1223              <OperationPhaseIcon app={app} isButton={isButton} /> {getOperationStateTitle(app)}
  1224          </React.Fragment>
  1225      );
  1226  };
  1227  
  1228  function isPodInitializedConditionTrue(status: any): boolean {
  1229      if (!status?.conditions) {
  1230          return false;
  1231      }
  1232  
  1233      for (const condition of status.conditions) {
  1234          if (condition.type !== 'Initialized') {
  1235              continue;
  1236          }
  1237          return condition.status === 'True';
  1238      }
  1239  
  1240      return false;
  1241  }
  1242  
  1243  // isPodPhaseTerminal returns true if the pod's phase is terminal.
  1244  function isPodPhaseTerminal(phase: appModels.PodPhase): boolean {
  1245      return phase === appModels.PodPhase.PodFailed || phase === appModels.PodPhase.PodSucceeded;
  1246  }
  1247  
  1248  export function getPodStateReason(pod: appModels.State): {message: string; reason: string; netContainerStatuses: any[]} {
  1249      if (!pod.status) {
  1250          return {reason: 'Unknown', message: '', netContainerStatuses: []};
  1251      }
  1252  
  1253      const podPhase = pod.status.phase;
  1254      let reason = podPhase;
  1255      let message = '';
  1256      if (pod.status.reason) {
  1257          reason = pod.status.reason;
  1258      }
  1259  
  1260      let netContainerStatuses = pod.status.initContainerStatuses || [];
  1261      netContainerStatuses = netContainerStatuses.concat(pod.status.containerStatuses || []);
  1262  
  1263      for (const condition of pod.status.conditions || []) {
  1264          if (condition.type === 'PodScheduled' && condition.reason === 'SchedulingGated') {
  1265              reason = 'SchedulingGated';
  1266          }
  1267      }
  1268  
  1269      const initContainers: Record<string, any> = {};
  1270  
  1271      for (const container of pod.spec.initContainers ?? []) {
  1272          initContainers[container.name] = container;
  1273      }
  1274  
  1275      let initializing = false;
  1276      const initContainerStatuses = pod.status.initContainerStatuses || [];
  1277      for (let i = 0; i < initContainerStatuses.length; i++) {
  1278          const container = initContainerStatuses[i];
  1279          if (container.state.terminated && container.state.terminated.exitCode === 0) {
  1280              continue;
  1281          }
  1282  
  1283          if (container.started && initContainers[container.name].restartPolicy === 'Always') {
  1284              continue;
  1285          }
  1286  
  1287          if (container.state.terminated) {
  1288              if (container.state.terminated.reason) {
  1289                  reason = `Init:ExitCode:${container.state.terminated.exitCode}`;
  1290              } else {
  1291                  reason = `Init:${container.state.terminated.reason}`;
  1292                  message = container.state.terminated.message;
  1293              }
  1294          } else if (container.state.waiting && container.state.waiting.reason && container.state.waiting.reason !== 'PodInitializing') {
  1295              reason = `Init:${container.state.waiting.reason}`;
  1296              message = `Init:${container.state.waiting.message}`;
  1297          } else {
  1298              reason = `Init:${i}/${(pod.spec.initContainers || []).length}`;
  1299          }
  1300          initializing = true;
  1301          break;
  1302      }
  1303  
  1304      if (!initializing || isPodInitializedConditionTrue(pod.status)) {
  1305          let hasRunning = false;
  1306          for (const container of pod.status.containerStatuses || []) {
  1307              if (container.state.waiting && container.state.waiting.reason) {
  1308                  reason = container.state.waiting.reason;
  1309                  message = container.state.waiting.message;
  1310              } else if (container.state.terminated && container.state.terminated.reason) {
  1311                  reason = container.state.terminated.reason;
  1312                  message = container.state.terminated.message;
  1313              } else if (container.state.terminated && !container.state.terminated.reason) {
  1314                  if (container.state.terminated.signal !== 0) {
  1315                      reason = `Signal:${container.state.terminated.signal}`;
  1316                      message = '';
  1317                  } else {
  1318                      reason = `ExitCode:${container.state.terminated.exitCode}`;
  1319                      message = '';
  1320                  }
  1321              } else if (container.ready && container.state.running) {
  1322                  hasRunning = true;
  1323              }
  1324          }
  1325  
  1326          // change pod status back to 'Running' if there is at least one container still reporting as 'Running' status
  1327          if (reason === 'Completed' && hasRunning) {
  1328              reason = 'Running';
  1329              message = '';
  1330          }
  1331      }
  1332  
  1333      if ((pod as any).metadata.deletionTimestamp && pod.status.reason === 'NodeLost') {
  1334          reason = 'Unknown';
  1335          message = '';
  1336      } else if ((pod as any).metadata.deletionTimestamp && !isPodPhaseTerminal(podPhase)) {
  1337          reason = 'Terminating';
  1338          message = '';
  1339      }
  1340  
  1341      return {reason, message, netContainerStatuses};
  1342  }
  1343  
  1344  export const getPodReadinessGatesState = (pod: appModels.State): {nonExistingConditions: string[]; notPassedConditions: string[]} => {
  1345      // if pod does not have readiness gates then return empty status
  1346      if (!pod.spec?.readinessGates?.length) {
  1347          return {
  1348              nonExistingConditions: [],
  1349              notPassedConditions: []
  1350          };
  1351      }
  1352  
  1353      const existingConditions = new Map<string, boolean>();
  1354      const podConditions = new Map<string, boolean>();
  1355  
  1356      const podStatusConditions = pod.status?.conditions || [];
  1357  
  1358      for (const condition of podStatusConditions) {
  1359          existingConditions.set(condition.type, true);
  1360          // priority order of conditions
  1361          // e.g. if there are multiple conditions set with same name then the one which comes first is evaluated
  1362          if (podConditions.has(condition.type)) {
  1363              continue;
  1364          }
  1365  
  1366          if (condition.status === 'False') {
  1367              podConditions.set(condition.type, false);
  1368          } else if (condition.status === 'True') {
  1369              podConditions.set(condition.type, true);
  1370          }
  1371      }
  1372  
  1373      const nonExistingConditions: string[] = [];
  1374      const failedConditions: string[] = [];
  1375  
  1376      const readinessGates: appModels.ReadinessGate[] = pod.spec?.readinessGates || [];
  1377  
  1378      for (const readinessGate of readinessGates) {
  1379          if (!existingConditions.has(readinessGate.conditionType)) {
  1380              nonExistingConditions.push(readinessGate.conditionType);
  1381          } else if (podConditions.get(readinessGate.conditionType) === false) {
  1382              failedConditions.push(readinessGate.conditionType);
  1383          }
  1384      }
  1385  
  1386      return {
  1387          nonExistingConditions,
  1388          notPassedConditions: failedConditions
  1389      };
  1390  };
  1391  
  1392  export function getConditionCategory(condition: appModels.ApplicationCondition): 'error' | 'warning' | 'info' {
  1393      if (condition.type.endsWith('Error')) {
  1394          return 'error';
  1395      } else if (condition.type.endsWith('Warning')) {
  1396          return 'warning';
  1397      } else {
  1398          return 'info';
  1399      }
  1400  }
  1401  
  1402  export function isAppNode(node: appModels.ResourceNode) {
  1403      return node.kind === 'Application' && node.group === 'argoproj.io';
  1404  }
  1405  
  1406  export function getAppOverridesCount(app: appModels.Application) {
  1407      const source = getAppDefaultSource(app);
  1408      if (source?.kustomize?.images) {
  1409          return source.kustomize.images.length;
  1410      }
  1411      if (source?.helm?.parameters) {
  1412          return source.helm.parameters.length;
  1413      }
  1414      return 0;
  1415  }
  1416  
  1417  // getAppDefaultSource gets the first app source from `sources` or, if that list is missing or empty, the `source`
  1418  // field.
  1419  export function getAppDefaultSource(app?: appModels.Application) {
  1420      if (!app) {
  1421          return null;
  1422      }
  1423      return getAppSpecDefaultSource(app.spec);
  1424  }
  1425  
  1426  // getAppDefaultSyncRevision gets the first app revisions from `status.sync.revisions` or, if that list is missing or empty, the `revision`
  1427  // field.
  1428  export function getAppDefaultSyncRevision(app?: appModels.Application) {
  1429      if (!app || !app.status || !app.status.sync) {
  1430          return '';
  1431      }
  1432      return app.status.sync.revisions && app.status.sync.revisions.length > 0 ? app.status.sync.revisions[0] : app.status.sync.revision;
  1433  }
  1434  
  1435  // getAppDefaultOperationSyncRevision gets the first app revisions from `status.operationState.syncResult.revisions` or, if that list is missing or empty, the `revision`
  1436  // field.
  1437  export function getAppDefaultOperationSyncRevision(app?: appModels.Application) {
  1438      if (!app || !app.status || !app.status.operationState || !app.status.operationState.syncResult) {
  1439          return '';
  1440      }
  1441      return app.status.operationState.syncResult.revisions && app.status.operationState.syncResult.revisions.length > 0
  1442          ? app.status.operationState.syncResult.revisions[0]
  1443          : app.status.operationState.syncResult.revision;
  1444  }
  1445  
  1446  // getAppCurrentVersion gets the first app revisions from `status.sync.revisions` or, if that list is missing or empty, the `revision`
  1447  // field.
  1448  export function getAppCurrentVersion(app?: appModels.Application): number | null {
  1449      if (!app || !app.status || !app.status.history || app.status.history.length === 0) {
  1450          return null;
  1451      }
  1452      return app.status.history[app.status.history.length - 1].id;
  1453  }
  1454  
  1455  // getAppDefaultSyncRevisionExtra gets the extra message with others revision count
  1456  export function getAppDefaultSyncRevisionExtra(app?: appModels.Application) {
  1457      if (!app || !app.status || !app.status.sync) {
  1458          return '';
  1459      }
  1460  
  1461      if (app.status.sync.revisions && app.status.sync.revisions.length > 0) {
  1462          return ` and (${app.status.sync.revisions.length - 1}) more`;
  1463      }
  1464  
  1465      return '';
  1466  }
  1467  
  1468  // getAppDefaultOperationSyncRevisionExtra gets the first app revisions from `status.operationState.syncResult.revisions` or, if that list is missing or empty, the `revision`
  1469  // field.
  1470  export function getAppDefaultOperationSyncRevisionExtra(app?: appModels.Application) {
  1471      if (!app || !app.status || !app.status.operationState || !app.status.operationState.syncResult || !app.status.operationState.syncResult.revisions) {
  1472          return '';
  1473      }
  1474  
  1475      if (app.status.operationState.syncResult.revisions.length > 0) {
  1476          return ` and (${app.status.operationState.syncResult.revisions.length - 1}) more`;
  1477      }
  1478      return '';
  1479  }
  1480  
  1481  export function getAppSpecDefaultSource(spec: appModels.ApplicationSpec) {
  1482      if (spec.sourceHydrator) {
  1483          return {
  1484              repoURL: spec.sourceHydrator.drySource.repoURL,
  1485              targetRevision: spec.sourceHydrator.syncSource.targetBranch,
  1486              path: spec.sourceHydrator.syncSource.path
  1487          };
  1488      }
  1489      return spec.sources && spec.sources.length > 0 ? spec.sources[0] : spec.source;
  1490  }
  1491  
  1492  export function isAppRefreshing(app: appModels.Application) {
  1493      return !!(app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]);
  1494  }
  1495  
  1496  export function setAppRefreshing(app: appModels.Application) {
  1497      if (!app.metadata.annotations) {
  1498          app.metadata.annotations = {};
  1499      }
  1500      if (!app.metadata.annotations[appModels.AnnotationRefreshKey]) {
  1501          app.metadata.annotations[appModels.AnnotationRefreshKey] = 'refreshing';
  1502      }
  1503  }
  1504  
  1505  export function refreshLinkAttrs(app: appModels.Application) {
  1506      return {disabled: isAppRefreshing(app)};
  1507  }
  1508  
  1509  export const SyncWindowStatusIcon = ({state, window}: {state: appModels.SyncWindowsState; window: appModels.SyncWindow}) => {
  1510      let className = '';
  1511      let color = '';
  1512      let current = '';
  1513  
  1514      if (state.windows === undefined) {
  1515          current = 'Inactive';
  1516      } else {
  1517          for (const w of state.windows) {
  1518              if (w.kind === window.kind && w.schedule === window.schedule && w.duration === window.duration && w.timeZone === window.timeZone) {
  1519                  current = 'Active';
  1520                  break;
  1521              } else {
  1522                  current = 'Inactive';
  1523              }
  1524          }
  1525      }
  1526  
  1527      switch (current + ':' + window.kind) {
  1528          case 'Active:deny':
  1529          case 'Inactive:allow':
  1530              className = 'fa fa-stop-circle';
  1531              if (window.manualSync) {
  1532                  color = COLORS.sync_window.manual;
  1533              } else {
  1534                  color = COLORS.sync_window.deny;
  1535              }
  1536              break;
  1537          case 'Active:allow':
  1538          case 'Inactive:deny':
  1539              className = 'fa fa-check-circle';
  1540              color = COLORS.sync_window.allow;
  1541              break;
  1542          default:
  1543              className = 'fas fa-question-circle';
  1544              color = COLORS.sync_window.unknown;
  1545              current = 'Unknown';
  1546              break;
  1547      }
  1548  
  1549      return (
  1550          <React.Fragment>
  1551              <i title={current} className={className} style={{color}} /> {current}
  1552          </React.Fragment>
  1553      );
  1554  };
  1555  
  1556  export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state?: appModels.ApplicationSyncWindowState}) => {
  1557      let className = '';
  1558      let color = '';
  1559      let deny = false;
  1560      let allow = false;
  1561      let inactiveAllow = false;
  1562      if (state?.assignedWindows !== undefined && state?.assignedWindows.length > 0) {
  1563          if (state.activeWindows !== undefined && state.activeWindows.length > 0) {
  1564              for (const w of state.activeWindows) {
  1565                  if (w.kind === 'deny') {
  1566                      deny = true;
  1567                  } else if (w.kind === 'allow') {
  1568                      allow = true;
  1569                  }
  1570              }
  1571          }
  1572          for (const a of state.assignedWindows) {
  1573              if (a.kind === 'allow') {
  1574                  inactiveAllow = true;
  1575              }
  1576          }
  1577      } else {
  1578          allow = true;
  1579      }
  1580  
  1581      if (deny || (!deny && !allow && inactiveAllow)) {
  1582          className = 'fa fa-stop-circle';
  1583          if (state.canSync) {
  1584              color = COLORS.sync_window.manual;
  1585          } else {
  1586              color = COLORS.sync_window.deny;
  1587          }
  1588      } else {
  1589          className = 'fa fa-check-circle';
  1590          color = COLORS.sync_window.allow;
  1591      }
  1592  
  1593      const ctx = React.useContext(Context);
  1594  
  1595      return (
  1596          <a href={`${ctx.baseHref}settings/projects/${project}?tab=windows`} style={{color}}>
  1597              <i className={className} style={{color}} /> SyncWindow
  1598          </a>
  1599      );
  1600  };
  1601  
  1602  /**
  1603   * Automatically stops and restarts the given observable when page visibility changes.
  1604   */
  1605  export function handlePageVisibility<T>(src: () => Observable<T>): Observable<T> {
  1606      return new Observable<T>((observer: Observer<T>) => {
  1607          let subscription: Subscription;
  1608          const ensureUnsubscribed = () => {
  1609              if (subscription) {
  1610                  subscription.unsubscribe();
  1611                  subscription = null;
  1612              }
  1613          };
  1614          const start = () => {
  1615              ensureUnsubscribed();
  1616              subscription = src().subscribe(
  1617                  (item: T) => observer.next(item),
  1618                  err => observer.error(err),
  1619                  () => observer.complete()
  1620              );
  1621          };
  1622  
  1623          if (!document.hidden) {
  1624              start();
  1625          }
  1626  
  1627          const visibilityChangeSubscription = fromEvent(document, 'visibilitychange')
  1628              // wait until user stop clicking back and forth to avoid restarting observable too often
  1629              .pipe(debounceTime(500))
  1630              .subscribe(() => {
  1631                  if (document.hidden && subscription) {
  1632                      ensureUnsubscribed();
  1633                  } else if (!document.hidden && !subscription) {
  1634                      start();
  1635                  }
  1636              });
  1637  
  1638          return () => {
  1639              visibilityChangeSubscription.unsubscribe();
  1640              ensureUnsubscribed();
  1641          };
  1642      });
  1643  }
  1644  
  1645  export function parseApiVersion(apiVersion: string): {group: string; version: string} {
  1646      const parts = apiVersion.split('/');
  1647      if (parts.length > 1) {
  1648          return {group: parts[0], version: parts[1]};
  1649      }
  1650      return {version: parts[0], group: ''};
  1651  }
  1652  
  1653  export function getContainerName(pod: any, containerIndex: number | null): string {
  1654      if (containerIndex == null && pod.metadata?.annotations?.['kubectl.kubernetes.io/default-container']) {
  1655          return pod.metadata?.annotations?.['kubectl.kubernetes.io/default-container'];
  1656      }
  1657      const containers = (pod.spec.containers || []).concat(pod.spec.initContainers || []);
  1658      const container = containers[containerIndex || 0];
  1659      return container.name;
  1660  }
  1661  
  1662  export function isYoungerThanXMinutes(pod: any, x: number): boolean {
  1663      const createdAt = moment(pod.createdAt, 'YYYY-MM-DDTHH:mm:ssZ');
  1664      const xMinutesAgo = moment().subtract(x, 'minutes');
  1665      return createdAt.isAfter(xMinutesAgo);
  1666  }
  1667  
  1668  export const BASE_COLORS = [
  1669      '#0DADEA', // blue
  1670      '#DE7EAE', // pink
  1671      '#FF9500', // orange
  1672      '#4B0082', // purple
  1673      '#F5d905', // yellow
  1674      '#964B00' // brown
  1675  ];
  1676  
  1677  export const urlPattern = new RegExp(
  1678      new RegExp(
  1679          // tslint:disable-next-line:max-line-length
  1680          /^(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,})$/,
  1681          'gi'
  1682      )
  1683  );
  1684  
  1685  export function appQualifiedName(app: appModels.Application, nsEnabled: boolean): string {
  1686      return (nsEnabled ? app.metadata.namespace + '/' : '') + app.metadata.name;
  1687  }
  1688  
  1689  export function appInstanceName(app: appModels.Application): string {
  1690      return app.metadata.namespace + '_' + app.metadata.name;
  1691  }
  1692  
  1693  export function formatCreationTimestamp(creationTimestamp: string) {
  1694      const createdAt = moment.utc(creationTimestamp).local().format('MM/DD/YYYY HH:mm:ss');
  1695      const fromNow = moment.utc(creationTimestamp).local().fromNow();
  1696      return (
  1697          <span>
  1698              {createdAt}
  1699              <i style={{padding: '2px'}} /> ({fromNow})
  1700          </span>
  1701      );
  1702  }
  1703  
  1704  /*
  1705   * formatStatefulSetChange reformats a single line describing changes to immutable fields in a StatefulSet.
  1706   * It extracts the field name and its "from" and "to" values for better readability.
  1707   */
  1708  function formatStatefulSetChange(line: string): string {
  1709      if (line.startsWith('-')) {
  1710          // Remove leading "- " from the line and split into field and changes
  1711          const [field, changes] = line.substring(2).split(':');
  1712          if (changes) {
  1713              // Split "from: X to: Y" into separate lines with aligned values
  1714              const [from, to] = changes.split('to:').map(s => s.trim());
  1715              return `   - ${field}:\n      from: ${from.replace('from:', '').trim()}\n      to:   ${to}`;
  1716          }
  1717      }
  1718      return line;
  1719  }
  1720  
  1721  export function formatOperationMessage(message: string): string {
  1722      if (!message) {
  1723          return message;
  1724      }
  1725  
  1726      // Format immutable fields error message
  1727      if (message.includes('attempting to change immutable fields:')) {
  1728          const [header, ...details] = message.split('\n');
  1729          const formattedDetails = details
  1730              // Remove empty lines
  1731              .filter(line => line.trim())
  1732              // Use helper function
  1733              .map(formatStatefulSetChange)
  1734              .join('\n');
  1735  
  1736          return `${header}\n${formattedDetails}`;
  1737      }
  1738  
  1739      return message;
  1740  }
  1741  
  1742  export const selectPostfix = (arr: string[], singular: string, plural: string) => (arr.length > 1 ? plural : singular);
  1743  
  1744  export function getUsrMsgKeyToDisplay(appName: string, msgKey: string, usrMessages: appModels.UserMessages[]) {
  1745      const usrMsg = usrMessages?.find((msg: appModels.UserMessages) => msg.appName === appName && msg.msgKey === msgKey);
  1746      if (usrMsg !== undefined) {
  1747          return {...usrMsg, display: true};
  1748      } else {
  1749          return {appName, msgKey, display: false, duration: 1} as appModels.UserMessages;
  1750      }
  1751  }
  1752  
  1753  export const userMsgsList: {[key: string]: string} = {
  1754      groupNodes: `Since the number of pods has surpassed the threshold pod count of 15, you will now be switched to the group node view.
  1755                   If you prefer the tree view, you can simply click on the Group Nodes toolbar button to deselect the current view.`
  1756  };
  1757  
  1758  export function getAppUrl(app: appModels.Application): string {
  1759      if (typeof app.metadata.namespace === 'undefined') {
  1760          return `applications/${app.metadata.name}`;
  1761      }
  1762      return `applications/${app.metadata.namespace}/${app.metadata.name}`;
  1763  }
  1764  
  1765  export const getProgressiveSyncStatusIcon = ({status, isButton}: {status: string; isButton?: boolean}) => {
  1766      const getIconProps = () => {
  1767          switch (status) {
  1768              case 'Healthy':
  1769                  return {icon: 'fa-check-circle', color: COLORS.health.healthy};
  1770              case 'Progressing':
  1771                  return {icon: 'fa-circle-notch fa-spin', color: COLORS.health.progressing};
  1772              case 'Pending':
  1773                  return {icon: 'fa-clock', color: COLORS.health.degraded};
  1774              case 'Waiting':
  1775                  return {icon: 'fa-clock', color: COLORS.sync.out_of_sync};
  1776              case 'Error':
  1777                  return {icon: 'fa-times-circle', color: COLORS.health.degraded};
  1778              case 'Synced':
  1779                  return {icon: 'fa-check-circle', color: COLORS.sync.synced};
  1780              case 'OutOfSync':
  1781                  return {icon: 'fa-exclamation-triangle', color: COLORS.sync.out_of_sync};
  1782              default:
  1783                  return {icon: 'fa-question-circle', color: COLORS.sync.unknown};
  1784          }
  1785      };
  1786  
  1787      const {icon, color} = getIconProps();
  1788      const className = `fa ${icon}${isButton ? ' application-status-panel__item-value__status-button' : ''}`;
  1789      return <i className={className} style={{color}} />;
  1790  };
  1791  
  1792  export const getProgressiveSyncStatusColor = (status: string): string => {
  1793      switch (status) {
  1794          case 'Waiting':
  1795              return COLORS.sync.out_of_sync;
  1796          case 'Pending':
  1797              return COLORS.health.degraded;
  1798          case 'Progressing':
  1799              return COLORS.health.progressing;
  1800          case 'Healthy':
  1801              return COLORS.health.healthy;
  1802          case 'Error':
  1803              return COLORS.health.degraded;
  1804          case 'Synced':
  1805              return COLORS.sync.synced;
  1806          case 'OutOfSync':
  1807              return COLORS.sync.out_of_sync;
  1808          default:
  1809              return COLORS.sync.unknown;
  1810      }
  1811  };
  1812  
  1813  // constant for podrequests
  1814  export const podRequests = {
  1815      CPU: 'Requests (CPU)',
  1816      MEMORY: 'Requests (MEM)'
  1817  } as const;
  1818  
  1819  export function formatResourceInfo(name: string, value: string): {displayValue: string; tooltipValue: string} {
  1820      const numValue = parseInt(value, 10);
  1821  
  1822      const formatCPUValue = (milliCpu: number): string => {
  1823          return milliCpu >= 1000 ? `${(milliCpu / 1000).toFixed(1)}` : `${milliCpu}m`;
  1824      };
  1825  
  1826      const formatMemoryValue = (milliBytes: number): string => {
  1827          const mib = Math.round(milliBytes / (1024 * 1024 * 1000));
  1828          return `${mib}Mi`;
  1829      };
  1830  
  1831      const formatCPUTooltip = (milliCpu: number): string => {
  1832          const displayValue = milliCpu >= 1000 ? `${(milliCpu / 1000).toFixed(1)} cores` : `${milliCpu}m`;
  1833          return `CPU Request: ${displayValue}`;
  1834      };
  1835  
  1836      const formatMemoryTooltip = (milliBytes: number): string => {
  1837          const mib = Math.round(milliBytes / (1024 * 1024 * 1000));
  1838          return `Memory Request: ${mib}Mi`;
  1839      };
  1840  
  1841      if (name === 'cpu') {
  1842          return {
  1843              displayValue: formatCPUValue(numValue),
  1844              tooltipValue: formatCPUTooltip(numValue)
  1845          };
  1846      } else if (name === 'memory') {
  1847          return {
  1848              displayValue: formatMemoryValue(numValue),
  1849              tooltipValue: formatMemoryTooltip(numValue)
  1850          };
  1851      }
  1852  
  1853      return {
  1854          displayValue: value,
  1855          tooltipValue: `${name}: ${value}`
  1856      };
  1857  }