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

     1  import {HelpIcon} from 'argo-ui';
     2  import * as React from 'react';
     3  import {ARGO_GRAY6_COLOR, DataLoader} from '../../../shared/components';
     4  import {Revision} from '../../../shared/components/revision';
     5  import {Timestamp} from '../../../shared/components/timestamp';
     6  import * as models from '../../../shared/models';
     7  import {services} from '../../../shared/services';
     8  import {
     9      ApplicationSyncWindowStatusIcon,
    10      ComparisonStatusIcon,
    11      getAppDefaultSource,
    12      getAppDefaultSyncRevisionExtra,
    13      getAppOperationState,
    14      HydrateOperationPhaseIcon,
    15      hydrationStatusMessage,
    16      getProgressiveSyncStatusColor,
    17      getProgressiveSyncStatusIcon
    18  } from '../utils';
    19  import {getConditionCategory, HealthStatusIcon, OperationState, syncStatusMessage, getAppDefaultSyncRevision, getAppDefaultOperationSyncRevision} from '../utils';
    20  import {RevisionMetadataPanel} from './revision-metadata-panel';
    21  import * as utils from '../utils';
    22  import {COLORS} from '../../../shared/components/colors';
    23  
    24  import './application-status-panel.scss';
    25  
    26  interface Props {
    27      application: models.Application;
    28      showDiff?: () => any;
    29      showOperation?: () => any;
    30      showHydrateOperation?: () => any;
    31      showConditions?: () => any;
    32      showExtension?: (id: string) => any;
    33      showMetadataInfo?: (revision: string) => any;
    34  }
    35  
    36  interface SectionInfo {
    37      title: string;
    38      helpContent?: string;
    39  }
    40  
    41  const sectionLabel = (info: SectionInfo) => (
    42      <label style={{fontSize: '12px', fontWeight: 600, color: ARGO_GRAY6_COLOR}}>
    43          {info.title}
    44          {info.helpContent && <HelpIcon title={info.helpContent} />}
    45      </label>
    46  );
    47  
    48  const sectionHeader = (info: SectionInfo, onClick?: () => any) => {
    49      return (
    50          <div style={{display: 'flex', alignItems: 'center', marginBottom: '0.5em'}}>
    51              {sectionLabel(info)}
    52              {onClick && (
    53                  <button className='argo-button application-status-panel__more-button' onClick={onClick}>
    54                      <i className='fa fa-ellipsis-h' />
    55                  </button>
    56              )}
    57          </div>
    58      );
    59  };
    60  
    61  const getApplicationSetOwnerRef = (application: models.Application) => {
    62      return application.metadata.ownerReferences?.find(ref => ref.kind === 'ApplicationSet');
    63  };
    64  
    65  const ProgressiveSyncStatus = ({application}: {application: models.Application}) => {
    66      const appSetRef = getApplicationSetOwnerRef(application);
    67      if (!appSetRef) {
    68          return null;
    69      }
    70  
    71      return (
    72          <DataLoader
    73              input={application}
    74              errorRenderer={() => {
    75                  // For any errors, show a minimal error state
    76                  return (
    77                      <div className='application-status-panel__item'>
    78                          {sectionHeader({
    79                              title: 'PROGRESSIVE SYNC',
    80                              helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet.'
    81                          })}
    82                          <div className='application-status-panel__item-value'>
    83                              <i className='fa fa-exclamation-triangle' style={{color: COLORS.sync.unknown}} /> Error
    84                          </div>
    85                          <div className='application-status-panel__item-name'>Unable to load Progressive Sync status</div>
    86                      </div>
    87                  );
    88              }}
    89              load={async () => {
    90                  // Find ApplicationSet by searching all namespaces dynamically
    91                  const appSetList = await services.applications.listApplicationSets();
    92                  const appSet = appSetList.items?.find(item => item.metadata.name === appSetRef.name);
    93  
    94                  return {appSet};
    95              }}>
    96              {({appSet}: {appSet: models.ApplicationSet}) => {
    97                  // Hide panel if: Progressive Sync disabled, no permission, or not RollingSync strategy
    98                  if (!appSet || !appSet.status?.applicationStatus || appSet?.spec?.strategy?.type !== 'RollingSync') {
    99                      return null;
   100                  }
   101  
   102                  // Get the current application's status from the ApplicationSet applicationStatus
   103                  const appResource = appSet.status?.applicationStatus?.find(status => status.application === application.metadata.name);
   104  
   105                  // If no application status is found, show a default status
   106                  if (!appResource) {
   107                      return (
   108                          <div className='application-status-panel__item'>
   109                              {sectionHeader({
   110                                  title: 'PROGRESSIVE SYNC',
   111                                  helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.'
   112                              })}
   113                              <div className='application-status-panel__item-value'>
   114                                  <i className='fa fa-clock' style={{color: COLORS.sync.out_of_sync}} /> Waiting
   115                              </div>
   116                              <div className='application-status-panel__item-name'>Application status not yet available from ApplicationSet</div>
   117                          </div>
   118                      );
   119                  }
   120  
   121                  // Get last transition time from application status
   122                  const lastTransitionTime = appResource?.lastTransitionTime;
   123  
   124                  return (
   125                      <div className='application-status-panel__item'>
   126                          {sectionHeader({
   127                              title: 'PROGRESSIVE SYNC',
   128                              helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.'
   129                          })}
   130                          <div className='application-status-panel__item-value' style={{color: getProgressiveSyncStatusColor(appResource.status)}}>
   131                              {getProgressiveSyncStatusIcon({status: appResource.status})}&nbsp;{appResource.status}
   132                          </div>
   133                          {appResource?.step && <div className='application-status-panel__item-value'>Wave: {appResource.step}</div>}
   134                          {lastTransitionTime && (
   135                              <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
   136                                  Last Transition: <br />
   137                                  <Timestamp date={lastTransitionTime} />
   138                              </div>
   139                          )}
   140                          {appResource?.message && <div className='application-status-panel__item-name'>{appResource.message}</div>}
   141                      </div>
   142                  );
   143              }}
   144          </DataLoader>
   145      );
   146  };
   147  
   148  export const ApplicationStatusPanel = ({application, showDiff, showOperation, showHydrateOperation, showConditions, showExtension, showMetadataInfo}: Props) => {
   149      const [showProgressiveSync, setShowProgressiveSync] = React.useState(false);
   150  
   151      React.useEffect(() => {
   152          // Only show Progressive Sync if the application has an ApplicationSet parent
   153          // The actual strategy validation will be done inside ProgressiveSyncStatus component
   154          setShowProgressiveSync(!!getApplicationSetOwnerRef(application));
   155      }, [application]);
   156  
   157      const today = new Date();
   158  
   159      let daysSinceLastSynchronized = 0;
   160      const history = application.status.history || [];
   161      if (history.length > 0) {
   162          const deployDate = new Date(history[history.length - 1].deployedAt);
   163          daysSinceLastSynchronized = Math.round(Math.abs((today.getTime() - deployDate.getTime()) / (24 * 60 * 60 * 1000)));
   164      }
   165      const cntByCategory = (application.status.conditions || []).reduce(
   166          (map, next) => map.set(getConditionCategory(next), (map.get(getConditionCategory(next)) || 0) + 1),
   167          new Map<string, number>()
   168      );
   169      const appOperationState = getAppOperationState(application);
   170      if (application.metadata.deletionTimestamp && !appOperationState) {
   171          showOperation = null;
   172      }
   173  
   174      const statusExtensions = services.extensions.getStatusPanelExtensions();
   175  
   176      const revision = getAppDefaultSyncRevision(application);
   177      const operationStateRevision = getAppDefaultOperationSyncRevision(application);
   178      const infos = cntByCategory.get('info');
   179      const warnings = cntByCategory.get('warning');
   180      const errors = cntByCategory.get('error');
   181      const source = getAppDefaultSource(application);
   182      const hasMultipleSources = application.spec.sources?.length > 0;
   183      const revisionType = source?.repoURL?.startsWith('oci://') ? 'oci' : source?.chart ? 'helm' : 'git';
   184      return (
   185          <div className='application-status-panel row'>
   186              <div className='application-status-panel__item'>
   187                  <div style={{lineHeight: '19.5px', marginBottom: '0.3em'}}>{sectionLabel({title: 'APP HEALTH', helpContent: 'The health status of your app'})}</div>
   188                  <div className='application-status-panel__item-value'>
   189                      <HealthStatusIcon state={application.status.health} />
   190                      &nbsp;
   191                      {application.status.health.status}
   192                  </div>
   193                  {application.status.health.message && <div className='application-status-panel__item-name'>{application.status.health.message}</div>}
   194              </div>
   195              {application.spec.sourceHydrator && application.status?.sourceHydrator?.currentOperation && (
   196                  <div className='application-status-panel__item'>
   197                      <div style={{lineHeight: '19.5px', marginBottom: '0.3em'}}>
   198                          {sectionLabel({
   199                              title: 'SOURCE HYDRATOR',
   200                              helpContent: 'The source hydrator reads manifests from git, hydrates (renders) them, and pushes them to a different location in git.'
   201                          })}
   202                      </div>
   203                      <div className='application-status-panel__item-value'>
   204                          <a className='application-status-panel__item-value__hydrator-link' onClick={() => showHydrateOperation && showHydrateOperation()}>
   205                              <HydrateOperationPhaseIcon operationState={application.status.sourceHydrator.currentOperation} isButton={true} />
   206                              &nbsp;
   207                              {application.status.sourceHydrator.currentOperation.phase}
   208                          </a>
   209                          <div className='application-status-panel__item-value__revision show-for-large'>{hydrationStatusMessage(application)}</div>
   210                      </div>
   211                      <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
   212                          {application.status.sourceHydrator.currentOperation.phase}{' '}
   213                          <Timestamp date={application.status.sourceHydrator.currentOperation.finishedAt || application.status.sourceHydrator.currentOperation.startedAt} />
   214                      </div>
   215                      {application.status.sourceHydrator.currentOperation.message && (
   216                          <div className='application-status-panel__item-name'>{application.status.sourceHydrator.currentOperation.message}</div>
   217                      )}
   218                      <div className='application-status-panel__item-name'>
   219                          {application.status.sourceHydrator.currentOperation.drySHA && (
   220                              <RevisionMetadataPanel
   221                                  appName={application.metadata.name}
   222                                  appNamespace={application.metadata.namespace}
   223                                  type={''}
   224                                  revision={application.status.sourceHydrator.currentOperation.drySHA}
   225                                  versionId={utils.getAppCurrentVersion(application)}
   226                              />
   227                          )}
   228                      </div>
   229                  </div>
   230              )}
   231              <div className='application-status-panel__item'>
   232                  {sectionHeader(
   233                      {
   234                          title: 'SYNC STATUS',
   235                          helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.'
   236                      },
   237                      () => showMetadataInfo(application.status.sync ? 'SYNC_STATUS_REVISION' : null)
   238                  )}
   239                  <div className={`application-status-panel__item-value${appOperationState?.phase ? ` application-status-panel__item-value--${appOperationState.phase}` : ''}`}>
   240                      <div>
   241                          {application.status.sync.status === models.SyncStatuses.OutOfSync ? (
   242                              <a onClick={() => showDiff && showDiff()}>
   243                                  <ComparisonStatusIcon status={application.status.sync.status} label={true} isButton={true} />
   244                              </a>
   245                          ) : (
   246                              <ComparisonStatusIcon status={application.status.sync.status} label={true} />
   247                          )}
   248                      </div>
   249                      <div className='application-status-panel__item-value__revision show-for-large'>{syncStatusMessage(application)}</div>
   250                  </div>
   251                  <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
   252                      {application.spec.syncPolicy?.automated && application.spec.syncPolicy.automated.enabled !== false ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'}
   253                  </div>
   254                  {application.status &&
   255                      application.status.sync &&
   256                      (hasMultipleSources
   257                          ? application.status.sync.revisions && application.status.sync.revisions[0] && application.spec.sources && !application.spec.sources[0].chart
   258                          : application.status.sync.revision && !application.spec?.source?.chart) && (
   259                          <div className='application-status-panel__item-name'>
   260                              <RevisionMetadataPanel
   261                                  appName={application.metadata.name}
   262                                  appNamespace={application.metadata.namespace}
   263                                  type={revisionType}
   264                                  revision={revision}
   265                                  versionId={utils.getAppCurrentVersion(application)}
   266                              />
   267                          </div>
   268                      )}
   269              </div>
   270              {appOperationState && (
   271                  <div className='application-status-panel__item'>
   272                      {sectionHeader(
   273                          {
   274                              title: 'LAST SYNC',
   275                              helpContent:
   276                                  'Whether or not your last app sync was successful. It has been ' +
   277                                  daysSinceLastSynchronized +
   278                                  ' days since last sync. Click for the status of that sync.'
   279                          },
   280                          () =>
   281                              showMetadataInfo(
   282                                  appOperationState.syncResult && (appOperationState.syncResult.revisions || appOperationState.syncResult.revision)
   283                                      ? 'OPERATION_STATE_REVISION'
   284                                      : null
   285                              )
   286                      )}
   287                      <div className={`application-status-panel__item-value application-status-panel__item-value--${appOperationState.phase}`}>
   288                          <a onClick={() => showOperation && showOperation()}>
   289                              <OperationState app={application} isButton={true} />{' '}
   290                          </a>
   291                          {appOperationState.syncResult && (appOperationState.syncResult.revision || appOperationState.syncResult.revisions) && (
   292                              <div className='application-status-panel__item-value__revision show-for-large'>
   293                                  to <Revision repoUrl={source.repoURL} revision={operationStateRevision} /> {getAppDefaultSyncRevisionExtra(application)}
   294                              </div>
   295                          )}
   296                      </div>
   297                      <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
   298                          {appOperationState.phase} <Timestamp date={appOperationState.finishedAt || appOperationState.startedAt} />
   299                      </div>
   300                      {(appOperationState.syncResult && operationStateRevision && (
   301                          <RevisionMetadataPanel
   302                              appName={application.metadata.name}
   303                              appNamespace={application.metadata.namespace}
   304                              type={revisionType}
   305                              revision={operationStateRevision}
   306                              versionId={utils.getAppCurrentVersion(application)}
   307                          />
   308                      )) || <div className='application-status-panel__item-name'>{appOperationState.message}</div>}
   309                  </div>
   310              )}
   311              {application.status.conditions && (
   312                  <div className={`application-status-panel__item`}>
   313                      {sectionLabel({title: 'APP CONDITIONS'})}
   314                      <div className='application-status-panel__item-value application-status-panel__conditions' onClick={() => showConditions && showConditions()}>
   315                          {infos && (
   316                              <a className='info'>
   317                                  <i className='fa fa-info-circle application-status-panel__item-value__status-button' /> {infos} Info
   318                              </a>
   319                          )}
   320                          {warnings && (
   321                              <a className='warning'>
   322                                  <i className='fa fa-exclamation-triangle application-status-panel__item-value__status-button' /> {warnings} Warning{warnings !== 1 && 's'}
   323                              </a>
   324                          )}
   325                          {errors && (
   326                              <a className='error'>
   327                                  <i className='fa fa-exclamation-circle application-status-panel__item-value__status-button' /> {errors} Error{errors !== 1 && 's'}
   328                              </a>
   329                          )}
   330                      </div>
   331                  </div>
   332              )}
   333              <DataLoader
   334                  noLoaderOnInputChange={true}
   335                  input={application}
   336                  load={async app => {
   337                      return await services.applications.getApplicationSyncWindowState(app.metadata.name, app.metadata.namespace);
   338                  }}>
   339                  {(data: models.ApplicationSyncWindowState) => (
   340                      <React.Fragment>
   341                          {data?.assignedWindows && (
   342                              <div className='application-status-panel__item' style={{position: 'relative'}}>
   343                                  {sectionLabel({
   344                                      title: 'SYNC WINDOWS',
   345                                      helpContent:
   346                                          'The aggregate state of sync windows for this app. ' +
   347                                          'Red: no syncs allowed. ' +
   348                                          'Yellow: manual syncs allowed. ' +
   349                                          'Green: all syncs allowed'
   350                                  })}
   351                                  <div className='application-status-panel__item-value' style={{margin: 'auto 0'}}>
   352                                      <ApplicationSyncWindowStatusIcon project={application.spec.project} state={data} />
   353                                  </div>
   354                              </div>
   355                          )}
   356                      </React.Fragment>
   357                  )}
   358              </DataLoader>
   359              {showProgressiveSync && <ProgressiveSyncStatus application={application} />}
   360              {statusExtensions && statusExtensions.map(ext => <ext.component key={ext.title} application={application} openFlyout={() => showExtension && showExtension(ext.id)} />)}
   361          </div>
   362      );
   363  };