Skip to content

Commit 9e99bae

Browse files
authored
feat(loading): merge github fetch with the loading bar (#6630)
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
1 parent 7283a1d commit 9e99bae

File tree

13 files changed

+333
-54
lines changed

13 files changed

+333
-54
lines changed

editor/src/components/canvas/canvas-loading-screen.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Global, css } from '@emotion/react'
33
import { useColorTheme } from '../../uuiui'
44
import { useEditorState } from '../editor/store/store-hook'
55
import { Substores } from '../editor/store/store-hook'
6+
import { getTotalImportStatusAndResult } from '../../core/shared/import/import-operation-service'
7+
import type { TotalImportResult } from '../../core/shared/import/import-operation-types'
68

79
export const CanvasLoadingScreen = React.memo(() => {
810
const colorTheme = useColorTheme()
@@ -17,17 +19,26 @@ export const CanvasLoadingScreen = React.memo(() => {
1719
'CanvasLoadingScreen importWizardOpen',
1820
)
1921

22+
const totalImportResult: TotalImportResult = React.useMemo(
23+
() => getTotalImportStatusAndResult(importState),
24+
[importState],
25+
)
26+
2027
const importingStoppedStyleOverride = React.useMemo(
2128
() =>
2229
// if the importing was stopped, we want to pause the shimmer animation
23-
(importWizardOpen && importState.importStatus.status === 'done') ||
24-
importState.importStatus.status === 'paused'
30+
(importWizardOpen && totalImportResult.importStatus.status === 'done') ||
31+
totalImportResult.importStatus.status === 'paused'
2532
? {
2633
background: colorTheme.codeEditorShimmerPrimary.value,
2734
animation: 'none',
2835
}
2936
: {},
30-
[importWizardOpen, importState.importStatus.status, colorTheme.codeEditorShimmerPrimary.value],
37+
[
38+
importWizardOpen,
39+
totalImportResult.importStatus.status,
40+
colorTheme.codeEditorShimmerPrimary.value,
41+
],
3142
)
3243

3344
return (

editor/src/components/editor/actions/actions.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ import {
520520
import { addToastToState, includeToast, removeToastFromState } from './toast-helpers'
521521
import { AspectRatioLockedProp } from '../../aspect-ratio'
522522
import {
523+
getDependenciesStatus,
523524
refreshDependencies,
524525
removeModulesFromNodeModules,
525526
} from '../../../core/shared/dependencies'
@@ -629,6 +630,8 @@ import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
629630
import { styleP } from '../../inspector/inspector-common'
630631
import {
631632
getUpdateOperationResult,
633+
notifyOperationFinished,
634+
notifyOperationStarted,
632635
notifyImportStatusToDiscord,
633636
} from '../../../core/shared/import/import-operation-service'
634637
import { updateRequirements } from '../../../core/shared/import/project-health-check/utopia-requirements-service'
@@ -6319,6 +6322,8 @@ export async function load(
63196322
// this action is now async!
63206323
const migratedModel = applyMigrations(model)
63216324
const npmDependencies = dependenciesWithEditorRequirements(migratedModel.projectContents)
6325+
// side effect ☢️
6326+
notifyOperationStarted(dispatch, { type: 'refreshDependencies' })
63226327
const fetchNodeModulesResult = await fetchNodeModules(
63236328
dispatch,
63246329
npmDependencies,
@@ -6333,6 +6338,13 @@ export async function load(
63336338
fetchNodeModulesResult.dependenciesNotFound,
63346339
)
63356340

6341+
// side effect ☢️
6342+
notifyOperationFinished(
6343+
dispatch,
6344+
{ type: 'refreshDependencies' },
6345+
getDependenciesStatus(packageResult),
6346+
)
6347+
63366348
const codeResultCache: CodeResultCache = generateCodeResultCache(
63376349
// TODO is this sufficient here?
63386350
migratedModel.projectContents,

editor/src/components/editor/import-wizard/import-wizard.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ function ActionButtons() {
188188
fontSize: 14,
189189
cursor: 'pointer',
190190
}
191+
React.useEffect(() => {
192+
if (
193+
importResult.importStatus.status == 'done' &&
194+
importResult.result == ImportOperationResult.Success
195+
) {
196+
hideWizard()
197+
}
198+
}, [importResult, hideWizard])
191199
if (
192200
importResult.importStatus.status === 'in-progress' ||
193201
importResult.importStatus.status === 'not-started'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React from 'react'
2+
import { Substores, useEditorState } from './store/store-hook'
3+
import { getImportOperationTextAsJsx } from './import-wizard/import-wizard-helpers'
4+
import { getTotalImportStatusAndResult } from '../../core/shared/import/import-operation-service'
5+
import type { TotalImportResult } from '../../core/shared/import/import-operation-types'
6+
import type { Theme } from '../../uuiui'
7+
import { useColorTheme } from '../../uuiui'
8+
import { getCurrentTheme } from './store/editor-state'
9+
import ReactDOM from 'react-dom'
10+
11+
export function LoadingEditorComponent() {
12+
const colorTheme = useColorTheme()
13+
14+
const currentTheme: Theme = useEditorState(
15+
Substores.theme,
16+
(store) => getCurrentTheme(store.userState),
17+
'currentTheme',
18+
)
19+
20+
const importState = useEditorState(
21+
Substores.restOfEditor,
22+
(store) => store.editor.importState,
23+
'LoadingEditorComponent importState',
24+
)
25+
26+
const githubRepo = useEditorState(
27+
Substores.userState,
28+
(store) => store.userState.githubState.gitRepoToLoad,
29+
'LoadingEditorComponent githubRepoToLoad',
30+
)
31+
32+
const totalImportResult: TotalImportResult = React.useMemo(
33+
() => getTotalImportStatusAndResult(importState),
34+
[importState],
35+
)
36+
37+
const projectId = useEditorState(
38+
Substores.restOfEditor,
39+
(store) => store.editor.id,
40+
'LoadingEditorComponent projectId',
41+
)
42+
43+
const cleared = React.useRef(false)
44+
45+
const currentOperationToShow: {
46+
text: React.ReactNode
47+
id: string
48+
timeDone: number | null | undefined
49+
timeStarted: number | null | undefined
50+
} | null = React.useMemo(() => {
51+
if (totalImportResult.importStatus.status == 'not-started') {
52+
if (projectId == null) {
53+
return {
54+
text: 'Loading Editor...',
55+
id: 'loading-editor',
56+
timeDone: null,
57+
timeStarted: null,
58+
}
59+
} else {
60+
return {
61+
text: `Parsing files`,
62+
id: 'parseFiles',
63+
timeDone: null,
64+
timeStarted: null,
65+
}
66+
}
67+
}
68+
for (const op of importState.importOperations) {
69+
if (op?.children?.length == 0 || op.type == 'refreshDependencies') {
70+
if (op.timeStarted != null && op.timeDone == null) {
71+
return {
72+
text: getImportOperationTextAsJsx(op),
73+
id: op.id ?? op.type,
74+
timeDone: op.timeDone,
75+
timeStarted: op.timeStarted,
76+
}
77+
}
78+
}
79+
if (op.type !== 'refreshDependencies') {
80+
for (const child of op.children ?? []) {
81+
if (child.timeStarted != null && child.timeDone == null) {
82+
return {
83+
text: getImportOperationTextAsJsx(child),
84+
id: child.id ?? child.type,
85+
timeDone: child.timeDone,
86+
timeStarted: child.timeStarted,
87+
}
88+
}
89+
}
90+
}
91+
}
92+
return {
93+
text: 'Loading Editor...',
94+
id: 'loading-editor',
95+
timeDone: null,
96+
timeStarted: null,
97+
}
98+
}, [totalImportResult, importState.importOperations, projectId])
99+
100+
const shouldBeCleared = React.useMemo(() => {
101+
return (
102+
cleared.current ||
103+
(totalImportResult.importStatus.status == 'done' &&
104+
(githubRepo == null || totalImportResult.result == 'criticalError')) ||
105+
totalImportResult.importStatus.status == 'paused'
106+
)
107+
}, [totalImportResult, githubRepo])
108+
109+
React.useEffect(() => {
110+
if (shouldBeCleared) {
111+
const loadingScreenWrapper = document.getElementById('loading-screen-wrapper')
112+
if (loadingScreenWrapper != null) {
113+
loadingScreenWrapper.remove()
114+
}
115+
}
116+
}, [shouldBeCleared])
117+
118+
const portal = React.useRef(document.getElementById('loading-screen-progress-bar-portal')).current
119+
const hasMounted = React.useRef(false)
120+
if (portal == null) {
121+
return null
122+
}
123+
124+
if (shouldBeCleared) {
125+
cleared.current = true
126+
return null
127+
}
128+
129+
if (!hasMounted.current) {
130+
portal.innerHTML = ''
131+
hasMounted.current = true
132+
}
133+
134+
const themeStyle =
135+
currentTheme === 'dark'
136+
? `
137+
.editor-loading-screen { background-color: ${colorTheme.bg6.value} }
138+
.utopia-logo-pyramid.light { display: none; }
139+
.utopia-logo-pyramid.dark { display: block; }
140+
`
141+
: ''
142+
143+
return ReactDOM.createPortal(
144+
<React.Fragment>
145+
<style>{themeStyle}</style>
146+
<div className='progress-bar-shell' style={{ borderColor: colorTheme.fg0.value }}>
147+
<div
148+
className='progress-bar-progress animation-progress'
149+
style={{
150+
transform: 'translateX(-180px)',
151+
animationName: 'animation-keyframes-2',
152+
backgroundColor: colorTheme.fg0.value,
153+
}}
154+
></div>
155+
</div>
156+
<div>
157+
<ul className='loading-screen-import-operations'>
158+
{currentOperationToShow != null ? (
159+
<li
160+
style={{
161+
listStyle: 'none',
162+
color: colorTheme.fg0.value,
163+
}}
164+
key={currentOperationToShow.id}
165+
>
166+
{currentOperationToShow.text}
167+
</li>
168+
) : null}
169+
</ul>
170+
</div>
171+
</React.Fragment>,
172+
portal,
173+
)
174+
}

editor/src/components/editor/store/dispatch.tsx

+17-2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ import {
8989
} from '../../../core/performance/performance-utils'
9090
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
9191
import { resetUpdatedProperties } from '../../canvas/plugins/style-plugins'
92+
import {
93+
notifyOperationFinished,
94+
notifyOperationStarted,
95+
} from '../../../core/shared/import/import-operation-service'
96+
import { ImportOperationResult } from '../../../core/shared/import/import-operation-types'
97+
import { updateImportStatus } from '../actions/action-creators'
9298

9399
type DispatchResultFields = {
94100
nothingChanged: boolean
@@ -327,6 +333,7 @@ function maybeRequestModelUpdate(
327333
// Should anything need to be sent across, do so here.
328334
if (filesToUpdate.length > 0) {
329335
const { endMeasure } = startPerformanceMeasure('file-parse', { uniqueId: true })
336+
notifyOperationStarted(dispatch, { type: 'parseFiles' })
330337
const parseFinished = getParseResult(
331338
workers,
332339
filesToUpdate,
@@ -337,6 +344,7 @@ function maybeRequestModelUpdate(
337344
getParseCacheOptions(),
338345
)
339346
.then((parseResult) => {
347+
notifyOperationFinished(dispatch, { type: 'parseFiles' }, ImportOperationResult.Success)
340348
const duration = endMeasure()
341349
if (isConcurrencyLoggingEnabled() && filesToUpdate.length > 1) {
342350
console.info(
@@ -364,12 +372,19 @@ function maybeRequestModelUpdate(
364372
)
365373
}
366374

367-
dispatch([EditorActions.mergeWithPrevUndo(actionsToDispatch)])
375+
dispatch([
376+
EditorActions.mergeWithPrevUndo(actionsToDispatch),
377+
updateImportStatus({ status: 'done' }),
378+
])
368379
return true
369380
})
370381
.catch((e) => {
371382
console.error('error during parse', e)
372-
dispatch([EditorActions.clearParseOrPrintInFlight()])
383+
notifyOperationFinished(dispatch, { type: 'parseFiles' }, ImportOperationResult.Error)
384+
dispatch([
385+
EditorActions.clearParseOrPrintInFlight(),
386+
updateImportStatus({ status: 'done' }),
387+
])
373388
return true
374389
})
375390
return {

editor/src/components/github/github-repository-clone-flow.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,8 @@ import { notice } from '../common/notice'
2929
import { isFeatureEnabled } from '../../utils/feature-switches'
3030
import {
3131
notifyOperationCriticalError,
32-
notifyOperationFinished,
3332
startImportProcess,
34-
updateProjectImportStatus,
3533
} from '../../core/shared/import/import-operation-service'
36-
import { ImportOperationResult } from '../../core/shared/import/import-operation-types'
3734

3835
export const LoadActionsDispatched = 'loadActionDispatched'
3936

editor/src/core/shared/dependencies.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ function isPackageMissing(status: PackageDetails): boolean {
120120
return status.status === 'error' || status.status === 'not-found'
121121
}
122122

123-
function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap): ImportOperationResult {
123+
export function getDependenciesStatus(
124+
loadedPackagesStatus: PackageStatusMap,
125+
): ImportOperationResult {
124126
if (Object.values(loadedPackagesStatus).every(isPackageMissing)) {
125127
return ImportOperationResult.Error
126128
}

editor/src/core/shared/github/operations/load-branch.ts

-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import {
4949
notifyOperationStarted,
5050
pauseImport,
5151
startImportProcess,
52-
updateProjectImportStatus,
5352
} from '../../import/import-operation-service'
5453
import {
5554
RequirementResolutionResult,

editor/src/core/shared/import/import-operation-service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { sendDiscordMessage } from '../../../components/editor/server'
1919
import type { DiscordEndpointSiteImport, DiscordMessageType } from 'utopia-shared/src/types'
2020
import { getImportOperationText } from '../../../components/editor/import-wizard/import-wizard-helpers'
2121

22-
export function startImportProcess(dispatch: EditorDispatch) {
22+
export function startImportProcess(dispatch: EditorDispatch, customSteps?: ImportOperation[]) {
2323
const actions: EditorAction[] = [
2424
updateImportStatus({ status: 'in-progress' }),
2525
updateImportOperations(
26-
[
26+
customSteps ?? [
2727
{ type: 'loadBranch' },
2828
{ type: 'checkRequirementsPreParse' },
2929
{ type: 'parseFiles' },

editor/src/templates/editor.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ import { getParserWorkerCount } from '../core/workers/common/concurrency-utils'
137137
import { canMeasurePerformance } from '../core/performance/performance-utils'
138138
import { getChildGroupsForNonGroupParents } from '../components/canvas/canvas-strategies/strategies/fragment-like-helpers'
139139
import { EditorModes } from '../components/editor/editor-modes'
140+
import { startImportProcess } from '../core/shared/import/import-operation-service'
141+
import { LoadingEditorComponent } from '../components/editor/loading-screen'
140142

141143
if (PROBABLY_ELECTRON) {
142144
let { webFrame } = requireElectron()
@@ -267,6 +269,10 @@ export class Editor {
267269
projectName: string,
268270
project: PersistentModel,
269271
) => {
272+
startImportProcess(this.boundDispatch, [
273+
{ type: 'parseFiles' },
274+
{ type: 'refreshDependencies' },
275+
])
270276
await load(this.boundDispatch, project, projectName, projectId, builtInDependencies)
271277
PubSub.publish(LoadActionsDispatched, { projectId: projectId })
272278
}
@@ -768,6 +774,7 @@ export const EditorRoot: React.FunctionComponent<{
768774
<AnimationContext.Provider
769775
value={{ animate: animate, scope: animationScope }}
770776
>
777+
<LoadingEditorComponent />
771778
<EditorComponent />
772779
</AnimationContext.Provider>
773780
</UiJsxCanvasCtxAtom.Provider>

0 commit comments

Comments
 (0)