github.com/argoproj/argo-cd/v2@v2.10.9/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 applications from './applications'; 8 import help from './help'; 9 import login from './login'; 10 import settings from './settings'; 11 import {Layout} from './shared/components/layout/layout'; 12 import {Page} from './shared/components/page/page'; 13 import {VersionPanel} from './shared/components/version-info/version-info-panel'; 14 import {AuthSettingsCtx, Provider} from './shared/context'; 15 import {services} from './shared/services'; 16 import requests from './shared/services/requests'; 17 import {hashCode} from './shared/utils'; 18 import {Banner} from './ui-banner/ui-banner'; 19 import userInfo from './user-info'; 20 import {AuthSettings} from './shared/models'; 21 import {PKCEVerification} from './login/components/pkce-verify'; 22 23 services.viewPreferences.init(); 24 const bases = document.getElementsByTagName('base'); 25 const base = bases.length > 0 ? bases[0].getAttribute('href') || '/' : '/'; 26 export const history = createBrowserHistory({basename: base}); 27 requests.setBaseHRef(base); 28 29 type Routes = {[path: string]: {component: React.ComponentType<RouteComponentProps<any>>; noLayout?: boolean; extension?: boolean}}; 30 31 const routes: Routes = { 32 '/login': {component: login.component as any, noLayout: true}, 33 '/applications': {component: applications.component}, 34 '/settings': {component: settings.component}, 35 '/user-info': {component: userInfo.component}, 36 '/help': {component: help.component}, 37 '/pkce/verify': {component: PKCEVerification, noLayout: true} 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 requests.onError.subscribe(async err => { 89 if (err.status === 401) { 90 if (history.location.pathname.startsWith('/login')) { 91 return; 92 } 93 94 const isSSO = await isExpiredSSO(); 95 // location might change after async method call, so we need to check again. 96 if (history.location.pathname.startsWith('/login')) { 97 return; 98 } 99 // Query for basehref and remove trailing /. 100 // If basehref is the default `/` it will become an empty string. 101 const basehref = document 102 .querySelector('head > base') 103 .getAttribute('href') 104 .replace(/\/$/, ''); 105 if (isSSO) { 106 window.location.href = `${basehref}/auth/login?return_url=${encodeURIComponent(location.href)}`; 107 } else { 108 history.push(`/login?return_url=${encodeURIComponent(location.href)}`); 109 } 110 } 111 }); 112 113 export class App extends React.Component< 114 {}, 115 {popupProps: PopupProps; showVersionPanel: boolean; error: Error; navItems: NavItem[]; routes: Routes; extensionsLoaded: boolean; authSettings: AuthSettings} 116 > { 117 public static childContextTypes = { 118 history: PropTypes.object, 119 apis: PropTypes.object 120 }; 121 122 public static getDerivedStateFromError(error: Error) { 123 return {error}; 124 } 125 126 private popupManager: PopupManager; 127 private notificationsManager: NotificationsManager; 128 private navigationManager: NavigationManager; 129 private navItems: NavItem[]; 130 private routes: Routes; 131 132 constructor(props: {}) { 133 super(props); 134 this.state = {popupProps: null, error: null, showVersionPanel: false, navItems: [], routes: null, extensionsLoaded: false, authSettings: null}; 135 this.popupManager = new PopupManager(); 136 this.notificationsManager = new NotificationsManager(); 137 this.navigationManager = new NavigationManager(history); 138 this.navItems = navItems; 139 this.routes = routes; 140 } 141 142 public async componentDidMount() { 143 this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps})); 144 const authSettings = await services.authService.settings(); 145 const {trackingID, anonymizeUsers} = authSettings.googleAnalytics || {trackingID: '', anonymizeUsers: true}; 146 const {loggedIn, username} = await services.users.get(); 147 if (trackingID) { 148 const ga = await import('react-ga'); 149 ga.initialize(trackingID); 150 const trackPageView = () => { 151 if (loggedIn && username) { 152 const userId = !anonymizeUsers ? username : hashCode(username).toString(); 153 ga.set({userId}); 154 } 155 ga.pageview(location.pathname + location.search); 156 }; 157 trackPageView(); 158 history.listen(trackPageView); 159 } 160 if (authSettings.uiCssURL) { 161 const link = document.createElement('link'); 162 link.href = authSettings.uiCssURL; 163 link.rel = 'stylesheet'; 164 link.type = 'text/css'; 165 document.head.appendChild(link); 166 } 167 168 const systemExtensions = services.extensions.getSystemExtensions(); 169 const extendedNavItems = this.navItems; 170 const extendedRoutes = this.routes; 171 for (const extension of systemExtensions) { 172 extendedNavItems.push({ 173 title: extension.title, 174 path: extension.path, 175 iconClassName: `fa ${extension.icon}` 176 }); 177 const component = () => ( 178 <> 179 <Helmet> 180 <title>{extension.title} - Argo CD</title> 181 </Helmet> 182 <Page title={extension.title}> 183 <extension.component /> 184 </Page> 185 </> 186 ); 187 extendedRoutes[extension.path] = { 188 component: component as React.ComponentType<React.ComponentProps<any>>, 189 extension: true 190 }; 191 } 192 193 this.setState({...this.state, navItems: extendedNavItems, routes: extendedRoutes, extensionsLoaded: true, authSettings}); 194 } 195 196 public render() { 197 if (this.state.error != null) { 198 const stack = this.state.error.stack; 199 const url = 'https://github.com/argoproj/argo-cd/issues/new?labels=bug&template=bug_report.md'; 200 201 return ( 202 <React.Fragment> 203 <p>Something went wrong!</p> 204 <p> 205 Consider submitting an issue <a href={url}>here</a>. 206 </p> 207 <br /> 208 <p>Stacktrace:</p> 209 <pre>{stack}</pre> 210 </React.Fragment> 211 ); 212 } 213 214 return ( 215 <React.Fragment> 216 <Helmet> 217 <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-32x32.png`} sizes='32x32' /> 218 <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-16x16.png`} sizes='16x16' /> 219 </Helmet> 220 <PageContext.Provider value={{title: 'Argo CD'}}> 221 <Provider value={{history, popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base}}> 222 <DataLoader load={() => services.viewPreferences.getPreferences()}> 223 {pref => <div className={pref.theme ? 'theme-' + pref.theme : 'theme-light'}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</div>} 224 </DataLoader> 225 <AuthSettingsCtx.Provider value={this.state.authSettings}> 226 <Router history={history}> 227 <Switch> 228 <Redirect exact={true} path='/' to='/applications' /> 229 {Object.keys(this.routes).map(path => { 230 const route = this.routes[path]; 231 return ( 232 <Route 233 key={path} 234 path={path} 235 render={routeProps => 236 route.noLayout ? ( 237 <div> 238 <route.component {...routeProps} /> 239 </div> 240 ) : ( 241 <DataLoader load={() => services.viewPreferences.getPreferences()}> 242 {pref => ( 243 <Layout 244 onVersionClick={() => this.setState({showVersionPanel: true})} 245 navItems={this.navItems} 246 pref={pref} 247 isExtension={route.extension}> 248 <Banner> 249 <route.component {...routeProps} /> 250 </Banner> 251 </Layout> 252 )} 253 </DataLoader> 254 ) 255 } 256 /> 257 ); 258 })} 259 {this.state.extensionsLoaded && <Redirect path='*' to='/' />} 260 </Switch> 261 </Router> 262 </AuthSettingsCtx.Provider> 263 </Provider> 264 </PageContext.Provider> 265 <Notifications notifications={this.notificationsManager.notifications} /> 266 <VersionPanel version={versionLoader} isShown={this.state.showVersionPanel} onClose={() => this.setState({showVersionPanel: false})} /> 267 </React.Fragment> 268 ); 269 } 270 271 public getChildContext() { 272 return {history, apis: {popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager}}; 273 } 274 }