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

     1  import {DataLoader, NavigationManager, Notifications, NotificationsManager, PageContext, Popup, PopupManager, PopupProps} from 'argo-ui';
     2  import {createBrowserHistory} from 'history';
     3  import * as PropTypes from 'prop-types';
     4  import * as React from 'react';
     5  import {Helmet} from 'react-helmet';
     6  import {Redirect, Route, RouteComponentProps, Router, Switch} from 'react-router';
     7  import {Subscription} from 'rxjs';
     8  import applications from './applications';
     9  import help from './help';
    10  import login from './login';
    11  import settings from './settings';
    12  import {Layout, ThemeWrapper} from './shared/components/layout/layout';
    13  import {Page} from './shared/components/page/page';
    14  import {VersionPanel} from './shared/components/version-info/version-info-panel';
    15  import {AuthSettingsCtx, Provider} from './shared/context';
    16  import {services} from './shared/services';
    17  import requests from './shared/services/requests';
    18  import {hashCode} from './shared/utils';
    19  import {Banner} from './ui-banner/ui-banner';
    20  import userInfo from './user-info';
    21  import {AuthSettings} from './shared/models';
    22  import {SystemLevelExtension} from './shared/services/extensions-service';
    23  
    24  services.viewPreferences.init();
    25  const bases = document.getElementsByTagName('base');
    26  const base = bases.length > 0 ? bases[0].getAttribute('href') || '/' : '/';
    27  export const history = createBrowserHistory({basename: base});
    28  requests.setBaseHRef(base);
    29  
    30  type Routes = {[path: string]: {component: React.ComponentType<RouteComponentProps<any>>; noLayout?: boolean}};
    31  
    32  const routes: Routes = {
    33      '/login': {component: login.component as any, noLayout: true},
    34      '/applications': {component: applications.component},
    35      '/settings': {component: settings.component},
    36      '/user-info': {component: userInfo.component},
    37      '/help': {component: help.component}
    38  };
    39  
    40  interface NavItem {
    41      title: string;
    42      tooltip?: string;
    43      path: string;
    44      iconClassName: string;
    45  }
    46  
    47  const navItems: NavItem[] = [
    48      {
    49          title: 'Applications',
    50          tooltip: 'Manage your applications, and diagnose health problems.',
    51          path: '/applications',
    52          iconClassName: 'argo-icon argo-icon-application'
    53      },
    54      {
    55          title: 'Settings',
    56          tooltip: 'Manage your repositories, projects, settings',
    57          path: '/settings',
    58          iconClassName: 'argo-icon argo-icon-settings'
    59      },
    60      {
    61          title: 'User Info',
    62          path: '/user-info',
    63          iconClassName: 'fa fa-user-circle'
    64      },
    65      {
    66          title: 'Documentation',
    67          tooltip: 'Read the documentation, and get help and assistance.',
    68          path: '/help',
    69          iconClassName: 'argo-icon argo-icon-docs'
    70      }
    71  ];
    72  
    73  const versionLoader = services.version.version();
    74  
    75  async function isExpiredSSO() {
    76      try {
    77          const {iss} = await services.users.get();
    78          const authSettings = await services.authService.settings();
    79          if (iss && iss !== 'argocd') {
    80              return ((authSettings.dexConfig && authSettings.dexConfig.connectors) || []).length > 0 || authSettings.oidcConfig;
    81          }
    82      } catch {
    83          return false;
    84      }
    85      return false;
    86  }
    87  
    88  export class App extends React.Component<{}, {popupProps: PopupProps; showVersionPanel: boolean; error: Error; navItems: NavItem[]; routes: Routes; authSettings: AuthSettings}> {
    89      public static childContextTypes = {
    90          history: PropTypes.object,
    91          apis: PropTypes.object
    92      };
    93  
    94      public static getDerivedStateFromError(error: Error) {
    95          return {error};
    96      }
    97  
    98      private popupManager: PopupManager;
    99      private notificationsManager: NotificationsManager;
   100      private navigationManager: NavigationManager;
   101      private navItems: NavItem[];
   102      private routes: Routes;
   103      private popupPropsSubscription: Subscription;
   104      private unauthorizedSubscription: Subscription;
   105  
   106      constructor(props: {}) {
   107          super(props);
   108          this.state = {popupProps: null, error: null, showVersionPanel: false, navItems: [], routes: null, authSettings: null};
   109          this.popupManager = new PopupManager();
   110          this.notificationsManager = new NotificationsManager();
   111          this.navigationManager = new NavigationManager(history);
   112          this.navItems = navItems;
   113          this.routes = routes;
   114          this.popupPropsSubscription = null;
   115          this.unauthorizedSubscription = null;
   116          services.extensions.addEventListener('systemLevel', this.onAddSystemLevelExtension.bind(this));
   117      }
   118  
   119      public async componentDidMount() {
   120          this.popupPropsSubscription = this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps}));
   121          this.subscribeUnauthorized().then(subscription => {
   122              this.unauthorizedSubscription = subscription;
   123          });
   124          const authSettings = await services.authService.settings();
   125          const {trackingID, anonymizeUsers} = authSettings.googleAnalytics || {trackingID: '', anonymizeUsers: true};
   126          const {loggedIn, username} = await services.users.get();
   127          if (trackingID) {
   128              const ga = await import('react-ga');
   129              ga.initialize(trackingID);
   130              const trackPageView = () => {
   131                  if (loggedIn && username) {
   132                      const userId = !anonymizeUsers ? username : hashCode(username).toString();
   133                      ga.set({userId});
   134                  }
   135                  ga.pageview(location.pathname + location.search);
   136              };
   137              trackPageView();
   138              history.listen(trackPageView);
   139          }
   140          if (authSettings.uiCssURL) {
   141              const link = document.createElement('link');
   142              link.href = authSettings.uiCssURL;
   143              link.rel = 'stylesheet';
   144              link.type = 'text/css';
   145              document.head.appendChild(link);
   146          }
   147  
   148          this.setState({...this.state, navItems: this.navItems, routes: this.routes, authSettings});
   149      }
   150  
   151      public componentWillUnmount() {
   152          if (this.popupPropsSubscription) {
   153              this.popupPropsSubscription.unsubscribe();
   154          }
   155          if (this.unauthorizedSubscription) {
   156              this.unauthorizedSubscription.unsubscribe();
   157          }
   158      }
   159  
   160      public render() {
   161          if (this.state.error != null) {
   162              const stack = this.state.error.stack;
   163              const url = 'https://github.com/argoproj/argo-cd/issues/new?labels=bug&template=bug_report.md';
   164  
   165              return (
   166                  <React.Fragment>
   167                      <p>Something went wrong!</p>
   168                      <p>
   169                          Consider submitting an issue <a href={url}>here</a>.
   170                      </p>
   171                      <br />
   172                      <p>Stacktrace:</p>
   173                      <pre>{stack}</pre>
   174                  </React.Fragment>
   175              );
   176          }
   177  
   178          return (
   179              <React.Fragment>
   180                  <Helmet>
   181                      <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-32x32.png`} sizes='32x32' />
   182                      <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-16x16.png`} sizes='16x16' />
   183                  </Helmet>
   184                  <PageContext.Provider value={{title: 'Argo CD'}}>
   185                      <Provider value={{history, popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base}}>
   186                          <DataLoader load={() => services.viewPreferences.getPreferences()}>
   187                              {pref => <ThemeWrapper theme={pref.theme}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</ThemeWrapper>}
   188                          </DataLoader>
   189                          <AuthSettingsCtx.Provider value={this.state.authSettings}>
   190                              <Router history={history}>
   191                                  <Switch>
   192                                      <Redirect exact={true} path='/' to='/applications' />
   193                                      {Object.keys(this.routes).map(path => {
   194                                          const route = this.routes[path];
   195                                          return (
   196                                              <Route
   197                                                  key={path}
   198                                                  path={path}
   199                                                  render={routeProps =>
   200                                                      route.noLayout ? (
   201                                                          <div>
   202                                                              <route.component {...routeProps} />
   203                                                          </div>
   204                                                      ) : (
   205                                                          <DataLoader load={() => services.viewPreferences.getPreferences()}>
   206                                                              {pref => (
   207                                                                  <Layout onVersionClick={() => this.setState({showVersionPanel: true})} navItems={this.navItems} pref={pref}>
   208                                                                      <Banner>
   209                                                                          <route.component {...routeProps} />
   210                                                                      </Banner>
   211                                                                  </Layout>
   212                                                              )}
   213                                                          </DataLoader>
   214                                                      )
   215                                                  }
   216                                              />
   217                                          );
   218                                      })}
   219                                  </Switch>
   220                              </Router>
   221                          </AuthSettingsCtx.Provider>
   222                      </Provider>
   223                  </PageContext.Provider>
   224                  <Notifications notifications={this.notificationsManager.notifications} />
   225                  <VersionPanel version={versionLoader} isShown={this.state.showVersionPanel} onClose={() => this.setState({showVersionPanel: false})} />
   226              </React.Fragment>
   227          );
   228      }
   229  
   230      public getChildContext() {
   231          return {history, apis: {popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base}};
   232      }
   233  
   234      private async subscribeUnauthorized() {
   235          return requests.onError.subscribe(async err => {
   236              if (err.status === 401) {
   237                  if (history.location.pathname.startsWith('/login')) {
   238                      return;
   239                  }
   240  
   241                  const isSSO = await isExpiredSSO();
   242                  // location might change after async method call, so we need to check again.
   243                  if (history.location.pathname.startsWith('/login')) {
   244                      return;
   245                  }
   246                  // Query for basehref and remove trailing /.
   247                  // If basehref is the default `/` it will become an empty string.
   248                  const basehref = document.querySelector('head > base').getAttribute('href').replace(/\/$/, '');
   249                  if (isSSO) {
   250                      window.location.href = `${basehref}/auth/login?return_url=${encodeURIComponent(location.href)}`;
   251                  } else {
   252                      history.push(`/login?return_url=${encodeURIComponent(location.href)}`);
   253                  }
   254              }
   255          });
   256      }
   257  
   258      private onAddSystemLevelExtension(extension: SystemLevelExtension) {
   259          const extendedNavItems = this.navItems;
   260          const extendedRoutes = this.routes;
   261          extendedNavItems.push({
   262              title: extension.title,
   263              path: extension.path,
   264              iconClassName: `fa ${extension.icon}`
   265          });
   266          const component = () => (
   267              <>
   268                  <Helmet>
   269                      <title>{extension.title} - Argo CD</title>
   270                  </Helmet>
   271                  <Page title={extension.title}>
   272                      <extension.component />
   273                  </Page>
   274              </>
   275          );
   276          extendedRoutes[extension.path] = {
   277              component: component as React.ComponentType<React.ComponentProps<any>>
   278          };
   279          this.setState({...this.state, navItems: extendedNavItems, routes: extendedRoutes});
   280      }
   281  }