Skip to content

Commit

Permalink
feat(loading): merge github fetch with the loading bar (#6630)
Browse files Browse the repository at this point in the history
This PR adds the loading stages to the loading bar screen and progress
bar.

Stages for GH import:
1. Loading Editor...
2. Fetching <repo name>
3. Parsing files
4. Fetching dependencies

Stages for project load (we reverse the order in our project load):
1. Fetching dependencies
2. Parsing files

If everything is validated correctly - it just shows the editor with the
project ready to be worked on. If not - it shows an error modal (its UI
was slightly changed in #6677 to better reflect errors).

It also adds support for Dark mode.

**Note:**
- The new logic is almost entirely in
[editor/src/components/editor/loading-screen.tsx](https://github.com/concrete-utopia/utopia/pull/6630/files#diff-5aa4d7e811e0bfb07a60910e517fe3a01cd270c9b4fd05132e513d939d69f769)
- There is a jump in the progress bar since these are actually two DOM
elements, one that is rendered before the React is loaded and a React
component that replaces it after the React code is loaded. This jump
will be fixed in a subsequent PR.
- Currently the progress bar is "static" (advances by time and not
stages). This will be changed in (the same) subsequent PR.

<video
src="https://github.com/user-attachments/assets/a6d52f3d-b27b-4110-be1e-a7f213f563ab"></video>

**Manual Tests:**
I hereby swear that:

- [X] I opened a hydrogen project and it loaded
- [X] I could navigate to various routes in Play mode
  • Loading branch information
liady authored Nov 25, 2024
1 parent 7283a1d commit 9e99bae
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 54 deletions.
17 changes: 14 additions & 3 deletions editor/src/components/canvas/canvas-loading-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Global, css } from '@emotion/react'
import { useColorTheme } from '../../uuiui'
import { useEditorState } from '../editor/store/store-hook'
import { Substores } from '../editor/store/store-hook'
import { getTotalImportStatusAndResult } from '../../core/shared/import/import-operation-service'
import type { TotalImportResult } from '../../core/shared/import/import-operation-types'

export const CanvasLoadingScreen = React.memo(() => {
const colorTheme = useColorTheme()
Expand All @@ -17,17 +19,26 @@ export const CanvasLoadingScreen = React.memo(() => {
'CanvasLoadingScreen importWizardOpen',
)

const totalImportResult: TotalImportResult = React.useMemo(
() => getTotalImportStatusAndResult(importState),
[importState],
)

const importingStoppedStyleOverride = React.useMemo(
() =>
// if the importing was stopped, we want to pause the shimmer animation
(importWizardOpen && importState.importStatus.status === 'done') ||
importState.importStatus.status === 'paused'
(importWizardOpen && totalImportResult.importStatus.status === 'done') ||
totalImportResult.importStatus.status === 'paused'
? {
background: colorTheme.codeEditorShimmerPrimary.value,
animation: 'none',
}
: {},
[importWizardOpen, importState.importStatus.status, colorTheme.codeEditorShimmerPrimary.value],
[
importWizardOpen,
totalImportResult.importStatus.status,
colorTheme.codeEditorShimmerPrimary.value,
],
)

return (
Expand Down
12 changes: 12 additions & 0 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ import {
import { addToastToState, includeToast, removeToastFromState } from './toast-helpers'
import { AspectRatioLockedProp } from '../../aspect-ratio'
import {
getDependenciesStatus,
refreshDependencies,
removeModulesFromNodeModules,
} from '../../../core/shared/dependencies'
Expand Down Expand Up @@ -629,6 +630,8 @@ import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
import { styleP } from '../../inspector/inspector-common'
import {
getUpdateOperationResult,
notifyOperationFinished,
notifyOperationStarted,
notifyImportStatusToDiscord,
} from '../../../core/shared/import/import-operation-service'
import { updateRequirements } from '../../../core/shared/import/project-health-check/utopia-requirements-service'
Expand Down Expand Up @@ -6319,6 +6322,8 @@ export async function load(
// this action is now async!
const migratedModel = applyMigrations(model)
const npmDependencies = dependenciesWithEditorRequirements(migratedModel.projectContents)
// side effect ☢️
notifyOperationStarted(dispatch, { type: 'refreshDependencies' })
const fetchNodeModulesResult = await fetchNodeModules(
dispatch,
npmDependencies,
Expand All @@ -6333,6 +6338,13 @@ export async function load(
fetchNodeModulesResult.dependenciesNotFound,
)

// side effect ☢️
notifyOperationFinished(
dispatch,
{ type: 'refreshDependencies' },
getDependenciesStatus(packageResult),
)

const codeResultCache: CodeResultCache = generateCodeResultCache(
// TODO is this sufficient here?
migratedModel.projectContents,
Expand Down
8 changes: 8 additions & 0 deletions editor/src/components/editor/import-wizard/import-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ function ActionButtons() {
fontSize: 14,
cursor: 'pointer',
}
React.useEffect(() => {
if (
importResult.importStatus.status == 'done' &&
importResult.result == ImportOperationResult.Success
) {
hideWizard()
}
}, [importResult, hideWizard])
if (
importResult.importStatus.status === 'in-progress' ||
importResult.importStatus.status === 'not-started'
Expand Down
174 changes: 174 additions & 0 deletions editor/src/components/editor/loading-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React from 'react'
import { Substores, useEditorState } from './store/store-hook'
import { getImportOperationTextAsJsx } from './import-wizard/import-wizard-helpers'
import { getTotalImportStatusAndResult } from '../../core/shared/import/import-operation-service'
import type { TotalImportResult } from '../../core/shared/import/import-operation-types'
import type { Theme } from '../../uuiui'
import { useColorTheme } from '../../uuiui'
import { getCurrentTheme } from './store/editor-state'
import ReactDOM from 'react-dom'

export function LoadingEditorComponent() {
const colorTheme = useColorTheme()

const currentTheme: Theme = useEditorState(
Substores.theme,
(store) => getCurrentTheme(store.userState),
'currentTheme',
)

const importState = useEditorState(
Substores.restOfEditor,
(store) => store.editor.importState,
'LoadingEditorComponent importState',
)

const githubRepo = useEditorState(
Substores.userState,
(store) => store.userState.githubState.gitRepoToLoad,
'LoadingEditorComponent githubRepoToLoad',
)

const totalImportResult: TotalImportResult = React.useMemo(
() => getTotalImportStatusAndResult(importState),
[importState],
)

const projectId = useEditorState(
Substores.restOfEditor,
(store) => store.editor.id,
'LoadingEditorComponent projectId',
)

const cleared = React.useRef(false)

const currentOperationToShow: {
text: React.ReactNode
id: string
timeDone: number | null | undefined
timeStarted: number | null | undefined
} | null = React.useMemo(() => {
if (totalImportResult.importStatus.status == 'not-started') {
if (projectId == null) {
return {
text: 'Loading Editor...',
id: 'loading-editor',
timeDone: null,
timeStarted: null,
}
} else {
return {
text: `Parsing files`,
id: 'parseFiles',
timeDone: null,
timeStarted: null,
}
}
}
for (const op of importState.importOperations) {
if (op?.children?.length == 0 || op.type == 'refreshDependencies') {
if (op.timeStarted != null && op.timeDone == null) {
return {
text: getImportOperationTextAsJsx(op),
id: op.id ?? op.type,
timeDone: op.timeDone,
timeStarted: op.timeStarted,
}
}
}
if (op.type !== 'refreshDependencies') {
for (const child of op.children ?? []) {
if (child.timeStarted != null && child.timeDone == null) {
return {
text: getImportOperationTextAsJsx(child),
id: child.id ?? child.type,
timeDone: child.timeDone,
timeStarted: child.timeStarted,
}
}
}
}
}
return {
text: 'Loading Editor...',
id: 'loading-editor',
timeDone: null,
timeStarted: null,
}
}, [totalImportResult, importState.importOperations, projectId])

const shouldBeCleared = React.useMemo(() => {
return (
cleared.current ||
(totalImportResult.importStatus.status == 'done' &&
(githubRepo == null || totalImportResult.result == 'criticalError')) ||
totalImportResult.importStatus.status == 'paused'
)
}, [totalImportResult, githubRepo])

React.useEffect(() => {
if (shouldBeCleared) {
const loadingScreenWrapper = document.getElementById('loading-screen-wrapper')
if (loadingScreenWrapper != null) {
loadingScreenWrapper.remove()
}
}
}, [shouldBeCleared])

const portal = React.useRef(document.getElementById('loading-screen-progress-bar-portal')).current
const hasMounted = React.useRef(false)
if (portal == null) {
return null
}

if (shouldBeCleared) {
cleared.current = true
return null
}

if (!hasMounted.current) {
portal.innerHTML = ''
hasMounted.current = true
}

const themeStyle =
currentTheme === 'dark'
? `
.editor-loading-screen { background-color: ${colorTheme.bg6.value} }
.utopia-logo-pyramid.light { display: none; }
.utopia-logo-pyramid.dark { display: block; }
`
: ''

return ReactDOM.createPortal(
<React.Fragment>
<style>{themeStyle}</style>
<div className='progress-bar-shell' style={{ borderColor: colorTheme.fg0.value }}>
<div
className='progress-bar-progress animation-progress'
style={{
transform: 'translateX(-180px)',
animationName: 'animation-keyframes-2',
backgroundColor: colorTheme.fg0.value,
}}
></div>
</div>
<div>
<ul className='loading-screen-import-operations'>
{currentOperationToShow != null ? (
<li
style={{
listStyle: 'none',
color: colorTheme.fg0.value,
}}
key={currentOperationToShow.id}
>
{currentOperationToShow.text}
</li>
) : null}
</ul>
</div>
</React.Fragment>,
portal,
)
}
19 changes: 17 additions & 2 deletions editor/src/components/editor/store/dispatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ import {
} from '../../../core/performance/performance-utils'
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
import { resetUpdatedProperties } from '../../canvas/plugins/style-plugins'
import {
notifyOperationFinished,
notifyOperationStarted,
} from '../../../core/shared/import/import-operation-service'
import { ImportOperationResult } from '../../../core/shared/import/import-operation-types'
import { updateImportStatus } from '../actions/action-creators'

type DispatchResultFields = {
nothingChanged: boolean
Expand Down Expand Up @@ -327,6 +333,7 @@ function maybeRequestModelUpdate(
// Should anything need to be sent across, do so here.
if (filesToUpdate.length > 0) {
const { endMeasure } = startPerformanceMeasure('file-parse', { uniqueId: true })
notifyOperationStarted(dispatch, { type: 'parseFiles' })
const parseFinished = getParseResult(
workers,
filesToUpdate,
Expand All @@ -337,6 +344,7 @@ function maybeRequestModelUpdate(
getParseCacheOptions(),
)
.then((parseResult) => {
notifyOperationFinished(dispatch, { type: 'parseFiles' }, ImportOperationResult.Success)
const duration = endMeasure()
if (isConcurrencyLoggingEnabled() && filesToUpdate.length > 1) {
console.info(
Expand Down Expand Up @@ -364,12 +372,19 @@ function maybeRequestModelUpdate(
)
}

dispatch([EditorActions.mergeWithPrevUndo(actionsToDispatch)])
dispatch([
EditorActions.mergeWithPrevUndo(actionsToDispatch),
updateImportStatus({ status: 'done' }),
])
return true
})
.catch((e) => {
console.error('error during parse', e)
dispatch([EditorActions.clearParseOrPrintInFlight()])
notifyOperationFinished(dispatch, { type: 'parseFiles' }, ImportOperationResult.Error)
dispatch([
EditorActions.clearParseOrPrintInFlight(),
updateImportStatus({ status: 'done' }),
])
return true
})
return {
Expand Down
3 changes: 0 additions & 3 deletions editor/src/components/github/github-repository-clone-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ import { notice } from '../common/notice'
import { isFeatureEnabled } from '../../utils/feature-switches'
import {
notifyOperationCriticalError,
notifyOperationFinished,
startImportProcess,
updateProjectImportStatus,
} from '../../core/shared/import/import-operation-service'
import { ImportOperationResult } from '../../core/shared/import/import-operation-types'

export const LoadActionsDispatched = 'loadActionDispatched'

Expand Down
4 changes: 3 additions & 1 deletion editor/src/core/shared/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ function isPackageMissing(status: PackageDetails): boolean {
return status.status === 'error' || status.status === 'not-found'
}

function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap): ImportOperationResult {
export function getDependenciesStatus(
loadedPackagesStatus: PackageStatusMap,
): ImportOperationResult {
if (Object.values(loadedPackagesStatus).every(isPackageMissing)) {
return ImportOperationResult.Error
}
Expand Down
1 change: 0 additions & 1 deletion editor/src/core/shared/github/operations/load-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import {
notifyOperationStarted,
pauseImport,
startImportProcess,
updateProjectImportStatus,
} from '../../import/import-operation-service'
import {
RequirementResolutionResult,
Expand Down
4 changes: 2 additions & 2 deletions editor/src/core/shared/import/import-operation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import { sendDiscordMessage } from '../../../components/editor/server'
import type { DiscordEndpointSiteImport, DiscordMessageType } from 'utopia-shared/src/types'
import { getImportOperationText } from '../../../components/editor/import-wizard/import-wizard-helpers'

export function startImportProcess(dispatch: EditorDispatch) {
export function startImportProcess(dispatch: EditorDispatch, customSteps?: ImportOperation[]) {
const actions: EditorAction[] = [
updateImportStatus({ status: 'in-progress' }),
updateImportOperations(
[
customSteps ?? [
{ type: 'loadBranch' },
{ type: 'checkRequirementsPreParse' },
{ type: 'parseFiles' },
Expand Down
7 changes: 7 additions & 0 deletions editor/src/templates/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ import { getParserWorkerCount } from '../core/workers/common/concurrency-utils'
import { canMeasurePerformance } from '../core/performance/performance-utils'
import { getChildGroupsForNonGroupParents } from '../components/canvas/canvas-strategies/strategies/fragment-like-helpers'
import { EditorModes } from '../components/editor/editor-modes'
import { startImportProcess } from '../core/shared/import/import-operation-service'
import { LoadingEditorComponent } from '../components/editor/loading-screen'

if (PROBABLY_ELECTRON) {
let { webFrame } = requireElectron()
Expand Down Expand Up @@ -267,6 +269,10 @@ export class Editor {
projectName: string,
project: PersistentModel,
) => {
startImportProcess(this.boundDispatch, [
{ type: 'parseFiles' },
{ type: 'refreshDependencies' },
])
await load(this.boundDispatch, project, projectName, projectId, builtInDependencies)
PubSub.publish(LoadActionsDispatched, { projectId: projectId })
}
Expand Down Expand Up @@ -768,6 +774,7 @@ export const EditorRoot: React.FunctionComponent<{
<AnimationContext.Provider
value={{ animate: animate, scope: animationScope }}
>
<LoadingEditorComponent />
<EditorComponent />
</AnimationContext.Provider>
</UiJsxCanvasCtxAtom.Provider>
Expand Down
Loading

0 comments on commit 9e99bae

Please sign in to comment.