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

     1  /* eslint-disable no-prototype-builtins */
     2  import {AutocompleteField, Checkbox, DropDownMenu, ErrorNotification, FormField, FormSelect, HelpIcon, NotificationType} from 'argo-ui';
     3  import * as React from 'react';
     4  import {FormApi, Text} from 'react-form';
     5  import {
     6      ClipboardText,
     7      Cluster,
     8      DataLoader,
     9      EditablePanel,
    10      EditablePanelItem,
    11      Expandable,
    12      MapInputField,
    13      NumberField,
    14      Repo,
    15      Revision,
    16      RevisionHelpIcon
    17  } from '../../../shared/components';
    18  import {BadgePanel} from '../../../shared/components';
    19  import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context';
    20  import * as models from '../../../shared/models';
    21  import {services} from '../../../shared/services';
    22  
    23  import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options';
    24  import {RevisionFormField} from '../revision-form-field/revision-form-field';
    25  import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage, urlPattern, formatCreationTimestamp, getAppDefaultSource, getAppSpecDefaultSource, helpTip} from '../utils';
    26  import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
    27  import {ApplicationRetryView} from '../application-retry-view/application-retry-view';
    28  import {Link} from 'react-router-dom';
    29  import {EditNotificationSubscriptions, useEditNotificationSubscriptions} from './edit-notification-subscriptions';
    30  import {EditAnnotations} from './edit-annotations';
    31  
    32  import './application-summary.scss';
    33  import {DeepLinks} from '../../../shared/components/deep-links';
    34  
    35  function swap(array: any[], a: number, b: number) {
    36      array = array.slice();
    37      [array[a], array[b]] = [array[b], array[a]];
    38      return array;
    39  }
    40  
    41  function processPath(path: string) {
    42      if (path !== null && path !== undefined) {
    43          if (path === '.') {
    44              return '(root)';
    45          }
    46          return path;
    47      }
    48      return '';
    49  }
    50  
    51  export interface ApplicationSummaryProps {
    52      app: models.Application;
    53      updateApp: (app: models.Application, query: {validate?: boolean}) => Promise<any>;
    54  }
    55  
    56  export const ApplicationSummary = (props: ApplicationSummaryProps) => {
    57      const app = JSON.parse(JSON.stringify(props.app)) as models.Application;
    58      const source = getAppDefaultSource(app);
    59      const isHelm = source.hasOwnProperty('chart');
    60      const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL';
    61      const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
    62      const [destFormat, setDestFormat] = React.useState(initialState);
    63      const [, setChangeSync] = React.useState(false);
    64  
    65      const notificationSubscriptions = useEditNotificationSubscriptions(app.metadata.annotations || {});
    66      const updateApp = notificationSubscriptions.withNotificationSubscriptions(props.updateApp);
    67  
    68      const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0;
    69      const isHydrator = app.spec.sourceHydrator && true;
    70      const repoType = source.repoURL.startsWith('oci://') ? 'oci' : (source.hasOwnProperty('chart') && 'helm') || 'git';
    71  
    72      const attributes = [
    73          {
    74              title: 'PROJECT',
    75              view: <Link to={'/settings/projects/' + app.spec.project}>{app.spec.project}</Link>,
    76              edit: (formApi: FormApi) => (
    77                  <DataLoader load={() => services.projects.list('items.metadata.name').then(projs => projs.map(item => item.metadata.name))}>
    78                      {projects => <FormField formApi={formApi} field='spec.project' component={FormSelect} componentProps={{options: projects}} />}
    79                  </DataLoader>
    80              )
    81          },
    82          {
    83              title: 'LABELS',
    84              view: Object.keys(app.metadata.labels || {})
    85                  .map(label => `${label}=${app.metadata.labels[label]}`)
    86                  .join(' '),
    87              edit: (formApi: FormApi) => <FormField formApi={formApi} field='metadata.labels' component={MapInputField} />
    88          },
    89          {
    90              title: 'ANNOTATIONS',
    91              view: (
    92                  <Expandable height={48}>
    93                      {Object.keys(app.metadata.annotations || {})
    94                          .map(annotation => `${annotation}=${app.metadata.annotations[annotation]}`)
    95                          .join(' ')}
    96                  </Expandable>
    97              ),
    98              edit: (formApi: FormApi) => <EditAnnotations formApi={formApi} app={app} />
    99          },
   100          {
   101              title: 'NOTIFICATION SUBSCRIPTIONS',
   102              view: false, // eventually the subscription input values will be merged in 'ANNOTATIONS', therefore 'ANNOATIONS' section is responsible to represent subscription values,
   103              edit: () => <EditNotificationSubscriptions {...notificationSubscriptions} />
   104          },
   105          {
   106              title: 'CLUSTER',
   107              view: <Cluster server={app.spec.destination.server} name={app.spec.destination.name} showUrl={true} />,
   108              edit: (formApi: FormApi) => (
   109                  <DataLoader load={() => services.clusters.list().then(clusters => clusters.sort())}>
   110                      {clusters => {
   111                          return (
   112                              <div className='row'>
   113                                  {(destFormat.toUpperCase() === 'URL' && (
   114                                      <div className='columns small-10'>
   115                                          <FormField
   116                                              formApi={formApi}
   117                                              field='spec.destination.server'
   118                                              componentProps={{items: clusters.map(cluster => cluster.server)}}
   119                                              component={AutocompleteField}
   120                                          />
   121                                      </div>
   122                                  )) || (
   123                                      <div className='columns small-10'>
   124                                          <FormField
   125                                              formApi={formApi}
   126                                              field='spec.destination.name'
   127                                              componentProps={{items: clusters.map(cluster => cluster.name)}}
   128                                              component={AutocompleteField}
   129                                          />
   130                                      </div>
   131                                  )}
   132                                  <div className='columns small-2'>
   133                                      <div>
   134                                          <DropDownMenu
   135                                              anchor={() => (
   136                                                  <p>
   137                                                      {destFormat.toUpperCase()} <i className='fa fa-caret-down' />
   138                                                  </p>
   139                                              )}
   140                                              items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
   141                                                  title: type,
   142                                                  action: () => {
   143                                                      if (destFormat !== type) {
   144                                                          const updatedApp = formApi.getFormState().values as models.Application;
   145                                                          if (type === 'URL') {
   146                                                              updatedApp.spec.destination.server = '';
   147                                                              delete updatedApp.spec.destination.name;
   148                                                          } else {
   149                                                              updatedApp.spec.destination.name = '';
   150                                                              delete updatedApp.spec.destination.server;
   151                                                          }
   152                                                          formApi.setAllValues(updatedApp);
   153                                                          setDestFormat(type);
   154                                                      }
   155                                                  }
   156                                              }))}
   157                                          />
   158                                      </div>
   159                                  </div>
   160                              </div>
   161                          );
   162                      }}
   163                  </DataLoader>
   164              )
   165          },
   166          {
   167              title: 'NAMESPACE',
   168              view: <ClipboardText text={app.spec.destination.namespace} />,
   169              edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.destination.namespace' component={Text} />
   170          },
   171          {
   172              title: 'CREATED AT',
   173              view: formatCreationTimestamp(app.metadata.creationTimestamp)
   174          },
   175          !hasMultipleSources && {
   176              title: 'REPO URL',
   177              view: <Repo url={source?.repoURL} />,
   178              edit: (formApi: FormApi) => <FormField formApi={formApi} field={getRepoField(isHydrator)} component={Text} />
   179          },
   180          ...(!hasMultipleSources
   181              ? isHelm
   182                  ? [
   183                        {
   184                            title: 'CHART',
   185                            view: <span>{source && `${source.chart}:${source.targetRevision}`}</span>,
   186                            edit: (formApi: FormApi) =>
   187                                hasMultipleSources ? (
   188                                    helpTip('CHART is not editable for applications with multiple sources. You can edit them in the "Manifest" tab.')
   189                                ) : (
   190                                    <DataLoader
   191                                        input={{repoURL: getAppSpecDefaultSource(formApi.getFormState().values.spec).repoURL}}
   192                                        load={src => services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())}>
   193                                        {(charts: models.HelmChart[]) => (
   194                                            <div className='row'>
   195                                                <div className='columns small-8'>
   196                                                    <FormField
   197                                                        formApi={formApi}
   198                                                        field='spec.source.chart'
   199                                                        component={AutocompleteField}
   200                                                        componentProps={{
   201                                                            items: charts.map(chart => chart.name),
   202                                                            filterSuggestions: true
   203                                                        }}
   204                                                    />
   205                                                </div>
   206                                                <DataLoader
   207                                                    input={{charts, chart: getAppSpecDefaultSource(formApi.getFormState().values.spec).chart}}
   208                                                    load={async data => {
   209                                                        const chartInfo = data.charts.find(chart => chart.name === data.chart);
   210                                                        return (chartInfo && chartInfo.versions) || new Array<string>();
   211                                                    }}>
   212                                                    {(versions: string[]) => (
   213                                                        <div className='columns small-4'>
   214                                                            <FormField
   215                                                                formApi={formApi}
   216                                                                field='spec.source.targetRevision'
   217                                                                component={AutocompleteField}
   218                                                                componentProps={{
   219                                                                    items: versions
   220                                                                }}
   221                                                            />
   222                                                            <RevisionHelpIcon type='helm' top='0' />
   223                                                        </div>
   224                                                    )}
   225                                                </DataLoader>
   226                                            </div>
   227                                        )}
   228                                    </DataLoader>
   229                                )
   230                        }
   231                    ]
   232                  : [
   233                        {
   234                            title: 'TARGET REVISION',
   235                            view: <Revision repoUrl={source.repoURL} revision={source.targetRevision || 'HEAD'} />,
   236                            edit: (formApi: FormApi) =>
   237                                hasMultipleSources ? (
   238                                    helpTip('TARGET REVISION is not editable for applications with multiple sources. You can edit them in the "Manifest" tab.')
   239                                ) : (
   240                                    <RevisionFormField
   241                                        helpIconTop={'0'}
   242                                        hideLabel={true}
   243                                        formApi={formApi}
   244                                        fieldValue={getTargetRevisionField(isHydrator)}
   245                                        repoURL={source.repoURL}
   246                                        repoType={repoType}
   247                                    />
   248                                )
   249                        },
   250                        {
   251                            title: 'PATH',
   252                            view: (
   253                                <Revision repoUrl={source.repoURL} revision={source.targetRevision || 'HEAD'} path={source.path} isForPath={true}>
   254                                    {processPath(source.path)}
   255                                </Revision>
   256                            ),
   257                            edit: (formApi: FormApi) =>
   258                                hasMultipleSources ? (
   259                                    helpTip('PATH is not editable for applications with multiple sources. You can edit them in the "Manifest" tab.')
   260                                ) : (
   261                                    <FormField formApi={formApi} field={getPathField(isHydrator)} component={Text} />
   262                                )
   263                        }
   264                    ]
   265              : []),
   266          {
   267              title: 'REVISION HISTORY LIMIT',
   268              view: app.spec.revisionHistoryLimit,
   269              edit: (formApi: FormApi) => (
   270                  <div style={{position: 'relative'}}>
   271                      <FormField
   272                          formApi={formApi}
   273                          field='spec.revisionHistoryLimit'
   274                          componentProps={{style: {paddingRight: '1em', right: '1em'}, placeholder: '10'}}
   275                          component={NumberField}
   276                      />
   277                      <div style={{position: 'absolute', right: '0', top: '0'}}>
   278                          <HelpIcon
   279                              title='This limits the number of items kept in the apps revision history.
   280      This should only be changed in exceptional circumstances.
   281      Setting to zero will store no history. This will reduce storage used.
   282      Increasing will increase the space used to store the history, so we do not recommend increasing it.
   283      Default is 10.'
   284                          />
   285                      </div>
   286                  </div>
   287              )
   288          },
   289          {
   290              title: 'SYNC OPTIONS',
   291              view: (
   292                  <div style={{display: 'flex', flexWrap: 'wrap'}}>
   293                      {((app.spec.syncPolicy || {}).syncOptions || []).map(opt =>
   294                          opt.endsWith('=true') || opt.endsWith('=false') ? (
   295                              <div key={opt} style={{marginRight: '10px'}}>
   296                                  <i className={`fa fa-${opt.includes('=true') ? 'check-square' : 'times'}`} /> {opt.replace('=true', '').replace('=false', '')}
   297                              </div>
   298                          ) : (
   299                              <div key={opt} style={{marginRight: '10px'}}>
   300                                  {opt}
   301                              </div>
   302                          )
   303                      )}
   304                  </div>
   305              ),
   306              edit: (formApi: FormApi) => (
   307                  <div>
   308                      <FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
   309                  </div>
   310              )
   311          },
   312          {
   313              title: 'RETRY OPTIONS',
   314              view: <ApplicationRetryView initValues={app.spec.syncPolicy ? app.spec.syncPolicy.retry : null} />,
   315              edit: (formApi: FormApi) => (
   316                  <div>
   317                      <ApplicationRetryOptions formApi={formApi} initValues={app.spec.syncPolicy ? app.spec.syncPolicy.retry : null} field='spec.syncPolicy.retry' />
   318                  </div>
   319              )
   320          },
   321          {
   322              title: 'STATUS',
   323              view: (
   324                  <span>
   325                      <ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status} {syncStatusMessage(app)}
   326                  </span>
   327              )
   328          },
   329          {
   330              title: 'HEALTH',
   331              view: (
   332                  <span>
   333                      <HealthStatusIcon state={app.status.health} /> {app.status.health.status}
   334                  </span>
   335              )
   336          },
   337          {
   338              title: 'LINKS',
   339              view: (
   340                  <DataLoader load={() => services.applications.getLinks(app.metadata.name, app.metadata.namespace)} input={app} key='appLinks'>
   341                      {(links: models.LinksResponse) => <DeepLinks links={links.items} />}
   342                  </DataLoader>
   343              )
   344          }
   345      ];
   346  
   347      const urls = app.status.summary.externalURLs || [];
   348      if (urls.length > 0) {
   349          attributes.push({
   350              title: 'URLs',
   351              view: (
   352                  <div className='application-summary__links-rows'>
   353                      {urls
   354                          .map(item => item.split('|'))
   355                          .map((parts, i) => (
   356                              <div className='application-summary__links-row'>
   357                                  <a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='_blank'>
   358                                      {parts[0]} &nbsp;
   359                                  </a>
   360                              </div>
   361                          ))}
   362                  </div>
   363              )
   364          });
   365      }
   366  
   367      if ((app.status.summary.images || []).length) {
   368          attributes.push({
   369              title: 'IMAGES',
   370              view: (
   371                  <div className='application-summary__labels'>
   372                      {(app.status.summary.images || []).sort().map(image => (
   373                          <span className='application-summary__label' key={image}>
   374                              {image}
   375                          </span>
   376                      ))}
   377                  </div>
   378              )
   379          });
   380      }
   381  
   382      async function setAutoSync(ctx: ContextApis, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean, enable: boolean) {
   383          const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
   384          if (confirmed) {
   385              try {
   386                  setChangeSync(true);
   387                  const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
   388                  if (!updatedApp.spec.syncPolicy) {
   389                      updatedApp.spec.syncPolicy = {};
   390                  }
   391  
   392                  updatedApp.spec.syncPolicy.automated = {prune, selfHeal, enabled: enable};
   393                  await updateApp(updatedApp, {validate: false});
   394              } catch (e) {
   395                  ctx.notifications.show({
   396                      content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}:`} e={e} />,
   397                      type: NotificationType.Error
   398                  });
   399              } finally {
   400                  setChangeSync(false);
   401              }
   402          }
   403      }
   404  
   405      const items = app.spec.info || [];
   406      const [adjustedCount, setAdjustedCount] = React.useState(0);
   407  
   408      const added = new Array<{name: string; value: string; key: string}>();
   409      for (let i = 0; i < adjustedCount; i++) {
   410          added.push({name: '', value: '', key: (items.length + i).toString()});
   411      }
   412      for (let i = 0; i > adjustedCount; i--) {
   413          items.pop();
   414      }
   415      const allItems = items.concat(added);
   416      const infoItems: EditablePanelItem[] = allItems
   417          .map((info, i) => ({
   418              key: i.toString(),
   419              title: info.name,
   420              view: info.value.match(urlPattern) ? (
   421                  <a href={info.value} target='__blank'>
   422                      {info.value}
   423                  </a>
   424              ) : (
   425                  info.value
   426              ),
   427              titleEdit: (formApi: FormApi) => (
   428                  <React.Fragment>
   429                      {i > 0 && (
   430                          <i
   431                              className='fa fa-sort-up application-summary__sort-icon'
   432                              onClick={() => {
   433                                  formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i - 1));
   434                              }}
   435                          />
   436                      )}
   437                      <FormField formApi={formApi} field={`spec.info[${[i]}].name`} component={Text} componentProps={{style: {width: '99%'}}} />
   438                      {i < allItems.length - 1 && (
   439                          <i
   440                              className='fa fa-sort-down application-summary__sort-icon'
   441                              onClick={() => {
   442                                  formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i + 1));
   443                              }}
   444                          />
   445                      )}
   446                  </React.Fragment>
   447              ),
   448              edit: (formApi: FormApi) => (
   449                  <React.Fragment>
   450                      <FormField formApi={formApi} field={`spec.info[${[i]}].value`} component={Text} />
   451                      <i
   452                          className='fa fa-times application-summary__remove-icon'
   453                          onClick={() => {
   454                              const values = (formApi.getFormState().values.spec.info || []) as Array<any>;
   455                              formApi.setValue('spec.info', [...values.slice(0, i), ...values.slice(i + 1, values.length)]);
   456                              setAdjustedCount(adjustedCount - 1);
   457                          }}
   458                      />
   459                  </React.Fragment>
   460              )
   461          }))
   462          .concat({
   463              key: '-1',
   464              title: '',
   465              titleEdit: () => (
   466                  <button
   467                      className='argo-button argo-button--base'
   468                      onClick={() => {
   469                          setAdjustedCount(adjustedCount + 1);
   470                      }}>
   471                      ADD NEW ITEM
   472                  </button>
   473              ),
   474              view: null as any,
   475              edit: null
   476          });
   477  
   478      return (
   479          <div className='application-summary'>
   480              <EditablePanel
   481                  save={updateApp}
   482                  view={hasMultipleSources ? <>This is a multi-source app, see the Sources tab for repository URLs and source-related information.</> : null}
   483                  validate={input => ({
   484                      'spec.project': !input.spec.project && 'Project name is required',
   485                      'spec.destination.server': !input.spec.destination.server && input.spec.destination.hasOwnProperty('server') && 'Cluster server is required',
   486                      'spec.destination.name': !input.spec.destination.name && input.spec.destination.hasOwnProperty('name') && 'Cluster name is required'
   487                  })}
   488                  values={app}
   489                  title={app.metadata.name.toLocaleUpperCase()}
   490                  items={attributes}
   491                  onModeSwitch={() => notificationSubscriptions.onResetNotificationSubscriptions()}
   492              />
   493              <Consumer>
   494                  {ctx => (
   495                      <div className='white-box'>
   496                          <div className='white-box__details'>
   497                              <p>SYNC POLICY</p>
   498                              <div className='row white-box__details-row'>
   499                                  <div className='columns small-3'>
   500                                      {app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false ? <span>AUTOMATED</span> : <span>NONE</span>}
   501                                  </div>
   502                              </div>
   503                              <div className='row white-box__details-row'>
   504                                  <div className='columns small-12'>
   505                                      <div className='checkbox-container'>
   506                                          <Checkbox
   507                                              onChange={async (val: boolean) => {
   508                                                  const automated = app.spec.syncPolicy?.automated || {prune: false, selfHeal: false};
   509                                                  setAutoSync(
   510                                                      ctx,
   511                                                      val ? 'Enable Auto-Sync?' : 'Disable Auto-Sync?',
   512                                                      val
   513                                                          ? 'If checked, application will automatically sync when changes are detected'
   514                                                          : 'Are you sure you want to disable automated application synchronization',
   515                                                      automated.prune,
   516                                                      automated.selfHeal,
   517                                                      val
   518                                                  );
   519                                              }}
   520                                              checked={app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false}
   521                                              id='enable-auto-sync'
   522                                          />
   523                                          <label htmlFor='enable-auto-sync'>ENABLE AUTO-SYNC</label>
   524                                          <HelpIcon title='If checked, application will automatically sync when changes are detected' />
   525                                      </div>
   526                                  </div>
   527                              </div>
   528                              {app.spec.syncPolicy && app.spec.syncPolicy.automated && (
   529                                  <React.Fragment>
   530                                      <div className='row white-box__details-row'>
   531                                          <div className='columns small-12'>
   532                                              <div className='checkbox-container'>
   533                                                  <Checkbox
   534                                                      onChange={async (prune: boolean) => {
   535                                                          const automated = app.spec.syncPolicy?.automated || {selfHeal: false, enabled: false};
   536                                                          setAutoSync(
   537                                                              ctx,
   538                                                              prune ? 'Enable Prune Resources?' : 'Disable Prune Resources?',
   539                                                              prune
   540                                                                  ? 'Are you sure you want to enable resource pruning during automated application synchronization?'
   541                                                                  : 'Are you sure you want to disable resource pruning during automated application synchronization?',
   542                                                              prune,
   543                                                              automated.selfHeal,
   544                                                              automated.enabled
   545                                                          );
   546                                                      }}
   547                                                      checked={!!app.spec.syncPolicy?.automated?.prune}
   548                                                      id='prune-resources'
   549                                                  />
   550                                                  <label htmlFor='prune-resources'>PRUNE RESOURCES</label>
   551                                              </div>
   552                                          </div>
   553                                      </div>
   554                                      <div className='row white-box__details-row'>
   555                                          <div className='columns small-12'>
   556                                              <div className='checkbox-container'>
   557                                                  <Checkbox
   558                                                      onChange={async (selfHeal: boolean) => {
   559                                                          const automated = app.spec.syncPolicy?.automated || {prune: false, enabled: false};
   560                                                          setAutoSync(
   561                                                              ctx,
   562                                                              selfHeal ? 'Enable Self Heal?' : 'Disable Self Heal?',
   563                                                              selfHeal
   564                                                                  ? 'If checked, application will automatically sync when changes are detected'
   565                                                                  : 'Are you sure you want to enable automated self healing?',
   566                                                              automated.prune,
   567                                                              selfHeal,
   568                                                              automated.enabled
   569                                                          );
   570                                                      }}
   571                                                      checked={!!app.spec.syncPolicy?.automated?.selfHeal}
   572                                                      id='self-heal'
   573                                                  />
   574                                                  <label htmlFor='self-heal'>SELF HEAL</label>
   575                                              </div>
   576                                          </div>
   577                                      </div>
   578                                  </React.Fragment>
   579                              )}
   580                          </div>
   581                      </div>
   582                  )}
   583              </Consumer>
   584              <BadgePanel app={props.app.metadata.name} appNamespace={props.app.metadata.namespace} nsEnabled={useAuthSettingsCtx?.appsInAnyNamespaceEnabled} />
   585              <EditablePanel
   586                  save={updateApp}
   587                  values={app}
   588                  title='INFO'
   589                  items={infoItems}
   590                  onModeSwitch={() => {
   591                      setAdjustedCount(0);
   592                      notificationSubscriptions.onResetNotificationSubscriptions();
   593                  }}
   594              />
   595          </div>
   596      );
   597  };
   598  
   599  /** Get the repository URL field based on the hydrator status */
   600  const getRepoField = (isHydrator: boolean): string => {
   601      const repoURLField = isHydrator ? 'spec.sourceHydrator.drySource.repoURL' : 'spec.source.repoURL';
   602      return repoURLField;
   603  };
   604  
   605  const getTargetRevisionField = (isHydrator: boolean): string => {
   606      const targetRevisionField = isHydrator ? 'spec.sourceHydrator.drySource.targetRevision' : 'spec.source.targetRevision';
   607      return targetRevisionField;
   608  };
   609  
   610  const getPathField = (isHydrator: boolean): string => {
   611      const pathField = isHydrator ? 'spec.sourceHydrator.drySource.path' : 'spec.source.path';
   612      return pathField;
   613  };