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

     1  import {Checkbox, DropDown, Duration, NotificationType, Ticker, HelpIcon, Tooltip} from 'argo-ui';
     2  import * as moment from 'moment';
     3  import * as PropTypes from 'prop-types';
     4  import * as React from 'react';
     5  
     6  import {ErrorNotification, Revision, Timestamp} from '../../../shared/components';
     7  import {AppContext} from '../../../shared/context';
     8  import * as models from '../../../shared/models';
     9  import {services} from '../../../shared/services';
    10  import * as utils from '../utils';
    11  
    12  import './application-operation-state.scss';
    13  
    14  interface Props {
    15      application: models.Application;
    16      operationState: models.OperationState;
    17  }
    18  const buildResourceUniqueId = (res: Omit<models.ResourceRef, 'uid'>) => `${res.group || ''}-${res.kind || ''}-${res.version || ''}-${res.namespace || ''}-${res.name}`;
    19  const FilterableMessageStatuses = ['Changed', 'Unchanged'];
    20  
    21  const Filter = (props: {filters: string[]; setFilters: (f: string[]) => void; options: string[]; title: string; style?: React.CSSProperties}) => {
    22      const {filters, setFilters, options, title, style} = props;
    23      return (
    24          <DropDown
    25              isMenu={true}
    26              anchor={() => (
    27                  <div title='Filter' style={style}>
    28                      <button className='argo-button argo-button--base'>
    29                          {title} <i className='argo-icon-filter' aria-hidden='true' />
    30                      </button>
    31                  </div>
    32              )}>
    33              {options.map(f => (
    34                  <div key={f} style={{minWidth: '150px', lineHeight: '2em', padding: '5px'}}>
    35                      <Checkbox
    36                          checked={filters.includes(f)}
    37                          onChange={checked => {
    38                              const selectedValues = [...filters];
    39                              const idx = selectedValues.indexOf(f);
    40                              if (idx > -1 && !checked) {
    41                                  selectedValues.splice(idx, 1);
    42                              } else {
    43                                  selectedValues.push(f);
    44                              }
    45                              setFilters(selectedValues);
    46                          }}
    47                      />
    48                      <label htmlFor={`filter__${f}`}>{f}</label>
    49                  </div>
    50              ))}
    51          </DropDown>
    52      );
    53  };
    54  
    55  export const ApplicationOperationState: React.StatelessComponent<Props> = ({application, operationState}, ctx: AppContext) => {
    56      const [messageFilters, setMessageFilters] = React.useState([]);
    57  
    58      const operationAttributes = [
    59          {title: 'OPERATION', value: utils.getOperationType(application)},
    60          {title: 'PHASE', value: operationState.phase},
    61          ...(operationState.message
    62              ? [
    63                    {
    64                        title: 'MESSAGE',
    65                        value: (
    66                            <pre
    67                                style={{
    68                                    whiteSpace: 'pre-wrap',
    69                                    wordBreak: 'break-word',
    70                                    margin: 0,
    71                                    fontFamily: 'inherit'
    72                                }}>
    73                                {utils.formatOperationMessage(operationState.message)}
    74                            </pre>
    75                        )
    76                    }
    77                ]
    78              : []),
    79          {title: 'STARTED AT', value: <Timestamp date={operationState.startedAt} />},
    80          {
    81              title: 'DURATION',
    82              value: (
    83                  <Ticker>
    84                      {time => (
    85                          <Duration durationS={((operationState.finishedAt && moment(operationState.finishedAt)) || moment(time)).diff(moment(operationState.startedAt)) / 1000} />
    86                      )}
    87                  </Ticker>
    88              )
    89          }
    90      ];
    91  
    92      if (operationState.finishedAt && operationState.phase !== 'Running') {
    93          operationAttributes.push({title: 'FINISHED AT', value: <Timestamp date={operationState.finishedAt} />});
    94      } else if (operationState.phase !== 'Terminating') {
    95          operationAttributes.push({
    96              title: '',
    97              value: (
    98                  <button
    99                      className='argo-button argo-button--base'
   100                      onClick={async () => {
   101                          const confirmed = await ctx.apis.popup.confirm('Terminate operation', 'Are you sure you want to terminate operation?');
   102                          if (confirmed) {
   103                              try {
   104                                  await services.applications.terminateOperation(application.metadata.name, application.metadata.namespace);
   105                              } catch (e) {
   106                                  ctx.apis.notifications.show({
   107                                      content: <ErrorNotification title='Unable to terminate operation' e={e} />,
   108                                      type: NotificationType.Error
   109                                  });
   110                              }
   111                          }
   112                      }}>
   113                      Terminate
   114                  </button>
   115              )
   116          });
   117      }
   118      if (operationState.syncResult) {
   119          operationAttributes.push({
   120              title: 'REVISION',
   121              value: (
   122                  <div>
   123                      <Revision repoUrl={utils.getAppDefaultSource(application).repoURL} revision={utils.getAppDefaultOperationSyncRevision(application)} />
   124                      {utils.getAppDefaultOperationSyncRevisionExtra(application)}
   125                  </div>
   126              )
   127          });
   128      }
   129      let initiator = '';
   130      if (operationState.operation.initiatedBy) {
   131          if (operationState.operation.initiatedBy.automated) {
   132              initiator = 'automated sync policy';
   133          } else {
   134              initiator = operationState.operation.initiatedBy.username;
   135          }
   136      }
   137      operationAttributes.push({title: 'INITIATED BY', value: initiator || 'Unknown'});
   138  
   139      const resultAttributes: {title: string; value: string}[] = [];
   140      const syncResult = operationState.syncResult;
   141      if (operationState.finishedAt) {
   142          if (syncResult) {
   143              (syncResult.resources || []).forEach(res => {
   144                  resultAttributes.push({
   145                      title: `${res.namespace}/${res.kind}:${res.name}`,
   146                      value: res.message
   147                  });
   148              });
   149          }
   150      }
   151      const [filters, setFilters] = React.useState([]);
   152      const [healthFilters, setHealthFilters] = React.useState([]);
   153  
   154      const Healths = Object.keys(models.HealthStatuses);
   155      const Statuses = Object.keys(models.ResultCodes);
   156      const OperationPhases = Object.keys(models.OperationPhases);
   157      // const syncPhases = ['PreSync', 'Sync', 'PostSync', 'SyncFail'];
   158      // const hookPhases = ['Running', 'Terminating', 'Failed', 'Error', 'Succeeded'];
   159      const resourceHealth = application.status.resources.reduce(
   160          (acc, res) => {
   161              acc[buildResourceUniqueId(res)] = {
   162                  health: res.health,
   163                  syncWave: res.syncWave
   164              };
   165  
   166              return acc;
   167          },
   168          {} as Record<
   169              string,
   170              {
   171                  health: models.HealthStatus;
   172                  syncWave: number;
   173              }
   174          >
   175      );
   176  
   177      const combinedHealthSyncResult: models.SyncResourceResult[] = syncResult?.resources?.map(syncResultItem => {
   178          const uniqueResourceName = buildResourceUniqueId(syncResultItem);
   179  
   180          const healthStatus = resourceHealth[uniqueResourceName];
   181  
   182          const syncResultWithHealth: models.SyncResourceResult = {
   183              ...syncResultItem
   184          };
   185  
   186          if (healthStatus?.health) {
   187              syncResultWithHealth.health = healthStatus.health;
   188          }
   189  
   190          syncResultWithHealth.syncWave = healthStatus?.syncWave;
   191  
   192          return syncResultWithHealth;
   193      });
   194      let filtered: models.SyncResourceResult[] = [];
   195  
   196      if (combinedHealthSyncResult && combinedHealthSyncResult.length > 0) {
   197          filtered = combinedHealthSyncResult.filter(r => {
   198              if (filters.length === 0 && healthFilters.length === 0 && messageFilters.length === 0) {
   199                  return true;
   200              }
   201  
   202              let pass = true;
   203              if (filters.length !== 0 && !filters.includes(getStatus(r))) {
   204                  pass = false;
   205              }
   206  
   207              if (pass && healthFilters.length !== 0 && !healthFilters.includes(r.health?.status)) {
   208                  pass = false;
   209              }
   210  
   211              if (pass && messageFilters.length !== 0) {
   212                  pass = messageFilters.some(filter => {
   213                      if (filter === 'Changed') {
   214                          return r.message?.toLowerCase().includes('configured');
   215                      }
   216                      return r.message?.toLowerCase().includes(filter.toLowerCase());
   217                  });
   218              }
   219  
   220              return pass;
   221          });
   222      }
   223  
   224      return (
   225          <div>
   226              <div className='white-box'>
   227                  <div className='white-box__details'>
   228                      {operationAttributes.map(attr => (
   229                          <div className='row white-box__details-row' key={attr.title}>
   230                              <div className='columns small-3'>{attr.title}</div>
   231                              <div className='columns small-9'>{attr.value}</div>
   232                          </div>
   233                      ))}
   234                  </div>
   235              </div>
   236              {syncResult && syncResult.resources && syncResult.resources.length > 0 && (
   237                  <React.Fragment>
   238                      <div style={{display: 'flex'}}>
   239                          <label style={{display: 'block', marginBottom: '1em'}}>RESULT</label>
   240                          <div style={{marginLeft: 'auto'}}>
   241                              <Filter options={Healths} filters={healthFilters} setFilters={setHealthFilters} title='HEALTH' style={{marginRight: '5px'}} />
   242                              <Filter options={Statuses} filters={filters} setFilters={setFilters} title='STATUS' style={{marginRight: '5px'}} />
   243                              <Filter options={OperationPhases} filters={filters} setFilters={setFilters} title='HOOK' />
   244                              <Tooltip placement='top-start' content='Filter on resources that have changed or remained unchanged'>
   245                                  <div style={{display: 'inline-block'}}>
   246                                      <Filter options={FilterableMessageStatuses} filters={messageFilters} setFilters={setMessageFilters} title='MESSAGE' />
   247                                  </div>
   248                              </Tooltip>
   249                          </div>
   250                      </div>
   251                      <div className='argo-table-list'>
   252                          <div className='argo-table-list__head'>
   253                              <div className='row'>
   254                                  <div className='columns large-1 show-for-large application-operation-state__icons_container_padding'>SYNC WAVE</div>
   255                                  <div className='columns large-1 show-for-large application-operation-state__icons_container_padding'>KIND</div>
   256                                  <div className='columns large-1 show-for-large'>NAMESPACE</div>
   257                                  <div className='columns large-2 small-2'>NAME</div>
   258                                  <div className='columns large-1 small-2'>STATUS</div>
   259                                  <div className='columns large-1 small-2'>HEALTH</div>
   260                                  <div className='columns large-1 show-for-large'>HOOK</div>
   261                                  <div className='columns large-3 small-4'>MESSAGE</div>
   262                                  <div className='columns large-1 small-2'>IMAGES</div>
   263                              </div>
   264                          </div>
   265                          {filtered.length > 0 ? (
   266                              filtered.map((resource, i) => (
   267                                  <div className='argo-table-list__row' key={i}>
   268                                      <div className='row'>
   269                                          <div className='columns large-1 show-for-large application-operation-state__icons_container_padding' style={{textAlign: 'center'}}>
   270                                              <div className='application-operation-state__icons_container'>
   271                                                  {resource.hookType && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
   272                                              </div>
   273                                              {resource.syncWave || '0'}
   274                                          </div>
   275                                          <div className='columns large-1 show-for-large'>
   276                                              <span title={getKind(resource)}>{getKind(resource)}</span>
   277                                          </div>
   278                                          <div className='columns large-1 show-for-large' title={resource.namespace}>
   279                                              {resource.namespace}
   280                                          </div>
   281                                          <div className='columns large-2 small-2' title={resource.name}>
   282                                              {resource.name}
   283                                          </div>
   284                                          <div className='columns large-1 small-2' title={getStatus(resource)}>
   285                                              <utils.ResourceResultIcon resource={resource} /> {getStatus(resource)}
   286                                          </div>
   287                                          <div className='columns large-1 small-2'>
   288                                              {resource.health ? (
   289                                                  <div>
   290                                                      <utils.HealthStatusIcon state={resource?.health} /> {resource.health?.status}
   291                                                      {resource.health.message && <HelpIcon title={resource.health.message} />}
   292                                                  </div>
   293                                              ) : (
   294                                                  <>{'-'}</>
   295                                              )}
   296                                          </div>
   297                                          <div className='columns large-1 show-for-large' title={resource.hookType}>
   298                                              {resource.hookType}
   299                                          </div>
   300                                          <div className='columns large-3 small-4' title={resource.message}>
   301                                              <div className='application-operation-state__message'>{resource.message}</div>
   302                                          </div>
   303                                          <div className='columns large-1  small-2'>
   304                                              {resource.images && resource.images.length > 0 ? (
   305                                                  <Tooltip
   306                                                      placement='top'
   307                                                      content={
   308                                                          <div>
   309                                                              <ul className='application-operation-state__images-list' style={{margin: '10px'}}>
   310                                                                  {resource.images.map((image, idx) => (
   311                                                                      <li key={idx}>{image}</li>
   312                                                                  ))}
   313                                                              </ul>
   314                                                          </div>
   315                                                      }>
   316                                                      <span className='application-operation-state__images-count'>
   317                                                          {resource.images.length} image{resource.images.length !== 1 ? 's' : ''}
   318                                                      </span>
   319                                                  </Tooltip>
   320                                              ) : (
   321                                                  '-'
   322                                              )}
   323                                          </div>
   324                                      </div>
   325                                  </div>
   326                              ))
   327                          ) : (
   328                              <div style={{textAlign: 'center', marginTop: '2em', fontSize: '20px'}}>No Sync Results match filter</div>
   329                          )}
   330                      </div>
   331                  </React.Fragment>
   332              )}
   333          </div>
   334      );
   335  };
   336  
   337  const getKind = (resource: models.ResourceResult): string => {
   338      return (resource.group ? `${resource.group}/${resource.version}` : resource.version) + `/${resource.kind}`;
   339  };
   340  
   341  const getStatus = (resource: models.ResourceResult): string => {
   342      return resource.hookType ? resource.hookPhase : resource.status;
   343  };
   344  
   345  ApplicationOperationState.contextTypes = {
   346      apis: PropTypes.object
   347  };