Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
window.__CONNECT_BUNDLE_LOAD_STARTED_AT__ = performance.now();
</script>
<script type="module" src="/src/index.jsx"></script>

<script async src="https://www.googletagmanager.com/gtag/js?id=G-SD0LELN29K"></script>
Expand Down
15 changes: 8 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component, lazy, Suspense } from 'react';
import React, { Component, lazy } from 'react';
import { Provider } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router';
import { ConnectedRouter } from 'connected-react-router';
Expand All @@ -14,6 +14,7 @@ import { fetchTurnCredentials } from './utils/turn';
import store, { history } from './store';

import ErrorFallback from './components/ErrorFallback';
import BundleLoadBoundary from './components/BundleLoadBoundary';
import FullPageLoading from './components/FullPageLoading';

const Explorer = lazy(() => import('./components/explorer'));
Expand Down Expand Up @@ -123,11 +124,7 @@ class App extends Component {
}

const showLogin = !MyCommaAuth.isAuthenticated() && !getZoom(window.location.pathname) && !getSegmentRange(window.location.pathname);
let content = (
<Suspense fallback={<FullPageLoading />}>
{ showLogin ? this.anonymousRoutes() : this.authRoutes() }
</Suspense>
);
let content = showLogin ? this.anonymousRoutes() : this.authRoutes();

// Use ErrorBoundary in production only
if (import.meta.env.PROD) {
Expand All @@ -141,7 +138,11 @@ class App extends Component {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
{content}
<Route render={({ location }) => (
<BundleLoadBoundary key={location.key || location.pathname}>
{content}
</BundleLoadBoundary>
)} />
</ConnectedRouter>
</Provider>
);
Expand Down
20 changes: 19 additions & 1 deletion src/components/AppHeader/AccountMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import { Switch } from '@material-ui/core';

import MyCommaAuth from '@commaai/my-comma-auth';

import { USERADMIN_URL_ROOT } from '../../api';
import { getDeveloperToolsEnabled, setDeveloperToolsEnabled } from '../../userSettings';

const logOut = async () => {
await MyCommaAuth.logOut();
Expand Down Expand Up @@ -35,12 +37,19 @@ const Version = () => {

const AccountMenu = ({ profile, open, onClose }) => {
const version = useMemo(() => <Version />, []);
const [developerToolsEnabled, setDeveloperToolsState] = useState(getDeveloperToolsEnabled);

const onLogOut = useCallback(() => {
onClose();
logOut();
}, [onClose]);

const onDeveloperToolsChange = useCallback((event) => {
const enabled = event.target.checked;
setDeveloperToolsState(enabled);
setDeveloperToolsEnabled(enabled);
}, []);

if (!open) {
return null;
}
Expand All @@ -64,6 +73,15 @@ const AccountMenu = ({ profile, open, onClose }) => {
>
Manage Account
</a>
<label className="flex w-full cursor-pointer items-center justify-between px-4 py-2 text-white hover:bg-white/10">
<span>Developer tools</span>
<Switch
checked={developerToolsEnabled}
color="primary"
inputProps={{ 'aria-label': 'Enable developer tools' }}
onChange={onDeveloperToolsChange}
/>
</label>
<button
className="block w-full px-4 py-3 text-left text-white hover:bg-white/10"
onClick={onLogOut}
Expand Down
108 changes: 108 additions & 0 deletions src/components/BundleLoadBoundary/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { Component, Suspense } from 'react';
import PropTypes from 'prop-types';
import { DEV_TOOLS_CHANGED_EVENT, getDeveloperToolsEnabled } from '../../userSettings';
import FullPageLoading from '../FullPageLoading';

let initialLoadClaimed = false;

const now = () => (
typeof performance !== 'undefined' && typeof performance.now === 'function'
? performance.now()
: Date.now()
);

const getLoadStart = () => {
if (!initialLoadClaimed) {
initialLoadClaimed = true;
if (typeof window !== 'undefined' && Number.isFinite(window.__CONNECT_BUNDLE_LOAD_STARTED_AT__)) {
return window.__CONNECT_BUNDLE_LOAD_STARTED_AT__;
}
}
return now();
};

class ReadyMarker extends Component {
componentDidMount() {
this.props.onReady();
}

render() {
return null;
}
}

ReadyMarker.propTypes = {
onReady: PropTypes.func.isRequired,
};

class BundleLoadBoundary extends Component {
constructor(props) {
super(props);
this.startedAt = getLoadStart();
this.animationFrame = null;
this.state = {
developerToolsEnabled: getDeveloperToolsEnabled(),
durationMs: null,
};
this.handleReady = this.handleReady.bind(this);
this.handleDeveloperToolsChanged = this.handleDeveloperToolsChanged.bind(this);
}

componentDidMount() {
window.addEventListener(DEV_TOOLS_CHANGED_EVENT, this.handleDeveloperToolsChanged);
}

componentWillUnmount() {
window.removeEventListener(DEV_TOOLS_CHANGED_EVENT, this.handleDeveloperToolsChanged);
if (this.animationFrame !== null && typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(this.animationFrame);
}
}

handleDeveloperToolsChanged(event) {
this.setState({ developerToolsEnabled: event.detail.enabled });
}

handleReady() {
const finish = () => {
this.animationFrame = null;
this.setState({ durationMs: Math.max(0, Math.round(now() - this.startedAt)) });
};

if (typeof requestAnimationFrame === 'function') {
this.animationFrame = requestAnimationFrame(finish);
} else {
finish();
}
}

render() {
const { children } = this.props;
const { developerToolsEnabled, durationMs } = this.state;

return (
<>
<Suspense fallback={<FullPageLoading />}>
{children}
<ReadyMarker onReady={this.handleReady} />
</Suspense>
{durationMs === null
? <FullPageLoading />
: developerToolsEnabled && (
<footer
className="pointer-events-none fixed inset-x-0 bottom-0 z-[1300] border-t border-white/10 bg-[#16181A]/[.92] px-3 py-1 text-right text-[11px] leading-4 text-white/50"
data-testid="bundle-load-footer"
>
Bundle loaded in {durationMs} ms
</footer>
)}
</>
);
}
}

BundleLoadBoundary.propTypes = {
children: PropTypes.node.isRequired,
};

export default BundleLoadBoundary;
24 changes: 24 additions & 0 deletions src/userSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const DEV_TOOLS_STORAGE_KEY = 'developerToolsEnabled';
export const DEV_TOOLS_CHANGED_EVENT = 'connect:developer-tools-changed';

export const getDeveloperToolsEnabled = () => {
try {
return window.localStorage.getItem(DEV_TOOLS_STORAGE_KEY) === 'true';
} catch {
return false;
}
};

export const setDeveloperToolsEnabled = (enabled) => {
const nextValue = Boolean(enabled);

try {
window.localStorage.setItem(DEV_TOOLS_STORAGE_KEY, String(nextValue));
} catch {
// Keep the setting usable for this session when storage is unavailable.
}

window.dispatchEvent(new CustomEvent(DEV_TOOLS_CHANGED_EVENT, {
detail: { enabled: nextValue },
}));
};
Loading