From 9e99bae24b2988e2f4372066a9fb783438a1e7a9 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 25 Nov 2024 17:12:01 +0200 Subject: [PATCH] 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 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. **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 --- .../canvas/canvas-loading-screen.tsx | 17 +- .../src/components/editor/actions/actions.tsx | 12 ++ .../editor/import-wizard/import-wizard.tsx | 8 + .../src/components/editor/loading-screen.tsx | 174 ++++++++++++++++++ .../src/components/editor/store/dispatch.tsx | 19 +- .../github/github-repository-clone-flow.tsx | 3 - editor/src/core/shared/dependencies.ts | 4 +- .../shared/github/operations/load-branch.ts | 1 - .../shared/import/import-operation-service.ts | 4 +- editor/src/templates/editor.tsx | 7 + editor/src/templates/index.html | 136 +++++++++----- editor/src/uuiui/styles/theme/dark.ts | 1 + editor/src/uuiui/styles/theme/light.ts | 1 + 13 files changed, 333 insertions(+), 54 deletions(-) create mode 100644 editor/src/components/editor/loading-screen.tsx diff --git a/editor/src/components/canvas/canvas-loading-screen.tsx b/editor/src/components/canvas/canvas-loading-screen.tsx index 54e4c74042e4..854dec875c93 100644 --- a/editor/src/components/canvas/canvas-loading-screen.tsx +++ b/editor/src/components/canvas/canvas-loading-screen.tsx @@ -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() @@ -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 ( diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index de08aba96446..1f910fb5b9b0 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -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' @@ -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' @@ -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, @@ -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, diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 446e414fba93..8c5c54b76c86 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -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' diff --git a/editor/src/components/editor/loading-screen.tsx b/editor/src/components/editor/loading-screen.tsx new file mode 100644 index 000000000000..c3287334c007 --- /dev/null +++ b/editor/src/components/editor/loading-screen.tsx @@ -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( + + +
+
+
+
+
    + {currentOperationToShow != null ? ( +
  • + {currentOperationToShow.text} +
  • + ) : null} +
+
+
, + portal, + ) +} diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 6934b776d1bb..44a7a54adf44 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -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 @@ -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, @@ -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( @@ -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 { diff --git a/editor/src/components/github/github-repository-clone-flow.tsx b/editor/src/components/github/github-repository-clone-flow.tsx index a8688987f661..9ea1ca795b86 100644 --- a/editor/src/components/github/github-repository-clone-flow.tsx +++ b/editor/src/components/github/github-repository-clone-flow.tsx @@ -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' diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index ff6c48ed11a2..d9c1d9a5881b 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -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 } diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 1c4d29aa0c21..30d4ffc40ba3 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -49,7 +49,6 @@ import { notifyOperationStarted, pauseImport, startImportProcess, - updateProjectImportStatus, } from '../../import/import-operation-service' import { RequirementResolutionResult, diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index 9e9f95dfd111..6db47dfea46f 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -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' }, diff --git a/editor/src/templates/editor.tsx b/editor/src/templates/editor.tsx index 02bb102620c0..544fba73df9e 100644 --- a/editor/src/templates/editor.tsx +++ b/editor/src/templates/editor.tsx @@ -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() @@ -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 }) } @@ -768,6 +774,7 @@ export const EditorRoot: React.FunctionComponent<{ + diff --git a/editor/src/templates/index.html b/editor/src/templates/index.html index f00fc9341974..f3481b72d1d9 100644 --- a/editor/src/templates/index.html +++ b/editor/src/templates/index.html @@ -15,6 +15,11 @@ as="image" /> + + @@ -47,6 +52,15 @@ from { transform: translateX(-212px); } + to { + transform: translateX(-180px); + } + } + + @keyframes animation-keyframes-2 { + from { + transform: translateX(-180px); + } to { transform: translateX(0px); } @@ -61,9 +75,23 @@ } } + .editor-loading-screen { + position: fixed; + left: 0px; + top: 0px; + bottom: 0px; + right: 0px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: white; + z-index: 1000; + } + .animation-progress { animation-name: animation-keyframes; - animation-duration: 10s; + animation-duration: 30s; animation-iteration-count: infinite; animation-direction: alternate; } @@ -78,10 +106,34 @@ .progress-bar-shell { border: 1px solid black; + margin-top: 64px; + width: 212px; + height: 11px; + border-radius: 8px; + overflow: hidden; + box-sizing: border-box !important; } .progress-bar-progress { background-color: black; + border-radius: 6px; + height: 9px; + } + + .loading-screen-import-operations { + color: black; + height: 150px; + overflow: hidden; + width: auto; + display: flex; + list-style: none; + flex-direction: column; + align-items: center; + margin: 0; + padding: 0; + gap: 2px; + margin-top: 10px; + font-family: utopian-inter, 'sans-serif'; } .utopia-logo-pyramid.light { @@ -91,11 +143,27 @@ display: none; } + .loading-screen-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + @media (prefers-color-scheme: dark) { body { background-color: #1a1a1a; } + .editor-loading-screen { + background-color: #1a1a1a; + } + + .loading-screen-import-operations, + .loading-screen-import-operations li { + color: white; + } + .progress-bar-shell { border: 1px solid white; } @@ -137,54 +205,38 @@ justify-content: stretch; pointer-events: initial; " - > - - + > + + +
+ Utopia Logo + Utopia Logo
- Utopia Logo - Utopia Logo - -
+
- - +
    +
  • Loading Editor...
  • +
+ +
diff --git a/editor/src/uuiui/styles/theme/dark.ts b/editor/src/uuiui/styles/theme/dark.ts index 6c015f5afbf9..b1e57f176aa7 100644 --- a/editor/src/uuiui/styles/theme/dark.ts +++ b/editor/src/uuiui/styles/theme/dark.ts @@ -59,6 +59,7 @@ const darkBase = { bg3: createUtopiColor('#393d49'), bg4: createUtopiColor('#55575f'), bg5: createUtopiColor('#848998'), + bg6: createUtopiColor('#1a1a1a'), fg0: createUtopiColor('#ffffff'), fg1: createUtopiColor('#D9DCE3'), fg2: createUtopiColor('#c9cCc3'), diff --git a/editor/src/uuiui/styles/theme/light.ts b/editor/src/uuiui/styles/theme/light.ts index 2709b29c7a0a..69ef0bca8362 100644 --- a/editor/src/uuiui/styles/theme/light.ts +++ b/editor/src/uuiui/styles/theme/light.ts @@ -59,6 +59,7 @@ const lightBase = { bg3: createUtopiColor('hsl(0,0%,94%)'), bg4: createUtopiColor('hsl(0,0%,92%)'), bg5: createUtopiColor('hsl(0,0%,90%)'), + bg6: createUtopiColor('#ffffff'), fg0: createUtopiColor('hsl(0,0%,0%)'), fg1: createUtopiColor('hsl(0,0%,10%)'), fg2: createUtopiColor('hsl(0,0%,20%)'),