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