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

     1  import {ErrorNotification, FormField, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
     2  import * as React from 'react';
     3  import {Form, FormApi, Text} from 'react-form';
     4  
     5  import {ARGO_WARNING_COLOR, CheckboxField, Spinner} from '../../../shared/components';
     6  import {Consumer} from '../../../shared/context';
     7  import * as models from '../../../shared/models';
     8  import {services} from '../../../shared/services';
     9  import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
    10  import {
    11      ApplicationManualSyncFlags,
    12      ApplicationSyncOptions,
    13      FORCE_WARNING,
    14      SyncFlags,
    15      REPLACE_WARNING,
    16      PRUNE_ALL_WARNING
    17  } from '../application-sync-options/application-sync-options';
    18  import {ComparisonStatusIcon, getAppDefaultSource, nodeKey} from '../utils';
    19  
    20  import './application-sync-panel.scss';
    21  
    22  export const ApplicationSyncPanel = ({application, selectedResource, hide}: {application: models.Application; selectedResource: string; hide: () => any}) => {
    23      const [form, setForm] = React.useState<FormApi>(null);
    24      const isVisible = !!(selectedResource && application);
    25      const appResources = ((application && selectedResource && application.status && application.status.resources) || [])
    26          .sort((first, second) => nodeKey(first).localeCompare(nodeKey(second)))
    27          .filter(item => !item.hook);
    28      const syncResIndex = appResources.findIndex(item => nodeKey(item) === selectedResource);
    29      const syncStrategy = {} as models.SyncStrategy;
    30      const [isPending, setPending] = React.useState(false);
    31      const source = getAppDefaultSource(application);
    32  
    33      return (
    34          <Consumer>
    35              {ctx => (
    36                  <SlidingPanel
    37                      isMiddle={true}
    38                      isShown={isVisible}
    39                      onClose={() => hide()}
    40                      header={
    41                          <div>
    42                              <button
    43                                  qe-id='application-sync-panel-button-synchronize'
    44                                  className='argo-button argo-button--base'
    45                                  disabled={isPending}
    46                                  onClick={() => form.submitForm(null)}>
    47                                  <Spinner show={isPending} style={{marginRight: '5px'}} />
    48                                  Synchronize
    49                              </button>{' '}
    50                              <button onClick={() => hide()} className='argo-button argo-button--base-o'>
    51                                  Cancel
    52                              </button>
    53                          </div>
    54                      }>
    55                      {isVisible && (
    56                          <Form
    57                              defaultValues={{
    58                                  revision: new URLSearchParams(ctx.history.location.search).get('revision') || source.targetRevision || 'HEAD',
    59                                  resources: appResources.map((_, i) => i === syncResIndex || syncResIndex === -1),
    60                                  syncOptions: application.spec.syncPolicy ? application.spec.syncPolicy.syncOptions : []
    61                              }}
    62                              validateError={values => ({
    63                                  resources: values.resources.every((item: boolean) => !item) && 'Select at least one resource'
    64                              })}
    65                              onSubmit={async (params: any) => {
    66                                  setPending(true);
    67                                  let selectedResources = appResources.filter((_, i) => params.resources[i]);
    68                                  const allResourcesAreSelected = selectedResources.length === appResources.length;
    69                                  const syncFlags = {...params.syncFlags} as SyncFlags;
    70  
    71                                  const allRequirePruning = !selectedResources.some(resource => !resource?.requiresPruning);
    72                                  if (syncFlags.Prune && allRequirePruning && allResourcesAreSelected) {
    73                                      const confirmed = await ctx.popup.confirm('Prune all resources?', () => (
    74                                          <div>
    75                                              <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} />
    76                                              {PRUNE_ALL_WARNING} Are you sure you want to continue?
    77                                          </div>
    78                                      ));
    79                                      if (!confirmed) {
    80                                          setPending(false);
    81                                          return;
    82                                      }
    83                                  }
    84                                  if (allResourcesAreSelected) {
    85                                      selectedResources = null;
    86                                  }
    87                                  const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
    88                                  if (replace) {
    89                                      const confirmed = await ctx.popup.confirm('Synchronize using replace?', () => (
    90                                          <div>
    91                                              <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {REPLACE_WARNING} Are you sure you want to continue?
    92                                          </div>
    93                                      ));
    94                                      if (!confirmed) {
    95                                          setPending(false);
    96                                          return;
    97                                      }
    98                                  }
    99  
   100                                  const force = syncFlags.Force || false;
   101  
   102                                  if (syncFlags.ApplyOnly) {
   103                                      syncStrategy.apply = {force};
   104                                  } else {
   105                                      syncStrategy.hook = {force};
   106                                  }
   107                                  if (force) {
   108                                      const confirmed = await ctx.popup.confirm('Synchronize with force?', () => (
   109                                          <div>
   110                                              <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {FORCE_WARNING} Are you sure you want to continue?
   111                                          </div>
   112                                      ));
   113                                      if (!confirmed) {
   114                                          setPending(false);
   115                                          return;
   116                                      }
   117                                  }
   118  
   119                                  try {
   120                                      await services.applications.sync(
   121                                          application.metadata.name,
   122                                          application.metadata.namespace,
   123                                          params.revision,
   124                                          syncFlags.Prune || false,
   125                                          syncFlags.DryRun || false,
   126                                          syncStrategy,
   127                                          selectedResources,
   128                                          params.syncOptions,
   129                                          params.retryStrategy
   130                                      );
   131                                      hide();
   132                                  } catch (e) {
   133                                      ctx.notifications.show({
   134                                          content: <ErrorNotification title='Unable to sync' e={e} />,
   135                                          type: NotificationType.Error
   136                                      });
   137                                  } finally {
   138                                      setPending(false);
   139                                  }
   140                              }}
   141                              getApi={setForm}>
   142                              {formApi => (
   143                                  <form role='form' className='width-control' onSubmit={formApi.submitForm}>
   144                                      <h6>
   145                                          Synchronizing application manifests from <a href={source.repoURL}>{source.repoURL}</a>
   146                                      </h6>
   147                                      <div className='argo-form-row'>
   148                                          <FormField formApi={formApi} label='Revision' field='revision' component={Text} />
   149                                      </div>
   150  
   151                                      <div className='argo-form-row'>
   152                                          <div style={{marginBottom: '1em'}}>
   153                                              <FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
   154                                          </div>
   155                                          <div style={{marginBottom: '1em'}}>
   156                                              <label>Sync Options</label>
   157                                              <ApplicationSyncOptions
   158                                                  options={formApi.values.syncOptions}
   159                                                  onChanged={opts => {
   160                                                      formApi.setTouched('syncOptions', true);
   161                                                      formApi.setValue('syncOptions', opts);
   162                                                  }}
   163                                                  id='application-sync-panel'
   164                                              />
   165                                          </div>
   166  
   167                                          <ApplicationRetryOptions
   168                                              id='application-sync-panel'
   169                                              formApi={formApi}
   170                                              initValues={application.spec.syncPolicy ? application.spec.syncPolicy.retry : null}
   171                                          />
   172  
   173                                          <label>Synchronize resources:</label>
   174                                          <div style={{float: 'right'}}>
   175                                              <a
   176                                                  onClick={() =>
   177                                                      formApi.setValue(
   178                                                          'resources',
   179                                                          formApi.values.resources.map(() => true)
   180                                                      )
   181                                                  }>
   182                                                  all
   183                                              </a>{' '}
   184                                              /{' '}
   185                                              <a
   186                                                  onClick={() =>
   187                                                      formApi.setValue(
   188                                                          'resources',
   189                                                          application.status.resources
   190                                                              .filter(item => !item.hook)
   191                                                              .map((resource: models.ResourceStatus) => resource.status === models.SyncStatuses.OutOfSync)
   192                                                      )
   193                                                  }>
   194                                                  out of sync
   195                                              </a>{' '}
   196                                              /{' '}
   197                                              <a
   198                                                  onClick={() =>
   199                                                      formApi.setValue(
   200                                                          'resources',
   201                                                          formApi.values.resources.map(() => false)
   202                                                      )
   203                                                  }>
   204                                                  none
   205                                              </a>
   206                                          </div>
   207                                          <div className='application-details__warning'>
   208                                              {!formApi.values.resources.every((item: boolean) => item) && <div>WARNING: partial synchronization is not recorded in history</div>}
   209                                          </div>
   210                                          <div>
   211                                              {application.status.resources
   212                                                  .filter(item => !item.hook)
   213                                                  .map((item, i) => {
   214                                                      const resKey = nodeKey(item);
   215                                                      const contentStart = resKey.substr(0, Math.floor(resKey.length / 2));
   216                                                      let contentEnd = resKey.substr(-Math.floor(resKey.length / 2));
   217                                                      // We want the ellipsis to be in the middle of our text, so we use RTL layout to put it there.
   218                                                      // Unfortunately, strong LTR characters get jumbled around, so make sure that the last character isn't strong.
   219                                                      const firstLetter = /[a-z]/i.exec(contentEnd);
   220                                                      if (firstLetter) {
   221                                                          contentEnd = contentEnd.slice(firstLetter.index);
   222                                                      }
   223                                                      const isLongLabel = resKey.length > 68;
   224                                                      return (
   225                                                          <div key={resKey} className='application-sync-panel__resource'>
   226                                                              <CheckboxField id={resKey} field={`resources[${i}]`} />
   227                                                              <Tooltip content={<div style={{wordBreak: 'break-all'}}>{resKey}</div>}>
   228                                                                  <div className='container'>
   229                                                                      {isLongLabel ? (
   230                                                                          <label htmlFor={resKey} content-start={contentStart} content-end={contentEnd} />
   231                                                                      ) : (
   232                                                                          <label htmlFor={resKey}>{resKey}</label>
   233                                                                      )}
   234                                                                  </div>
   235                                                              </Tooltip>
   236                                                              <ComparisonStatusIcon status={item.status} resource={item} />
   237                                                          </div>
   238                                                      );
   239                                                  })}
   240                                              {formApi.errors.resources && <div className='argo-form-row__error-msg'>{formApi.errors.resources}</div>}
   241                                          </div>
   242                                      </div>
   243                                  </form>
   244                              )}
   245                          </Form>
   246                      )}
   247                  </SlidingPanel>
   248              )}
   249          </Consumer>
   250      );
   251  };