Skip to content

Commit

Permalink
Fix reloading when the saved project uses the Local Backend (#9627)
Browse files Browse the repository at this point in the history
- Fix enso-org/cloud-v2#1156
- Fix reloading when the saved project uses the Local Backend

# Important Notes
To reproduce the error:
- Open a project in the Local Backend
- Close and reopen the IDE, or refresh the IDE.
  • Loading branch information
somebody1234 authored Apr 8, 2024
1 parent c182b30 commit 07793f5
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 124 deletions.
52 changes: 43 additions & 9 deletions app/ide-desktop/lib/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ import SessionProvider from '#/providers/SessionProvider'

import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
import ErrorScreen from '#/pages/authentication/ErrorScreen'
import ForgotPassword from '#/pages/authentication/ForgotPassword'
import LoadingScreen from '#/pages/authentication/LoadingScreen'
import Login from '#/pages/authentication/Login'
import Registration from '#/pages/authentication/Registration'
import ResetPassword from '#/pages/authentication/ResetPassword'
Expand All @@ -70,7 +72,9 @@ import * as rootComponent from '#/components/Root'

import type Backend from '#/services/Backend'
import LocalBackend from '#/services/LocalBackend'
import * as projectManager from '#/services/ProjectManager'

import * as appBaseUrl from '#/utilities/appBaseUrl'
import * as eventModule from '#/utilities/event'
import LocalStorage from '#/utilities/LocalStorage'
import * as object from '#/utilities/object'
Expand Down Expand Up @@ -143,14 +147,37 @@ export interface AppProps {
* This component handles all the initialization and rendering of the app, and manages the app's
* routes. It also initializes an `AuthProvider` that will be used by the rest of the app. */
export default function App(props: AppProps) {
const { supportsLocalBackend } = props
// This is a React component even though it does not contain JSX.
// eslint-disable-next-line no-restricted-syntax
const Router = detect.isOnElectron() ? router.HashRouter : router.BrowserRouter
const queryClient = React.useMemo(() => reactQueryClientModule.createReactQueryClient(), [])
const [rootDirectoryPath, setRootDirectoryPath] = React.useState<projectManager.Path | null>(null)
const [error, setError] = React.useState<unknown>(null)
const isLoading = supportsLocalBackend && rootDirectoryPath == null

React.useEffect(() => {
if (supportsLocalBackend) {
void (async () => {
try {
const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`)
const text = await response.text()
setRootDirectoryPath(projectManager.Path(text))
} catch (innerError) {
setError(innerError)
}
})()
}
}, [supportsLocalBackend])

// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
// will redirect the user between the login/register pages and the dashboard.
return (
return error != null ? (
<ErrorScreen error={error} />
) : isLoading ? (
<LoadingScreen />
) : (
<reactQuery.QueryClientProvider client={queryClient}>
<toastify.ToastContainer
position="top-center"
Expand All @@ -163,7 +190,7 @@ export default function App(props: AppProps) {
/>
<Router basename={getMainPageUrl().pathname}>
<LocalStorageProvider>
<AppRouter {...props} />
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
</LocalStorageProvider>
</Router>
</reactQuery.QueryClientProvider>
Expand All @@ -174,14 +201,19 @@ export default function App(props: AppProps) {
// === AppRouter ===
// =================

/** Props for an {@link AppRouter}. */
export interface AppRouterProps extends AppProps {
readonly projectManagerRootDirectory: projectManager.Path | null
}

/** Router definition for the app.
*
* The only reason the {@link AppRouter} component is separate from the {@link App} component is
* because the {@link AppRouter} relies on React hooks, which can't be used in the same React
* component as the component that defines the provider. */
function AppRouter(props: AppProps) {
function AppRouter(props: AppRouterProps) {
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
const { onAuthenticated, projectManagerUrl } = props
const { onAuthenticated, projectManagerUrl, projectManagerRootDirectory } = props
// `navigateHooks.useNavigate` cannot be used here as it relies on `AuthProvider`, which has not
// yet been initialized at this point.
// eslint-disable-next-line no-restricted-properties
Expand Down Expand Up @@ -272,11 +304,12 @@ function AppRouter(props: AppProps) {

const userSession = authService?.cognito.userSession.bind(authService.cognito) ?? null
const registerAuthEventListener = authService?.registerAuthEventListener ?? null
const initialBackend: Backend = isAuthenticationDisabled
? new LocalBackend(projectManagerUrl)
: // This is safe, because the backend is always set by the authentication flow.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
null!
const initialBackend: Backend =
isAuthenticationDisabled && projectManagerUrl != null && projectManagerRootDirectory != null
? new LocalBackend(projectManagerUrl, projectManagerRootDirectory)
: // This is SAFE, because the backend is always set by the authentication flow.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
null!

React.useEffect(() => {
const onKeyDown = navigator2D.onKeyDown.bind(navigator2D)
Expand Down Expand Up @@ -366,6 +399,7 @@ function AppRouter(props: AppProps) {
authService={authService}
onAuthenticated={onAuthenticated}
projectManagerUrl={projectManagerUrl}
projectManagerRootDirectory={projectManagerRootDirectory}
>
{result}
</AuthProvider>
Expand Down
31 changes: 12 additions & 19 deletions app/ide-desktop/lib/dashboard/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import * as detect from 'enso-common/src/detect'
import type * as app from '#/App'
import App from '#/App'

import ProjectManager from '#/services/ProjectManager'

// =================
// === Constants ===
// =================
Expand All @@ -36,7 +34,7 @@ export // This export declaration must be broken up to satisfy the `require-jsdo
// This is not a React component even though it contains JSX.
// eslint-disable-next-line no-restricted-syntax
function run(props: app.AppProps) {
const { logger, vibrancy, supportsDeepLinks, supportsLocalBackend } = props
const { logger, vibrancy, supportsDeepLinks } = props
logger.log('Starting authentication/dashboard UI.')
if (
!detect.IS_DEV_MODE &&
Expand Down Expand Up @@ -77,22 +75,17 @@ function run(props: app.AppProps) {
// `supportsDeepLinks` will be incorrect when accessing the installed Electron app's pages
// via the browser.
const actuallySupportsDeepLinks = supportsDeepLinks && detect.isOnElectron()
void (async () => {
if (supportsLocalBackend) {
await ProjectManager.loadRootDirectory()
}
reactDOM.createRoot(root).render(
<sentry.ErrorBoundary>
{detect.IS_DEV_MODE ? (
<React.StrictMode>
<App {...props} />
</React.StrictMode>
) : (
<App {...props} supportsDeepLinks={actuallySupportsDeepLinks} />
)}
</sentry.ErrorBoundary>
)
})()
reactDOM.createRoot(root).render(
<sentry.ErrorBoundary>
{detect.IS_DEV_MODE ? (
<React.StrictMode>
<App {...props} />
</React.StrictMode>
) : (
<App {...props} supportsDeepLinks={actuallySupportsDeepLinks} />
)}
</sentry.ErrorBoundary>
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @file A loading screen, displayed while the user is logging in. */
import * as React from 'react'

import * as textProvider from '#/providers/TextProvider'

import * as aria from '#/components/aria'

import * as errorModule from '#/utilities/error'

// ===================
// === ErrorScreen ===
// ===================

/** Props for an {@link ErrorScreen}. */
export interface ErrorScreenProps {
readonly error: unknown
}

/** A loading screen. */
export default function ErrorScreen(props: ErrorScreenProps) {
const { error } = props
const { getText } = textProvider.useText()
return (
<div className="grid h-screen w-screen place-items-center text-primary">
<div className="flex flex-col items-center gap-status-page text-center text-base">
<aria.Text>{getText('appErroredMessage')}</aria.Text>
<aria.Text>{getText('appErroredPrompt')}</aria.Text>
<aria.Text className="text-delete">{errorModule.getMessageOrToString(error)}</aria.Text>
</div>
</div>
)
}
39 changes: 26 additions & 13 deletions app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type * as spinner from '#/components/Spinner'

import * as backendModule from '#/services/Backend'
import LocalBackend from '#/services/LocalBackend'
import type * as projectManager from '#/services/ProjectManager'
import RemoteBackend, * as remoteBackendModule from '#/services/RemoteBackend'

import * as array from '#/utilities/array'
Expand Down Expand Up @@ -111,12 +112,13 @@ export interface DashboardProps {
readonly appRunner: AppRunner
readonly initialProjectName: string | null
readonly projectManagerUrl: string | null
readonly projectManagerRootDirectory: projectManager.Path | null
}

/** The component that contains the entire UI. */
export default function Dashboard(props: DashboardProps) {
const { supportsLocalBackend, appRunner, initialProjectName } = props
const { projectManagerUrl } = props
const { projectManagerUrl, projectManagerRootDirectory } = props
const logger = loggerProvider.useLogger()
const session = authProvider.useNonPartialUserSession()
const { backend } = backendProvider.useBackend()
Expand Down Expand Up @@ -175,9 +177,11 @@ export default function Dashboard(props: DashboardProps) {
let currentBackend = backend
if (
supportsLocalBackend &&
projectManagerUrl != null &&
projectManagerRootDirectory != null &&
localStorage.get('backendType') === backendModule.BackendType.local
) {
currentBackend = new LocalBackend(projectManagerUrl)
currentBackend = new LocalBackend(projectManagerUrl, projectManagerRootDirectory)
setBackend(currentBackend)
}
const savedProjectStartupInfo = localStorage.get('projectStartupInfo')
Expand Down Expand Up @@ -218,17 +222,12 @@ export default function Dashboard(props: DashboardProps) {
savedProjectStartupInfo.projectAsset.title
)
if (backendModule.IS_OPENING_OR_OPENED[oldProject.state.type]) {
await remoteBackendModule.waitUntilProjectIsReady(
const project = await remoteBackendModule.waitUntilProjectIsReady(
remoteBackend,
savedProjectStartupInfo.projectAsset,
abortController
)
if (!abortController.signal.aborted) {
const project = await remoteBackend.getProjectDetails(
savedProjectStartupInfo.projectAsset.id,
savedProjectStartupInfo.projectAsset.parentId,
savedProjectStartupInfo.projectAsset.title
)
setProjectStartupInfo(object.merge(savedProjectStartupInfo, { project }))
if (page === pageSwitcher.Page.editor) {
setPage(page)
Expand All @@ -241,12 +240,19 @@ export default function Dashboard(props: DashboardProps) {
})()
}
}
} else {
const localBackend = new LocalBackend(projectManagerUrl)
} else if (projectManagerUrl != null && projectManagerRootDirectory != null) {
const localBackend =
currentBackend instanceof LocalBackend
? currentBackend
: new LocalBackend(projectManagerUrl, projectManagerRootDirectory)
void (async () => {
await localBackend.openProject(
savedProjectStartupInfo.projectAsset.id,
null,
{
executeAsync: false,
cognitoCredentials: null,
parentId: savedProjectStartupInfo.projectAsset.parentId,
},
savedProjectStartupInfo.projectAsset.title
)
const project = await localBackend.getProjectDetails(
Expand All @@ -255,6 +261,9 @@ export default function Dashboard(props: DashboardProps) {
savedProjectStartupInfo.projectAsset.title
)
setProjectStartupInfo(object.merge(savedProjectStartupInfo, { project }))
if (page === pageSwitcher.Page.editor) {
setPage(page)
}
})()
}
}
Expand Down Expand Up @@ -358,9 +367,12 @@ export default function Dashboard(props: DashboardProps) {
(newBackendType: backendModule.BackendType) => {
if (newBackendType !== backend.type) {
switch (newBackendType) {
case backendModule.BackendType.local:
setBackend(new LocalBackend(projectManagerUrl))
case backendModule.BackendType.local: {
if (projectManagerUrl != null && projectManagerRootDirectory != null) {
setBackend(new LocalBackend(projectManagerUrl, projectManagerRootDirectory))
}
break
}
case backendModule.BackendType.remote: {
const client = new HttpClient([
['Authorization', `Bearer ${session.accessToken ?? ''}`],
Expand All @@ -377,6 +389,7 @@ export default function Dashboard(props: DashboardProps) {
logger,
getText,
/* should never change */ projectManagerUrl,
/* should never change */ projectManagerRootDirectory,
/* should never change */ setBackend,
]
)
Expand Down
9 changes: 6 additions & 3 deletions app/ide-desktop/lib/dashboard/src/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import LoadingScreen from '#/pages/authentication/LoadingScreen'
import * as backendModule from '#/services/Backend'
import type Backend from '#/services/Backend'
import LocalBackend from '#/services/LocalBackend'
import type * as projectManager from '#/services/ProjectManager'
import RemoteBackend from '#/services/RemoteBackend'

import * as errorModule from '#/utilities/error'
Expand Down Expand Up @@ -144,12 +145,13 @@ export interface AuthProviderProps {
readonly onAuthenticated: (accessToken: string | null) => void
readonly children: React.ReactNode
readonly projectManagerUrl: string | null
readonly projectManagerRootDirectory: projectManager.Path | null
}

/** A React provider for the Cognito API. */
export default function AuthProvider(props: AuthProviderProps) {
const { shouldStartInOfflineMode, supportsLocalBackend, authService, onAuthenticated } = props
const { children, projectManagerUrl } = props
const { children, projectManagerUrl, projectManagerRootDirectory } = props
const logger = loggerProvider.useLogger()
const { cognito } = authService ?? {}
const { session, deinitializeSession, onSessionError } = sessionProvider.useSession()
Expand Down Expand Up @@ -184,8 +186,8 @@ export default function AuthProvider(props: AuthProviderProps) {
setInitialized(true)
sentry.setUser(null)
setUserSession(OFFLINE_USER_SESSION)
if (supportsLocalBackend) {
setBackendWithoutSavingType(new LocalBackend(projectManagerUrl))
if (supportsLocalBackend && projectManagerUrl != null && projectManagerRootDirectory != null) {
setBackendWithoutSavingType(new LocalBackend(projectManagerUrl, projectManagerRootDirectory))
} else {
// Provide dummy headers to avoid errors. This `Backend` will never be called as
// the entire UI will be disabled.
Expand All @@ -195,6 +197,7 @@ export default function AuthProvider(props: AuthProviderProps) {
}, [
getText,
/* should never change */ projectManagerUrl,
/* should never change */ projectManagerRootDirectory,
/* should never change */ supportsLocalBackend,
/* should never change */ logger,
/* should never change */ setBackendWithoutSavingType,
Expand Down
Loading

0 comments on commit 07793f5

Please sign in to comment.