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 }