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

     1  import {Autocomplete, ErrorNotification, MockupList, NotificationType, SlidingPanel, Toolbar, Tooltip} from 'argo-ui';
     2  import * as classNames from 'classnames';
     3  import * as React from 'react';
     4  import * as ReactDOM from 'react-dom';
     5  import {Key, KeybindingContext, KeybindingProvider} from 'argo-ui/v2';
     6  import {RouteComponentProps} from 'react-router';
     7  import {combineLatest, from, merge, Observable} from 'rxjs';
     8  import {bufferTime, delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
     9  import {AddAuthToToolbar, ClusterCtx, DataLoader, EmptyState, ObservableQuery, Page, Paginate, Query, Spinner} from '../../../shared/components';
    10  import {AuthSettingsCtx, Consumer, Context, ContextApis} from '../../../shared/context';
    11  import * as models from '../../../shared/models';
    12  import {AppsListViewKey, AppsListPreferences, AppsListViewType, HealthStatusBarPreferences, services} from '../../../shared/services';
    13  import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel';
    14  import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
    15  import {ApplicationsSyncPanel} from '../applications-sync-panel/applications-sync-panel';
    16  import * as AppUtils from '../utils';
    17  import {ApplicationsFilter, FilteredApp, getFilterResults} from './applications-filter';
    18  import {ApplicationsStatusBar} from './applications-status-bar';
    19  import {ApplicationsSummary} from './applications-summary';
    20  import {ApplicationsTable} from './applications-table';
    21  import {ApplicationTiles} from './applications-tiles';
    22  import {ApplicationsRefreshPanel} from '../applications-refresh-panel/applications-refresh-panel';
    23  import {useSidebarTarget} from '../../../sidebar/sidebar';
    24  
    25  import './applications-list.scss';
    26  import './flex-top-bar.scss';
    27  
    28  const EVENTS_BUFFER_TIMEOUT = 500;
    29  const WATCH_RETRY_TIMEOUT = 500;
    30  
    31  // The applications list/watch API supports only selected set of fields.
    32  // Make sure to register any new fields in the `appFields` map of `pkg/apiclient/application/forwarder_overwrite.go`.
    33  const APP_FIELDS = [
    34      'metadata.name',
    35      'metadata.namespace',
    36      'metadata.annotations',
    37      'metadata.labels',
    38      'metadata.creationTimestamp',
    39      'metadata.deletionTimestamp',
    40      'spec',
    41      'operation.sync',
    42      'status.sourceHydrator',
    43      'status.sync.status',
    44      'status.sync.revision',
    45      'status.health',
    46      'status.operationState.phase',
    47      'status.operationState.finishedAt',
    48      'status.operationState.operation.sync',
    49      'status.summary',
    50      'status.resources'
    51  ];
    52  const APP_LIST_FIELDS = ['metadata.resourceVersion', ...APP_FIELDS.map(field => `items.${field}`)];
    53  const APP_WATCH_FIELDS = ['result.type', ...APP_FIELDS.map(field => `result.application.${field}`)];
    54  
    55  function loadApplications(projects: string[], appNamespace: string): Observable<models.Application[]> {
    56      return from(services.applications.list(projects, {appNamespace, fields: APP_LIST_FIELDS})).pipe(
    57          mergeMap(applicationsList => {
    58              const applications = applicationsList.items;
    59              return merge(
    60                  from([applications]),
    61                  services.applications
    62                      .watch({projects, resourceVersion: applicationsList.metadata.resourceVersion}, {fields: APP_WATCH_FIELDS})
    63                      .pipe(repeat())
    64                      .pipe(retryWhen(errors => errors.pipe(delay(WATCH_RETRY_TIMEOUT))))
    65                      // batch events to avoid constant re-rendering and improve UI performance
    66                      .pipe(bufferTime(EVENTS_BUFFER_TIMEOUT))
    67                      .pipe(
    68                          map(appChanges => {
    69                              appChanges.forEach(appChange => {
    70                                  const index = applications.findIndex(item => AppUtils.appInstanceName(item) === AppUtils.appInstanceName(appChange.application));
    71                                  switch (appChange.type) {
    72                                      case 'DELETED':
    73                                          if (index > -1) {
    74                                              applications.splice(index, 1);
    75                                          }
    76                                          break;
    77                                      default:
    78                                          if (index > -1) {
    79                                              applications[index] = appChange.application;
    80                                          } else {
    81                                              applications.unshift(appChange.application);
    82                                          }
    83                                          break;
    84                                  }
    85                              });
    86                              return {applications, updated: appChanges.length > 0};
    87                          })
    88                      )
    89                      .pipe(filter(item => item.updated))
    90                      .pipe(map(item => item.applications))
    91              );
    92          })
    93      );
    94  }
    95  
    96  const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: number; search: string}) => React.ReactNode}) => (
    97      <ObservableQuery>
    98          {q => (
    99              <DataLoader
   100                  load={() =>
   101                      combineLatest([services.viewPreferences.getPreferences().pipe(map(item => item.appList)), q]).pipe(
   102                          map(items => {
   103                              const params = items[1];
   104                              const viewPref: AppsListPreferences = {...items[0]};
   105                              if (params.get('proj') != null) {
   106                                  viewPref.projectsFilter = params
   107                                      .get('proj')
   108                                      .split(',')
   109                                      .filter(item => !!item);
   110                              }
   111                              if (params.get('sync') != null) {
   112                                  viewPref.syncFilter = params
   113                                      .get('sync')
   114                                      .split(',')
   115                                      .filter(item => !!item);
   116                              }
   117                              if (params.get('autoSync') != null) {
   118                                  viewPref.autoSyncFilter = params
   119                                      .get('autoSync')
   120                                      .split(',')
   121                                      .filter(item => !!item);
   122                              }
   123                              if (params.get('health') != null) {
   124                                  viewPref.healthFilter = params
   125                                      .get('health')
   126                                      .split(',')
   127                                      .filter(item => !!item);
   128                              }
   129                              if (params.get('namespace') != null) {
   130                                  viewPref.namespacesFilter = params
   131                                      .get('namespace')
   132                                      .split(',')
   133                                      .filter(item => !!item);
   134                              }
   135                              if (params.get('cluster') != null) {
   136                                  viewPref.clustersFilter = params
   137                                      .get('cluster')
   138                                      .split(',')
   139                                      .filter(item => !!item);
   140                              }
   141                              if (params.get('showFavorites') != null) {
   142                                  viewPref.showFavorites = params.get('showFavorites') === 'true';
   143                              }
   144                              if (params.get('view') != null) {
   145                                  viewPref.view = params.get('view') as AppsListViewType;
   146                              }
   147                              if (params.get('labels') != null) {
   148                                  viewPref.labelsFilter = params
   149                                      .get('labels')
   150                                      .split(',')
   151                                      .map(decodeURIComponent)
   152                                      .filter(item => !!item);
   153                              }
   154                              return {...viewPref, page: parseInt(params.get('page') || '0', 10), search: params.get('search') || ''};
   155                          })
   156                      )
   157                  }>
   158                  {pref => children(pref)}
   159              </DataLoader>
   160          )}
   161      </ObservableQuery>
   162  );
   163  
   164  function filterApps(applications: models.Application[], pref: AppsListPreferences, search: string): {filteredApps: models.Application[]; filterResults: FilteredApp[]} {
   165      applications = applications.map(app => {
   166          let isAppOfAppsPattern = false;
   167          for (const resource of app.status.resources) {
   168              if (resource.kind === 'Application') {
   169                  isAppOfAppsPattern = true;
   170                  break;
   171              }
   172          }
   173          return {...app, isAppOfAppsPattern};
   174      });
   175      const filterResults = getFilterResults(applications, pref);
   176      return {
   177          filterResults,
   178          filteredApps: filterResults.filter(
   179              app => (search === '' || app.metadata.name.includes(search) || app.metadata.namespace.includes(search)) && Object.values(app.filterResult).every(val => val)
   180          )
   181      };
   182  }
   183  
   184  function tryJsonParse(input: string) {
   185      try {
   186          return (input && JSON.parse(input)) || null;
   187      } catch {
   188          return null;
   189      }
   190  }
   191  
   192  const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Application[]}) => {
   193      const {content, ctx, apps} = {...props};
   194  
   195      const searchBar = React.useRef<HTMLDivElement>(null);
   196  
   197      const query = new URLSearchParams(window.location.search);
   198      const appInput = tryJsonParse(query.get('new'));
   199  
   200      const {useKeybinding} = React.useContext(KeybindingContext);
   201      const [isFocused, setFocus] = React.useState(false);
   202      const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
   203  
   204      useKeybinding({
   205          keys: Key.SLASH,
   206          action: () => {
   207              if (searchBar.current && !appInput) {
   208                  searchBar.current.querySelector('input').focus();
   209                  setFocus(true);
   210                  return true;
   211              }
   212              return false;
   213          }
   214      });
   215  
   216      useKeybinding({
   217          keys: Key.ESCAPE,
   218          action: () => {
   219              if (searchBar.current && !appInput && isFocused) {
   220                  searchBar.current.querySelector('input').blur();
   221                  setFocus(false);
   222                  return true;
   223              }
   224              return false;
   225          }
   226      });
   227  
   228      return (
   229          <Autocomplete
   230              filterSuggestions={true}
   231              renderInput={inputProps => (
   232                  <div className='applications-list__search' ref={searchBar}>
   233                      <i
   234                          className='fa fa-search'
   235                          style={{marginRight: '9px', cursor: 'pointer'}}
   236                          onClick={() => {
   237                              if (searchBar.current) {
   238                                  searchBar.current.querySelector('input').focus();
   239                              }
   240                          }}
   241                      />
   242                      <input
   243                          {...inputProps}
   244                          onFocus={e => {
   245                              e.target.select();
   246                              if (inputProps.onFocus) {
   247                                  inputProps.onFocus(e);
   248                              }
   249                          }}
   250                          style={{fontSize: '14px'}}
   251                          className='argo-field'
   252                          placeholder='Search applications...'
   253                      />
   254                      <div className='keyboard-hint'>/</div>
   255                      {content && (
   256                          <i className='fa fa-times' onClick={() => ctx.navigation.goto('.', {search: null}, {replace: true})} style={{cursor: 'pointer', marginLeft: '5px'}} />
   257                      )}
   258                  </div>
   259              )}
   260              wrapperProps={{className: 'applications-list__search-wrapper'}}
   261              renderItem={item => (
   262                  <React.Fragment>
   263                      <i className='icon argo-icon-application' /> {item.label}
   264                  </React.Fragment>
   265              )}
   266              onSelect={val => {
   267                  const selectedApp = apps?.find(app => {
   268                      const qualifiedName = AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled);
   269                      return qualifiedName === val;
   270                  });
   271                  if (selectedApp) {
   272                      ctx.navigation.goto(`/${AppUtils.getAppUrl(selectedApp)}`);
   273                  }
   274              }}
   275              onChange={e => ctx.navigation.goto('.', {search: e.target.value}, {replace: true})}
   276              value={content || ''}
   277              items={apps.map(app => AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled))}
   278          />
   279      );
   280  };
   281  
   282  const FlexTopBar = (props: {toolbar: Toolbar | Observable<Toolbar>}) => {
   283      const ctx = React.useContext(Context);
   284      const loadToolbar = AddAuthToToolbar(props.toolbar, ctx);
   285      return (
   286          <React.Fragment>
   287              <div className='top-bar row flex-top-bar' key='tool-bar'>
   288                  <DataLoader load={() => loadToolbar}>
   289                      {toolbar => (
   290                          <React.Fragment>
   291                              <div className='flex-top-bar__actions'>
   292                                  {toolbar.actionMenu && (
   293                                      <React.Fragment>
   294                                          {toolbar.actionMenu.items.map((item, i) => (
   295                                              <Tooltip className='custom-tooltip' content={item.title}>
   296                                                  <button
   297                                                      disabled={!!item.disabled}
   298                                                      qe-id={item.qeId}
   299                                                      className='argo-button argo-button--base'
   300                                                      onClick={() => item.action()}
   301                                                      style={{marginRight: 2}}
   302                                                      key={i}>
   303                                                      {item.iconClassName && <i className={item.iconClassName} style={{marginLeft: '-5px', marginRight: '5px'}} />}
   304                                                      <span className='show-for-large'>{item.title}</span>
   305                                                  </button>
   306                                              </Tooltip>
   307                                          ))}
   308                                      </React.Fragment>
   309                                  )}
   310                              </div>
   311                              <div className='flex-top-bar__tools'>{toolbar.tools}</div>
   312                          </React.Fragment>
   313                      )}
   314                  </DataLoader>
   315              </div>
   316              <div className='flex-top-bar__padder' />
   317          </React.Fragment>
   318      );
   319  };
   320  
   321  export const ApplicationsList = (props: RouteComponentProps<{}>) => {
   322      const query = new URLSearchParams(props.location.search);
   323      const appInput = tryJsonParse(query.get('new'));
   324      const syncAppsInput = tryJsonParse(query.get('syncApps'));
   325      const refreshAppsInput = tryJsonParse(query.get('refreshApps'));
   326      const [createApi, setCreateApi] = React.useState(null);
   327      const clusters = React.useMemo(() => services.clusters.list(), []);
   328      const [isAppCreatePending, setAppCreatePending] = React.useState(false);
   329      const loaderRef = React.useRef<DataLoader>();
   330      const {List, Summary, Tiles} = AppsListViewKey;
   331  
   332      function refreshApp(appName: string, appNamespace: string) {
   333          // app refreshing might be done too quickly so that UI might miss it due to event batching
   334          // add refreshing annotation in the UI to improve user experience
   335          if (loaderRef.current) {
   336              const applications = loaderRef.current.getData() as models.Application[];
   337              const app = applications.find(item => item.metadata.name === appName && item.metadata.namespace === appNamespace);
   338              if (app) {
   339                  AppUtils.setAppRefreshing(app);
   340                  loaderRef.current.setData(applications);
   341              }
   342          }
   343          services.applications.get(appName, appNamespace, 'normal');
   344      }
   345  
   346      function onFilterPrefChanged(ctx: ContextApis, newPref: AppsListPreferences) {
   347          services.viewPreferences.updatePreferences({appList: newPref});
   348          ctx.navigation.goto(
   349              '.',
   350              {
   351                  proj: newPref.projectsFilter.join(','),
   352                  sync: newPref.syncFilter.join(','),
   353                  autoSync: newPref.autoSyncFilter.join(','),
   354                  health: newPref.healthFilter.join(','),
   355                  namespace: newPref.namespacesFilter.join(','),
   356                  cluster: newPref.clustersFilter.join(','),
   357                  labels: newPref.labelsFilter.map(encodeURIComponent).join(',')
   358              },
   359              {replace: true}
   360          );
   361      }
   362  
   363      function getPageTitle(view: string) {
   364          switch (view) {
   365              case List:
   366                  return 'Applications List';
   367              case Tiles:
   368                  return 'Applications Tiles';
   369              case Summary:
   370                  return 'Applications Summary';
   371          }
   372          return '';
   373      }
   374  
   375      const sidebarTarget = useSidebarTarget();
   376  
   377      return (
   378          <ClusterCtx.Provider value={clusters}>
   379              <KeybindingProvider>
   380                  <Consumer>
   381                      {ctx => (
   382                          <ViewPref>
   383                              {pref => (
   384                                  <Page
   385                                      key={pref.view}
   386                                      title={getPageTitle(pref.view)}
   387                                      useTitleOnly={true}
   388                                      toolbar={{breadcrumbs: [{title: 'Applications', path: '/applications'}]}}
   389                                      hideAuth={true}>
   390                                      <DataLoader
   391                                          input={pref.projectsFilter?.join(',')}
   392                                          ref={loaderRef}
   393                                          load={() => AppUtils.handlePageVisibility(() => loadApplications(pref.projectsFilter, query.get('appNamespace')))}
   394                                          loadingRenderer={() => (
   395                                              <div className='argo-container'>
   396                                                  <MockupList height={100} marginTop={30} />
   397                                              </div>
   398                                          )}>
   399                                          {(applications: models.Application[]) => {
   400                                              const healthBarPrefs = pref.statusBarView || ({} as HealthStatusBarPreferences);
   401                                              const {filteredApps, filterResults} = filterApps(applications, pref, pref.search);
   402                                              const handleCreatePanelClose = async () => {
   403                                                  const outsideDiv = document.querySelector('.sliding-panel__outside');
   404                                                  const closeButton = document.querySelector('.sliding-panel__close');
   405  
   406                                                  if (outsideDiv && closeButton && closeButton !== document.activeElement) {
   407                                                      const confirmed = await ctx.popup.confirm('Close Panel', 'Closing this panel will discard all entered values. Continue?');
   408                                                      if (confirmed) {
   409                                                          ctx.navigation.goto('.', {new: null}, {replace: true});
   410                                                      }
   411                                                  } else if (closeButton === document.activeElement) {
   412                                                      // If the close button is focused or clicked, close without confirmation
   413                                                      ctx.navigation.goto('.', {new: null}, {replace: true});
   414                                                  }
   415                                              };
   416                                              return (
   417                                                  <React.Fragment>
   418                                                      <FlexTopBar
   419                                                          toolbar={{
   420                                                              tools: (
   421                                                                  <React.Fragment key='app-list-tools'>
   422                                                                      <Query>{q => <SearchBar content={q.get('search')} apps={applications} ctx={ctx} />}</Query>
   423                                                                      <Tooltip content='Toggle Health Status Bar'>
   424                                                                          <button
   425                                                                              className={`applications-list__accordion argo-button argo-button--base${
   426                                                                                  healthBarPrefs.showHealthStatusBar ? '-o' : ''
   427                                                                              }`}
   428                                                                              style={{border: 'none'}}
   429                                                                              onClick={() => {
   430                                                                                  healthBarPrefs.showHealthStatusBar = !healthBarPrefs.showHealthStatusBar;
   431                                                                                  services.viewPreferences.updatePreferences({
   432                                                                                      appList: {
   433                                                                                          ...pref,
   434                                                                                          statusBarView: {
   435                                                                                              ...healthBarPrefs,
   436                                                                                              showHealthStatusBar: healthBarPrefs.showHealthStatusBar
   437                                                                                          }
   438                                                                                      }
   439                                                                                  });
   440                                                                              }}>
   441                                                                              <i className={`fas fa-ruler-horizontal`} />
   442                                                                          </button>
   443                                                                      </Tooltip>
   444                                                                      <div className='applications-list__view-type' style={{marginLeft: 'auto'}}>
   445                                                                          <i
   446                                                                              className={classNames('fa fa-th', {selected: pref.view === Tiles}, 'menu_icon')}
   447                                                                              title='Tiles'
   448                                                                              onClick={() => {
   449                                                                                  ctx.navigation.goto('.', {view: Tiles});
   450                                                                                  services.viewPreferences.updatePreferences({appList: {...pref, view: Tiles}});
   451                                                                              }}
   452                                                                          />
   453                                                                          <i
   454                                                                              className={classNames('fa fa-th-list', {selected: pref.view === List}, 'menu_icon')}
   455                                                                              title='List'
   456                                                                              onClick={() => {
   457                                                                                  ctx.navigation.goto('.', {view: List});
   458                                                                                  services.viewPreferences.updatePreferences({appList: {...pref, view: List}});
   459                                                                              }}
   460                                                                          />
   461                                                                          <i
   462                                                                              className={classNames('fa fa-chart-pie', {selected: pref.view === Summary}, 'menu_icon')}
   463                                                                              title='Summary'
   464                                                                              onClick={() => {
   465                                                                                  ctx.navigation.goto('.', {view: Summary});
   466                                                                                  services.viewPreferences.updatePreferences({appList: {...pref, view: Summary}});
   467                                                                              }}
   468                                                                          />
   469                                                                      </div>
   470                                                                  </React.Fragment>
   471                                                              ),
   472                                                              actionMenu: {
   473                                                                  items: [
   474                                                                      {
   475                                                                          title: 'New App',
   476                                                                          iconClassName: 'fa fa-plus',
   477                                                                          qeId: 'applications-list-button-new-app',
   478                                                                          action: () => ctx.navigation.goto('.', {new: '{}'}, {replace: true})
   479                                                                      },
   480                                                                      {
   481                                                                          title: 'Sync Apps',
   482                                                                          iconClassName: 'fa fa-sync',
   483                                                                          action: () => ctx.navigation.goto('.', {syncApps: true}, {replace: true})
   484                                                                      },
   485                                                                      {
   486                                                                          title: 'Refresh Apps',
   487                                                                          iconClassName: 'fa fa-redo',
   488                                                                          action: () => ctx.navigation.goto('.', {refreshApps: true}, {replace: true})
   489                                                                      }
   490                                                                  ]
   491                                                              }
   492                                                          }}
   493                                                      />
   494                                                      <div className='applications-list'>
   495                                                          {applications.length === 0 && pref.projectsFilter?.length === 0 && (pref.labelsFilter || []).length === 0 ? (
   496                                                              <EmptyState icon='argo-icon-application'>
   497                                                                  <h4>No applications available to you just yet</h4>
   498                                                                  <h5>Create new application to start managing resources in your cluster</h5>
   499                                                                  <button
   500                                                                      qe-id='applications-list-button-create-application'
   501                                                                      className='argo-button argo-button--base'
   502                                                                      onClick={() => ctx.navigation.goto('.', {new: JSON.stringify({})}, {replace: true})}>
   503                                                                      Create application
   504                                                                  </button>
   505                                                              </EmptyState>
   506                                                          ) : (
   507                                                              <>
   508                                                                  {ReactDOM.createPortal(
   509                                                                      <DataLoader load={() => services.viewPreferences.getPreferences()}>
   510                                                                          {allpref => (
   511                                                                              <ApplicationsFilter
   512                                                                                  apps={filterResults}
   513                                                                                  onChange={newPrefs => onFilterPrefChanged(ctx, newPrefs)}
   514                                                                                  pref={pref}
   515                                                                                  collapsed={allpref.hideSidebar}
   516                                                                              />
   517                                                                          )}
   518                                                                      </DataLoader>,
   519                                                                      sidebarTarget?.current
   520                                                                  )}
   521  
   522                                                                  {(pref.view === 'summary' && <ApplicationsSummary applications={filteredApps} />) || (
   523                                                                      <Paginate
   524                                                                          header={filteredApps.length > 1 && <ApplicationsStatusBar applications={filteredApps} />}
   525                                                                          showHeader={healthBarPrefs.showHealthStatusBar}
   526                                                                          preferencesKey='applications-list'
   527                                                                          page={pref.page}
   528                                                                          emptyState={() => (
   529                                                                              <EmptyState icon='fa fa-search'>
   530                                                                                  <h4>No matching applications found</h4>
   531                                                                                  <h5>
   532                                                                                      Change filter criteria or&nbsp;
   533                                                                                      <a
   534                                                                                          onClick={() => {
   535                                                                                              AppsListPreferences.clearFilters(pref);
   536                                                                                              onFilterPrefChanged(ctx, pref);
   537                                                                                          }}>
   538                                                                                          clear filters
   539                                                                                      </a>
   540                                                                                  </h5>
   541                                                                              </EmptyState>
   542                                                                          )}
   543                                                                          sortOptions={[
   544                                                                              {
   545                                                                                  title: 'Name',
   546                                                                                  compare: (a, b) => a.metadata.name.localeCompare(b.metadata.name, undefined, {numeric: true})
   547                                                                              },
   548                                                                              {
   549                                                                                  title: 'Created At',
   550                                                                                  compare: (b, a) => a.metadata.creationTimestamp.localeCompare(b.metadata.creationTimestamp)
   551                                                                              },
   552                                                                              {
   553                                                                                  title: 'Synchronized',
   554                                                                                  compare: (b, a) =>
   555                                                                                      a.status.operationState?.finishedAt?.localeCompare(b.status.operationState?.finishedAt)
   556                                                                              }
   557                                                                          ]}
   558                                                                          data={filteredApps}
   559                                                                          onPageChange={page => ctx.navigation.goto('.', {page})}>
   560                                                                          {data =>
   561                                                                              (pref.view === 'tiles' && (
   562                                                                                  <ApplicationTiles
   563                                                                                      applications={data}
   564                                                                                      syncApplication={(appName, appNamespace) =>
   565                                                                                          ctx.navigation.goto('.', {syncApp: appName, appNamespace}, {replace: true})
   566                                                                                      }
   567                                                                                      refreshApplication={refreshApp}
   568                                                                                      deleteApplication={(appName, appNamespace) =>
   569                                                                                          AppUtils.deleteApplication(appName, appNamespace, ctx)
   570                                                                                      }
   571                                                                                  />
   572                                                                              )) || (
   573                                                                                  <ApplicationsTable
   574                                                                                      applications={data}
   575                                                                                      syncApplication={(appName, appNamespace) =>
   576                                                                                          ctx.navigation.goto('.', {syncApp: appName, appNamespace}, {replace: true})
   577                                                                                      }
   578                                                                                      refreshApplication={refreshApp}
   579                                                                                      deleteApplication={(appName, appNamespace) =>
   580                                                                                          AppUtils.deleteApplication(appName, appNamespace, ctx)
   581                                                                                      }
   582                                                                                  />
   583                                                                              )
   584                                                                          }
   585                                                                      </Paginate>
   586                                                                  )}
   587                                                              </>
   588                                                          )}
   589                                                          <ApplicationsSyncPanel
   590                                                              key='syncsPanel'
   591                                                              show={syncAppsInput}
   592                                                              hide={() => ctx.navigation.goto('.', {syncApps: null}, {replace: true})}
   593                                                              apps={filteredApps}
   594                                                          />
   595                                                          <ApplicationsRefreshPanel
   596                                                              key='refreshPanel'
   597                                                              show={refreshAppsInput}
   598                                                              hide={() => ctx.navigation.goto('.', {refreshApps: null}, {replace: true})}
   599                                                              apps={filteredApps}
   600                                                          />
   601                                                      </div>
   602                                                      <ObservableQuery>
   603                                                          {q => (
   604                                                              <DataLoader
   605                                                                  load={() =>
   606                                                                      q.pipe(
   607                                                                          mergeMap(params => {
   608                                                                              const syncApp = params.get('syncApp');
   609                                                                              const appNamespace = params.get('appNamespace');
   610                                                                              return (syncApp && from(services.applications.get(syncApp, appNamespace))) || from([null]);
   611                                                                          })
   612                                                                      )
   613                                                                  }>
   614                                                                  {app => (
   615                                                                      <ApplicationSyncPanel
   616                                                                          key='syncPanel'
   617                                                                          application={app}
   618                                                                          selectedResource={'all'}
   619                                                                          hide={() => ctx.navigation.goto('.', {syncApp: null}, {replace: true})}
   620                                                                      />
   621                                                                  )}
   622                                                              </DataLoader>
   623                                                          )}
   624                                                      </ObservableQuery>
   625                                                      <SlidingPanel
   626                                                          isShown={!!appInput}
   627                                                          onClose={() => handleCreatePanelClose()} //Separate handling for outside click.
   628                                                          header={
   629                                                              <div>
   630                                                                  <button
   631                                                                      qe-id='applications-list-button-create'
   632                                                                      className='argo-button argo-button--base'
   633                                                                      disabled={isAppCreatePending}
   634                                                                      onClick={() => createApi && createApi.submitForm(null)}>
   635                                                                      <Spinner show={isAppCreatePending} style={{marginRight: '5px'}} />
   636                                                                      Create
   637                                                                  </button>{' '}
   638                                                                  <button
   639                                                                      qe-id='applications-list-button-cancel'
   640                                                                      onClick={() => ctx.navigation.goto('.', {new: null}, {replace: true})}
   641                                                                      className='argo-button argo-button--base-o'>
   642                                                                      Cancel
   643                                                                  </button>
   644                                                              </div>
   645                                                          }>
   646                                                          {appInput && (
   647                                                              <ApplicationCreatePanel
   648                                                                  getFormApi={api => {
   649                                                                      setCreateApi(api);
   650                                                                  }}
   651                                                                  createApp={async app => {
   652                                                                      setAppCreatePending(true);
   653                                                                      try {
   654                                                                          await services.applications.create(app);
   655                                                                          ctx.navigation.goto('.', {new: null}, {replace: true});
   656                                                                      } catch (e) {
   657                                                                          ctx.notifications.show({
   658                                                                              content: <ErrorNotification title='Unable to create application' e={e} />,
   659                                                                              type: NotificationType.Error
   660                                                                          });
   661                                                                      } finally {
   662                                                                          setAppCreatePending(false);
   663                                                                      }
   664                                                                  }}
   665                                                                  app={appInput}
   666                                                                  onAppChanged={app => ctx.navigation.goto('.', {new: JSON.stringify(app)}, {replace: true})}
   667                                                              />
   668                                                          )}
   669                                                      </SlidingPanel>
   670                                                  </React.Fragment>
   671                                              );
   672                                          }}
   673                                      </DataLoader>
   674                                  </Page>
   675                              )}
   676                          </ViewPref>
   677                      )}
   678                  </Consumer>
   679              </KeybindingProvider>
   680          </ClusterCtx.Provider>
   681      );
   682  };