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

     1  import {Checkbox, NotificationType} from 'argo-ui';
     2  import * as React from 'react';
     3  import {Observable, Observer, Subscription} from 'rxjs';
     4  
     5  import {COLORS, ErrorNotification, Revision} from '../../shared/components';
     6  import {ContextApis} from '../../shared/context';
     7  import * as appModels from '../../shared/models';
     8  import {services} from '../../shared/services';
     9  
    10  export interface NodeId {
    11      kind: string;
    12      namespace: string;
    13      name: string;
    14      group: string;
    15  }
    16  
    17  export function nodeKey(node: NodeId) {
    18      return [node.group, node.kind, node.namespace, node.name].join('/');
    19  }
    20  
    21  export function isSameNode(first: NodeId, second: NodeId) {
    22      return nodeKey(first) === nodeKey(second);
    23  }
    24  
    25  export async function deleteApplication(appName: string, apis: ContextApis): Promise<boolean> {
    26      let cascade = false;
    27      const confirmationForm = class extends React.Component<{}, {cascade: boolean}> {
    28          constructor(props: any) {
    29              super(props);
    30              this.state = {cascade: true};
    31          }
    32  
    33          public render() {
    34              return (
    35                  <div>
    36                      <p>Are you sure you want to delete the application '{appName}'?</p>
    37                      <p>
    38                          <Checkbox checked={this.state.cascade} onChange={val => this.setState({cascade: val})} /> Cascade
    39                      </p>
    40                  </div>
    41              );
    42          }
    43  
    44          public componentWillUnmount() {
    45              cascade = this.state.cascade;
    46          }
    47      };
    48      const confirmed = await apis.popup.confirm('Delete application', confirmationForm);
    49      if (confirmed) {
    50          try {
    51              await services.applications.delete(appName, cascade);
    52              return true;
    53          } catch (e) {
    54              apis.notifications.show({
    55                  content: <ErrorNotification title='Unable to delete application' e={e} />,
    56                  type: NotificationType.Error
    57              });
    58          }
    59      }
    60      return false;
    61  }
    62  
    63  export const OperationPhaseIcon = ({app}: {app: appModels.Application}) => {
    64      const operationState = getAppOperationState(app);
    65      if (operationState === undefined) {
    66          return <React.Fragment />;
    67      }
    68      let className = '';
    69      let color = '';
    70      switch (operationState.phase) {
    71          case appModels.OperationPhases.Succeeded:
    72              className = 'fa fa-check-circle';
    73              color = COLORS.operation.success;
    74              break;
    75          case appModels.OperationPhases.Error:
    76              className = 'fa fa-times-circle';
    77              color = COLORS.operation.error;
    78              break;
    79          case appModels.OperationPhases.Failed:
    80              className = 'fa fa-times-circle';
    81              color = COLORS.operation.failed;
    82              break;
    83          default:
    84              className = 'fa fa-circle-notch fa-spin';
    85              color = COLORS.operation.running;
    86              break;
    87      }
    88      return <i title={getOperationStateTitle(app)} qe-id='utils-operations-status-title' className={className} style={{color}} />;
    89  };
    90  
    91  export const ComparisonStatusIcon = ({status, resource, label}: {status: appModels.SyncStatusCode; resource?: {requiresPruning?: boolean}; label?: boolean}) => {
    92      let className = 'fa fa-ghost';
    93      let color = COLORS.sync.unknown;
    94      let title: string = 'Unknown';
    95  
    96      switch (status) {
    97          case appModels.SyncStatuses.Synced:
    98              className = 'fa fa-check-circle';
    99              color = COLORS.sync.synced;
   100              title = 'Synced';
   101              break;
   102          case appModels.SyncStatuses.OutOfSync:
   103              const requiresPruning = resource && resource.requiresPruning;
   104              className = requiresPruning ? 'fa fa-times-circle' : 'fa fa-arrow-alt-circle-up';
   105              title = 'OutOfSync';
   106              if (requiresPruning) {
   107                  title = `${title} (requires pruning)`;
   108              }
   109              color = COLORS.sync.out_of_sync;
   110              break;
   111          case appModels.SyncStatuses.Unknown:
   112              className = 'fa fa-circle-notch fa-spin';
   113              break;
   114      }
   115      return (
   116          <React.Fragment>
   117              <i qe-id='utils-sync-status-title' title={title} className={className} style={{color}} /> {label && title}
   118          </React.Fragment>
   119      );
   120  };
   121  
   122  export function syncStatusMessage(app: appModels.Application) {
   123      const rev = app.status.sync.revision || app.spec.source.targetRevision || 'HEAD';
   124      let message = app.spec.source.targetRevision || 'HEAD';
   125      if (app.status.sync.revision) {
   126          if (app.spec.source.chart) {
   127              message += ' (' + app.status.sync.revision + ')';
   128          } else if (app.status.sync.revision.length >= 7 && !app.status.sync.revision.startsWith(app.spec.source.targetRevision)) {
   129              message += ' (' + app.status.sync.revision.substr(0, 7) + ')';
   130          }
   131      }
   132      switch (app.status.sync.status) {
   133          case appModels.SyncStatuses.Synced:
   134              return (
   135                  <span>
   136                      To{' '}
   137                      <Revision repoUrl={app.spec.source.repoURL} revision={rev}>
   138                          {message}
   139                      </Revision>{' '}
   140                  </span>
   141              );
   142          case appModels.SyncStatuses.OutOfSync:
   143              return (
   144                  <span>
   145                      From{' '}
   146                      <Revision repoUrl={app.spec.source.repoURL} revision={rev}>
   147                          {message}
   148                      </Revision>{' '}
   149                  </span>
   150              );
   151          default:
   152              return <span>{message}</span>;
   153      }
   154  }
   155  
   156  export const HealthStatusIcon = ({state}: {state: appModels.HealthStatus}) => {
   157      let color = COLORS.health.unknown;
   158      let icon = 'fa-ghost';
   159  
   160      switch (state.status) {
   161          case appModels.HealthStatuses.Healthy:
   162              color = COLORS.health.healthy;
   163              icon = 'fa-heart';
   164              break;
   165          case appModels.HealthStatuses.Suspended:
   166              color = COLORS.health.suspended;
   167              icon = 'fa-heart';
   168              break;
   169          case appModels.HealthStatuses.Degraded:
   170              color = COLORS.health.degraded;
   171              icon = 'fa-heart-broken';
   172              break;
   173          case appModels.HealthStatuses.Progressing:
   174              color = COLORS.health.progressing;
   175              icon = 'fa fa-circle-notch fa-spin';
   176              break;
   177      }
   178      let title: string = state.status;
   179      if (state.message) {
   180          title = `${state.status}: ${state.message};`;
   181      }
   182      return <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon} style={{color}} />;
   183  };
   184  
   185  export const ResourceResultIcon = ({resource}: {resource: appModels.ResourceResult}) => {
   186      let color = COLORS.sync_result.unknown;
   187      let icon = 'fa-ghost';
   188  
   189      if (!resource.hookType && resource.status) {
   190          switch (resource.status) {
   191              case appModels.ResultCodes.Synced:
   192                  color = COLORS.sync_result.synced;
   193                  icon = 'fa-heart';
   194                  break;
   195              case appModels.ResultCodes.Pruned:
   196                  color = COLORS.sync_result.pruned;
   197                  icon = 'fa-heart';
   198                  break;
   199              case appModels.ResultCodes.SyncFailed:
   200                  color = COLORS.sync_result.failed;
   201                  icon = 'fa-heart-broken';
   202                  break;
   203              case appModels.ResultCodes.PruneSkipped:
   204                  icon = 'fa-heart';
   205                  break;
   206          }
   207          let title: string = resource.message;
   208          if (resource.message) {
   209              title = `${resource.status}: ${resource.message}`;
   210          }
   211          return <i title={title} className={'fa ' + icon} style={{color}} />;
   212      }
   213      if (resource.hookType && resource.hookPhase) {
   214          let className = '';
   215          switch (resource.hookPhase) {
   216              case appModels.OperationPhases.Running:
   217                  color = COLORS.operation.running;
   218                  className = 'fa fa-circle-notch fa-spin';
   219                  break;
   220              case appModels.OperationPhases.Failed:
   221                  color = COLORS.operation.failed;
   222                  className = 'fa fa-heart-broken';
   223                  break;
   224              case appModels.OperationPhases.Error:
   225                  color = COLORS.operation.error;
   226                  className = 'fa fa-heart-broken';
   227                  break;
   228              case appModels.OperationPhases.Succeeded:
   229                  color = COLORS.operation.success;
   230                  className = 'fa fa-heart';
   231                  break;
   232              case appModels.OperationPhases.Terminating:
   233                  color = COLORS.operation.terminating;
   234                  className = 'fa fa-circle-notch fa-spin';
   235                  break;
   236          }
   237          let title: string = resource.message;
   238          if (resource.message) {
   239              title = `${resource.hookPhase}: ${resource.message};`;
   240          }
   241          return <i title={title} className={className} style={{color}} />;
   242      }
   243      return null;
   244  };
   245  
   246  export const getAppOperationState = (app: appModels.Application): appModels.OperationState => {
   247      if (app.metadata.deletionTimestamp) {
   248          return {
   249              phase: appModels.OperationPhases.Running,
   250              startedAt: app.metadata.deletionTimestamp
   251          } as appModels.OperationState;
   252      } else if (app.operation) {
   253          return {
   254              phase: appModels.OperationPhases.Running,
   255              message: (app.status && app.status.operationState && app.status.operationState.message) || 'waiting to start',
   256              startedAt: new Date().toISOString(),
   257              operation: {
   258                  sync: {}
   259              }
   260          } as appModels.OperationState;
   261      } else {
   262          return app.status.operationState;
   263      }
   264  };
   265  
   266  export function getOperationType(application: appModels.Application) {
   267      if (application.metadata.deletionTimestamp) {
   268          return 'Delete';
   269      }
   270      const operation = application.operation || (application.status.operationState && application.status.operationState.operation);
   271      if (operation && operation.sync) {
   272          return 'Sync';
   273      }
   274      return 'Unknown';
   275  }
   276  
   277  const getOperationStateTitle = (app: appModels.Application) => {
   278      const appOperationState = getAppOperationState(app);
   279      const operationType = getOperationType(app);
   280      switch (operationType) {
   281          case 'Delete':
   282              return 'Deleting';
   283          case 'Sync':
   284              switch (appOperationState.phase) {
   285                  case 'Running':
   286                      return 'Syncing';
   287                  case 'Error':
   288                      return 'Sync error';
   289                  case 'Failed':
   290                      return 'Sync failed';
   291                  case 'Succeeded':
   292                      return 'Sync OK';
   293                  case 'Terminating':
   294                      return 'Terminated';
   295              }
   296      }
   297      return 'Unknown';
   298  };
   299  
   300  export const OperationState = ({app, quiet}: {app: appModels.Application; quiet?: boolean}) => {
   301      const appOperationState = getAppOperationState(app);
   302      if (appOperationState === undefined) {
   303          return <React.Fragment />;
   304      }
   305      if (quiet && [appModels.OperationPhases.Running, appModels.OperationPhases.Failed, appModels.OperationPhases.Error].indexOf(appOperationState.phase) === -1) {
   306          return <React.Fragment />;
   307      }
   308  
   309      return (
   310          <React.Fragment>
   311              <OperationPhaseIcon app={app} /> {getOperationStateTitle(app)}
   312          </React.Fragment>
   313      );
   314  };
   315  
   316  export function getPodStateReason(pod: appModels.State): {message: string; reason: string} {
   317      let reason = pod.status.phase;
   318      let message = '';
   319      if (pod.status.reason) {
   320          reason = pod.status.reason;
   321      }
   322  
   323      let initializing = false;
   324      for (const container of (pod.status.initContainerStatuses || []).slice().reverse()) {
   325          if (container.state.terminated && container.state.terminated.exitCode === 0) {
   326              continue;
   327          }
   328  
   329          if (container.state.terminated) {
   330              if (container.state.terminated.reason) {
   331                  reason = `Init:ExitCode:${container.state.terminated.exitCode}`;
   332              } else {
   333                  reason = `Init:${container.state.terminated.reason}`;
   334                  message = container.state.terminated.message;
   335              }
   336          } else if (container.state.waiting && container.state.waiting.reason && container.state.waiting.reason !== 'PodInitializing') {
   337              reason = `Init:${container.state.waiting.reason}`;
   338              message = `Init:${container.state.waiting.message}`;
   339          } else {
   340              reason = `Init: ${(pod.spec.initContainers || []).length})`;
   341          }
   342          initializing = true;
   343          break;
   344      }
   345  
   346      if (!initializing) {
   347          let hasRunning = false;
   348          for (const container of pod.status.containerStatuses || []) {
   349              if (container.state.waiting && container.state.waiting.reason) {
   350                  reason = container.state.waiting.reason;
   351                  message = container.state.waiting.message;
   352              } else if (container.state.terminated && container.state.terminated.reason) {
   353                  reason = container.state.terminated.reason;
   354                  message = container.state.terminated.message;
   355              } else if (container.state.terminated && container.state.terminated.reason) {
   356                  if (container.state.terminated.signal !== 0) {
   357                      reason = `Signal:${container.state.terminated.signal}`;
   358                      message = '';
   359                  } else {
   360                      reason = `ExitCode:${container.state.terminated.exitCode}`;
   361                      message = '';
   362                  }
   363              } else if (container.ready && container.state.running) {
   364                  hasRunning = true;
   365              }
   366          }
   367  
   368          // change pod status back to 'Running' if there is at least one container still reporting as 'Running' status
   369          if (reason === 'Completed' && hasRunning) {
   370              reason = 'Running';
   371              message = '';
   372          }
   373      }
   374  
   375      if ((pod as any).metadata.deletionTimestamp && pod.status.reason === 'NodeLost') {
   376          reason = 'Unknown';
   377          message = '';
   378      } else if ((pod as any).metadata.deletionTimestamp) {
   379          reason = 'Terminating';
   380          message = '';
   381      }
   382  
   383      return {reason, message};
   384  }
   385  
   386  export function getConditionCategory(condition: appModels.ApplicationCondition): 'error' | 'warning' | 'info' {
   387      if (condition.type.endsWith('Error')) {
   388          return 'error';
   389      } else if (condition.type.endsWith('Warning')) {
   390          return 'warning';
   391      } else {
   392          return 'info';
   393      }
   394  }
   395  
   396  export function isAppNode(node: appModels.ResourceNode) {
   397      return node.kind === 'Application' && node.group === 'argoproj.io';
   398  }
   399  
   400  export function getAppOverridesCount(app: appModels.Application) {
   401      if (app.spec.source.ksonnet && app.spec.source.ksonnet.parameters) {
   402          return app.spec.source.ksonnet.parameters.length;
   403      }
   404      if (app.spec.source.kustomize && app.spec.source.kustomize.images) {
   405          return app.spec.source.kustomize.images.length;
   406      }
   407      if (app.spec.source.helm && app.spec.source.helm.parameters) {
   408          return app.spec.source.helm.parameters.length;
   409      }
   410      return 0;
   411  }
   412  
   413  export function isAppRefreshing(app: appModels.Application) {
   414      return !!(app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]);
   415  }
   416  
   417  export function setAppRefreshing(app: appModels.Application) {
   418      if (!app.metadata.annotations) {
   419          app.metadata.annotations = {};
   420      }
   421      if (!app.metadata.annotations[appModels.AnnotationRefreshKey]) {
   422          app.metadata.annotations[appModels.AnnotationRefreshKey] = 'refreshing';
   423      }
   424  }
   425  
   426  export function refreshLinkAttrs(app: appModels.Application) {
   427      return {disabled: isAppRefreshing(app)};
   428  }
   429  
   430  export const SyncWindowStatusIcon = ({state, window}: {state: appModels.SyncWindowsState; window: appModels.SyncWindow}) => {
   431      let className = '';
   432      let color = '';
   433      let current = '';
   434  
   435      if (state.windows === undefined) {
   436          current = 'Inactive';
   437      } else {
   438          for (const w of state.windows) {
   439              if (w.kind === window.kind && w.schedule === window.schedule && w.duration === window.duration) {
   440                  current = 'Active';
   441                  break;
   442              } else {
   443                  current = 'Inactive';
   444              }
   445          }
   446      }
   447  
   448      switch (current + ':' + window.kind) {
   449          case 'Active:deny':
   450          case 'Inactive:allow':
   451              className = 'fa fa-stop-circle';
   452              if (window.manualSync) {
   453                  color = COLORS.sync_window.manual;
   454              } else {
   455                  color = COLORS.sync_window.deny;
   456              }
   457              break;
   458          case 'Active:allow':
   459          case 'Inactive:deny':
   460              className = 'fa fa-check-circle';
   461              color = COLORS.sync_window.allow;
   462              break;
   463          default:
   464              className = 'fa fa-ghost';
   465              color = COLORS.sync_window.unknown;
   466              current = 'Unknown';
   467              break;
   468      }
   469  
   470      return (
   471          <React.Fragment>
   472              <i title={current} className={className} style={{color}} /> {current}
   473          </React.Fragment>
   474      );
   475  };
   476  
   477  export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state: appModels.ApplicationSyncWindowState}) => {
   478      let className = '';
   479      let color = '';
   480      let deny = false;
   481      let allow = false;
   482      let inactiveAllow = false;
   483      if (state.assignedWindows !== undefined && state.assignedWindows.length > 0) {
   484          if (state.activeWindows !== undefined && state.activeWindows.length > 0) {
   485              for (const w of state.activeWindows) {
   486                  if (w.kind === 'deny') {
   487                      deny = true;
   488                  } else if (w.kind === 'allow') {
   489                      allow = true;
   490                  }
   491              }
   492          }
   493          for (const a of state.assignedWindows) {
   494              if (a.kind === 'allow') {
   495                  inactiveAllow = true;
   496              }
   497          }
   498      } else {
   499          allow = true;
   500      }
   501  
   502      if (deny || (!deny && !allow && inactiveAllow)) {
   503          className = 'fa fa-stop-circle';
   504          if (state.canSync) {
   505              color = COLORS.sync_window.manual;
   506          } else {
   507              color = COLORS.sync_window.deny;
   508          }
   509      } else {
   510          className = 'fa fa-check-circle';
   511          color = COLORS.sync_window.allow;
   512      }
   513  
   514      return (
   515          <a href={`/settings/projects/${project}?tab=windows`} style={{color}}>
   516              <i className={className} style={{color}} /> SyncWindow
   517          </a>
   518      );
   519  };
   520  
   521  /**
   522   * Automatically stops and restarts the given observable when page visibility changes.
   523   */
   524  export function handlePageVisibility<T>(src: () => Observable<T>): Observable<T> {
   525      return new Observable<T>((observer: Observer<T>) => {
   526          let subscription: Subscription;
   527          const ensureUnsubscribed = () => {
   528              if (subscription) {
   529                  subscription.unsubscribe();
   530                  subscription = null;
   531              }
   532          };
   533          const start = () => {
   534              ensureUnsubscribed();
   535              subscription = src().subscribe((item: T) => observer.next(item), err => observer.error(err), () => observer.complete());
   536          };
   537  
   538          if (!document.hidden) {
   539              start();
   540          }
   541  
   542          const visibilityChangeSubscription = Observable.fromEvent(document, 'visibilitychange')
   543              // wait until user stop clicking back and forth to avoid restarting observable too often
   544              .debounceTime(500)
   545              .subscribe(() => {
   546                  if (document.hidden && subscription) {
   547                      ensureUnsubscribed();
   548                  } else if (!document.hidden && !subscription) {
   549                      start();
   550                  }
   551              });
   552  
   553          return () => {
   554              visibilityChangeSubscription.unsubscribe();
   555              ensureUnsubscribed();
   556          };
   557      });
   558  }
   559  
   560  export function parseApiVersion(apiVersion: string): {group: string; version: string} {
   561      const parts = apiVersion.split('/');
   562      if (parts.length > 1) {
   563          return {group: parts[0], version: parts[1]};
   564      }
   565      return {version: parts[0], group: ''};
   566  }