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

     1  import {AutocompleteField, DropDownMenu, FormField, FormSelect, HelpIcon, PopupApi} from 'argo-ui';
     2  import * as React from 'react';
     3  import {FormApi, Text} from 'react-form';
     4  import {Cluster, DataLoader, EditablePanel, EditablePanelItem, Expandable, MapInputField, NumberField, Repo, Revision, RevisionHelpIcon} from '../../../shared/components';
     5  import {BadgePanel, Spinner} from '../../../shared/components';
     6  import {Consumer} from '../../../shared/context';
     7  import * as models from '../../../shared/models';
     8  import {services} from '../../../shared/services';
     9  
    10  import {ApplicationSyncOptionsField} from '../application-sync-options';
    11  import {RevisionFormField} from '../revision-form-field/revision-form-field';
    12  import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage} from '../utils';
    13  
    14  require('./application-summary.scss');
    15  
    16  const urlPattern = new RegExp(
    17      new RegExp(
    18          // tslint:disable-next-line:max-line-length
    19          /^(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,})$/,
    20          'gi'
    21      )
    22  );
    23  
    24  function swap(array: any[], a: number, b: number) {
    25      array = array.slice();
    26      [array[a], array[b]] = [array[b], array[a]];
    27      return array;
    28  }
    29  
    30  export const ApplicationSummary = (props: {app: models.Application; updateApp: (app: models.Application) => Promise<any>}) => {
    31      const app = JSON.parse(JSON.stringify(props.app)) as models.Application;
    32      const isHelm = app.spec.source.hasOwnProperty('chart');
    33      const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL';
    34      const [destFormat, setDestFormat] = React.useState(initialState);
    35      const [changeSync, setChangeSync] = React.useState(false);
    36      const attributes = [
    37          {
    38              title: 'PROJECT',
    39              view: <a href={'/settings/projects/' + app.spec.project}>{app.spec.project}</a>,
    40              edit: (formApi: FormApi) => (
    41                  <DataLoader load={() => services.projects.list('items.metadata.name').then(projs => projs.map(item => item.metadata.name))}>
    42                      {projects => <FormField formApi={formApi} field='spec.project' component={FormSelect} componentProps={{options: projects}} />}
    43                  </DataLoader>
    44              )
    45          },
    46          {
    47              title: 'LABELS',
    48              view: Object.keys(app.metadata.labels || {})
    49                  .map(label => `${label}=${app.metadata.labels[label]}`)
    50                  .join(' '),
    51              edit: (formApi: FormApi) => <FormField formApi={formApi} field='metadata.labels' component={MapInputField} />
    52          },
    53          {
    54              title: 'ANNOTATIONS',
    55              view: (
    56                  <Expandable height={48}>
    57                      {Object.keys(app.metadata.annotations || {})
    58                          .map(annotation => `${annotation}=${app.metadata.annotations[annotation]}`)
    59                          .join(' ')}
    60                  </Expandable>
    61              ),
    62              edit: (formApi: FormApi) => <FormField formApi={formApi} field='metadata.annotations' component={MapInputField} />
    63          },
    64          {
    65              title: 'CLUSTER',
    66              view: <Cluster server={app.spec.destination.server} name={app.spec.destination.name} showUrl={true} />,
    67              edit: (formApi: FormApi) => (
    68                  <DataLoader load={() => services.clusters.list().then(clusters => clusters.sort())}>
    69                      {clusters => {
    70                          return (
    71                              <div className='row'>
    72                                  {(destFormat.toUpperCase() === 'URL' && (
    73                                      <div className='columns small-10'>
    74                                          <FormField
    75                                              formApi={formApi}
    76                                              field='spec.destination.server'
    77                                              componentProps={{items: clusters.map(cluster => cluster.server)}}
    78                                              component={AutocompleteField}
    79                                          />
    80                                      </div>
    81                                  )) || (
    82                                      <div className='columns small-10'>
    83                                          <FormField
    84                                              formApi={formApi}
    85                                              field='spec.destination.name'
    86                                              componentProps={{items: clusters.map(cluster => cluster.name)}}
    87                                              component={AutocompleteField}
    88                                          />
    89                                      </div>
    90                                  )}
    91                                  <div className='columns small-2'>
    92                                      <div>
    93                                          <DropDownMenu
    94                                              anchor={() => (
    95                                                  <p>
    96                                                      {destFormat.toUpperCase()} <i className='fa fa-caret-down' />
    97                                                  </p>
    98                                              )}
    99                                              items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
   100                                                  title: type,
   101                                                  action: () => {
   102                                                      if (destFormat !== type) {
   103                                                          const updatedApp = formApi.getFormState().values as models.Application;
   104                                                          if (type === 'URL') {
   105                                                              updatedApp.spec.destination.server = '';
   106                                                              delete updatedApp.spec.destination.name;
   107                                                          } else {
   108                                                              updatedApp.spec.destination.name = '';
   109                                                              delete updatedApp.spec.destination.server;
   110                                                          }
   111                                                          formApi.setAllValues(updatedApp);
   112                                                          setDestFormat(type);
   113                                                      }
   114                                                  }
   115                                              }))}
   116                                          />
   117                                      </div>
   118                                  </div>
   119                              </div>
   120                          );
   121                      }}
   122                  </DataLoader>
   123              )
   124          },
   125          {
   126              title: 'NAMESPACE',
   127              view: app.spec.destination.namespace,
   128              edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.destination.namespace' component={Text} />
   129          },
   130          {
   131              title: 'REPO URL',
   132              view: <Repo url={app.spec.source.repoURL} />,
   133              edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.repoURL' component={Text} />
   134          },
   135          ...(isHelm
   136              ? [
   137                    {
   138                        title: 'CHART',
   139                        view: (
   140                            <span>
   141                                {app.spec.source.chart}:{app.spec.source.targetRevision}
   142                            </span>
   143                        ),
   144                        edit: (formApi: FormApi) => (
   145                            <DataLoader
   146                                input={{repoURL: formApi.getFormState().values.spec.source.repoURL}}
   147                                load={src => services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())}>
   148                                {(charts: models.HelmChart[]) => (
   149                                    <div className='row'>
   150                                        <div className='columns small-10'>
   151                                            <FormField
   152                                                formApi={formApi}
   153                                                field='spec.source.chart'
   154                                                component={AutocompleteField}
   155                                                componentProps={{
   156                                                    items: charts.map(chart => chart.name),
   157                                                    filterSuggestions: true
   158                                                }}
   159                                            />
   160                                        </div>
   161                                        <DataLoader
   162                                            input={{charts, chart: formApi.getFormState().values.spec.source.chart}}
   163                                            load={async data => {
   164                                                const chartInfo = data.charts.find(chart => chart.name === data.chart);
   165                                                return (chartInfo && chartInfo.versions) || new Array<string>();
   166                                            }}>
   167                                            {(versions: string[]) => (
   168                                                <div className='columns small-2'>
   169                                                    <FormField
   170                                                        formApi={formApi}
   171                                                        field='spec.source.targetRevision'
   172                                                        component={AutocompleteField}
   173                                                        componentProps={{
   174                                                            items: versions
   175                                                        }}
   176                                                    />
   177                                                    <RevisionHelpIcon type='helm' top='0' />
   178                                                </div>
   179                                            )}
   180                                        </DataLoader>
   181                                    </div>
   182                                )}
   183                            </DataLoader>
   184                        )
   185                    }
   186                ]
   187              : [
   188                    {
   189                        title: 'TARGET REVISION',
   190                        view: <Revision repoUrl={app.spec.source.repoURL} revision={app.spec.source.targetRevision || 'HEAD'} />,
   191                        edit: (formApi: FormApi) => <RevisionFormField helpIconTop={'0'} hideLabel={true} formApi={formApi} repoURL={app.spec.source.repoURL} />
   192                    },
   193                    {
   194                        title: 'PATH',
   195                        view: app.spec.source.path,
   196                        edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.path' component={Text} />
   197                    }
   198                ]),
   199  
   200          {
   201              title: 'REVISION HISTORY LIMIT',
   202              view: app.spec.revisionHistoryLimit,
   203              edit: (formApi: FormApi) => (
   204                  <div style={{position: 'relative'}}>
   205                      <FormField formApi={formApi} field='spec.revisionHistoryLimit' componentProps={{style: {paddingRight: '1em'}, placeholder: '10'}} component={NumberField} />
   206                      <div style={{position: 'absolute', right: '0', top: '0'}}>
   207                          <HelpIcon
   208                              title='This limits this number of items kept in the apps revision history.
   209      This should only be changed in exceptional circumstances.
   210      Setting to zero will store no history. This will reduce storage used.
   211      Increasing will increase the space used to store the history, so we do not recommend increasing it.
   212      Default is 10.'
   213                          />
   214                      </div>
   215                  </div>
   216              )
   217          },
   218          {
   219              title: 'SYNC OPTIONS',
   220              view: ((app.spec.syncPolicy || {}).syncOptions || []).join(', '),
   221              edit: (formApi: FormApi) => (
   222                  <div>
   223                      <FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
   224                  </div>
   225              )
   226          },
   227          {
   228              title: 'STATUS',
   229              view: (
   230                  <span>
   231                      <ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status} {syncStatusMessage(app)}
   232                  </span>
   233              )
   234          },
   235          {
   236              title: 'HEALTH',
   237              view: (
   238                  <span>
   239                      <HealthStatusIcon state={app.status.health} /> {app.status.health.status}
   240                  </span>
   241              )
   242          }
   243      ];
   244  
   245      const urls = app.status.summary.externalURLs || [];
   246      if (urls.length > 0) {
   247          attributes.push({
   248              title: 'URLs',
   249              view: (
   250                  <React.Fragment>
   251                      {urls
   252                          .map(item => item.split('|'))
   253                          .map((parts, i) => (
   254                              <a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='__blank'>
   255                                  {parts[0]} &nbsp;
   256                              </a>
   257                          ))}
   258                  </React.Fragment>
   259              )
   260          });
   261      }
   262  
   263      if ((app.status.summary.images || []).length) {
   264          attributes.push({
   265              title: 'IMAGES',
   266              view: (
   267                  <div className='application-summary__labels'>
   268                      {(app.status.summary.images || []).sort().map(image => (
   269                          <span className='application-summary__label' key={image}>
   270                              {image}
   271                          </span>
   272                      ))}
   273                  </div>
   274              )
   275          });
   276      }
   277  
   278      async function setAutoSync(ctx: {popup: PopupApi}, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean) {
   279          const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
   280          if (confirmed) {
   281              try {
   282                  setChangeSync(true);
   283                  const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
   284                  if (!updatedApp.spec.syncPolicy) {
   285                      updatedApp.spec.syncPolicy = {};
   286                  }
   287                  updatedApp.spec.syncPolicy.automated = {prune, selfHeal};
   288                  await props.updateApp(updatedApp);
   289              } finally {
   290                  setChangeSync(false);
   291              }
   292          }
   293      }
   294  
   295      async function unsetAutoSync(ctx: {popup: PopupApi}) {
   296          const confirmed = await ctx.popup.confirm('Disable Auto-Sync?', 'Are you sure you want to disable automated application synchronization');
   297          if (confirmed) {
   298              try {
   299                  setChangeSync(true);
   300                  const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
   301                  updatedApp.spec.syncPolicy.automated = null;
   302                  await props.updateApp(updatedApp);
   303              } finally {
   304                  setChangeSync(false);
   305              }
   306          }
   307      }
   308  
   309      const items = app.spec.info || [];
   310      const [adjustedCount, setAdjustedCount] = React.useState(0);
   311  
   312      const added = new Array<{name: string; value: string; key: string}>();
   313      for (let i = 0; i < adjustedCount; i++) {
   314          added.push({name: '', value: '', key: (items.length + i).toString()});
   315      }
   316      for (let i = 0; i > adjustedCount; i--) {
   317          items.pop();
   318      }
   319      const allItems = items.concat(added);
   320      const infoItems: EditablePanelItem[] = allItems
   321          .map((info, i) => ({
   322              key: i.toString(),
   323              title: info.name,
   324              view: info.value.match(urlPattern) ? (
   325                  <a href={info.value} target='__blank'>
   326                      {info.value}
   327                  </a>
   328              ) : (
   329                  info.value
   330              ),
   331              titleEdit: (formApi: FormApi) => (
   332                  <React.Fragment>
   333                      {i > 0 && (
   334                          <i
   335                              className='fa fa-sort-up application-summary__sort-icon'
   336                              onClick={() => {
   337                                  formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i - 1));
   338                              }}
   339                          />
   340                      )}
   341                      <FormField formApi={formApi} field={`spec.info[${[i]}].name`} component={Text} componentProps={{style: {width: '99%'}}} />
   342                      {i < allItems.length - 1 && (
   343                          <i
   344                              className='fa fa-sort-down application-summary__sort-icon'
   345                              onClick={() => {
   346                                  formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i + 1));
   347                              }}
   348                          />
   349                      )}
   350                  </React.Fragment>
   351              ),
   352              edit: (formApi: FormApi) => (
   353                  <React.Fragment>
   354                      <FormField formApi={formApi} field={`spec.info[${[i]}].value`} component={Text} />
   355                      <i
   356                          className='fa fa-times application-summary__remove-icon'
   357                          onClick={() => {
   358                              const values = (formApi.getFormState().values.spec.info || []) as Array<any>;
   359                              formApi.setValue('spec.info', [...values.slice(0, i), ...values.slice(i + 1, values.length)]);
   360                              setAdjustedCount(adjustedCount - 1);
   361                          }}
   362                      />
   363                  </React.Fragment>
   364              )
   365          }))
   366          .concat({
   367              key: '-1',
   368              title: '',
   369              titleEdit: () => (
   370                  <button
   371                      className='argo-button argo-button--base'
   372                      onClick={() => {
   373                          setAdjustedCount(adjustedCount + 1);
   374                      }}>
   375                      ADD NEW ITEM
   376                  </button>
   377              ),
   378              view: null as any,
   379              edit: null
   380          });
   381  
   382      return (
   383          <div className='application-summary'>
   384              <EditablePanel
   385                  save={props.updateApp}
   386                  validate={input => ({
   387                      'spec.project': !input.spec.project && 'Project name is required',
   388                      'spec.destination.server': !input.spec.destination.server && input.spec.destination.hasOwnProperty('server') && 'Cluster server is required',
   389                      'spec.destination.name': !input.spec.destination.name && input.spec.destination.hasOwnProperty('name') && 'Cluster name is required'
   390                  })}
   391                  values={app}
   392                  title={app.metadata.name.toLocaleUpperCase()}
   393                  items={attributes}
   394              />
   395              <Consumer>
   396                  {ctx => (
   397                      <div className='white-box'>
   398                          <div className='white-box__details'>
   399                              <p>Sync Policy</p>
   400                              <div className='row white-box__details-row'>
   401                                  <div className='columns small-3'>{(app.spec.syncPolicy && app.spec.syncPolicy.automated && <span>Automated</span>) || <span>None</span>}</div>
   402                                  <div className='columns small-9'>
   403                                      {(app.spec.syncPolicy && app.spec.syncPolicy.automated && (
   404                                          <button className='argo-button argo-button--base' onClick={() => unsetAutoSync(ctx)}>
   405                                              <Spinner show={changeSync} style={{marginRight: '5px'}} />
   406                                              Disable Auto-Sync
   407                                          </button>
   408                                      )) || (
   409                                          <button
   410                                              className='argo-button argo-button--base'
   411                                              onClick={() =>
   412                                                  setAutoSync(ctx, 'Enable Auto-Sync?', 'Are you sure you want to enable automated application synchronization?', false, false)
   413                                              }>
   414                                              <Spinner show={changeSync} style={{marginRight: '5px'}} />
   415                                              Enable Auto-Sync
   416                                          </button>
   417                                      )}
   418                                  </div>
   419                              </div>
   420  
   421                              {app.spec.syncPolicy && app.spec.syncPolicy.automated && (
   422                                  <React.Fragment>
   423                                      <div className='row white-box__details-row'>
   424                                          <div className='columns small-3'>Prune Resources</div>
   425                                          <div className='columns small-9'>
   426                                              {(app.spec.syncPolicy.automated.prune && (
   427                                                  <button
   428                                                      className='argo-button argo-button--base'
   429                                                      onClick={() =>
   430                                                          setAutoSync(
   431                                                              ctx,
   432                                                              'Disable Prune Resources?',
   433                                                              'Are you sure you want to disable resource pruning during automated application synchronization?',
   434                                                              false,
   435                                                              app.spec.syncPolicy.automated.selfHeal
   436                                                          )
   437                                                      }>
   438                                                      Disable
   439                                                  </button>
   440                                              )) || (
   441                                                  <button
   442                                                      className='argo-button argo-button--base'
   443                                                      onClick={() =>
   444                                                          setAutoSync(
   445                                                              ctx,
   446                                                              'Enable Prune Resources?',
   447                                                              'Are you sure you want to enable resource pruning during automated application synchronization?',
   448                                                              true,
   449                                                              app.spec.syncPolicy.automated.selfHeal
   450                                                          )
   451                                                      }>
   452                                                      Enable
   453                                                  </button>
   454                                              )}
   455                                          </div>
   456                                      </div>
   457                                      <div className='row white-box__details-row'>
   458                                          <div className='columns small-3'>Self Heal</div>
   459                                          <div className='columns small-9'>
   460                                              {(app.spec.syncPolicy.automated.selfHeal && (
   461                                                  <button
   462                                                      className='argo-button argo-button--base'
   463                                                      onClick={() =>
   464                                                          setAutoSync(
   465                                                              ctx,
   466                                                              'Disable Self Heal?',
   467                                                              'Are you sure you want to disable automated self healing?',
   468                                                              app.spec.syncPolicy.automated.prune,
   469                                                              false
   470                                                          )
   471                                                      }>
   472                                                      Disable
   473                                                  </button>
   474                                              )) || (
   475                                                  <button
   476                                                      className='argo-button argo-button--base'
   477                                                      onClick={() =>
   478                                                          setAutoSync(
   479                                                              ctx,
   480                                                              'Enable Self Heal?',
   481                                                              'Are you sure you want to enable automated self healing?',
   482                                                              app.spec.syncPolicy.automated.prune,
   483                                                              true
   484                                                          )
   485                                                      }>
   486                                                      Enable
   487                                                  </button>
   488                                              )}
   489                                          </div>
   490                                      </div>
   491                                  </React.Fragment>
   492                              )}
   493                          </div>
   494                      </div>
   495                  )}
   496              </Consumer>
   497              <BadgePanel app={props.app.metadata.name} />
   498              <EditablePanel save={props.updateApp} values={app} title='Info' items={infoItems} onModeSwitch={() => setAdjustedCount(0)} />
   499          </div>
   500      );
   501  };