Skip to content

Commit b08b24f

Browse files
authored
feat(import): split checks (#6618)
This PR splits the validation checks to allow an early break before parsing the files. In the UI they are split to these phases: - **Validating code** (which are actually the pre-parse validations) - Parse files (as today) - **Checking Utopia requirements** (which are the post-parse validations) **Details:** 1. The requirements are still aggregated in the editor state, but they are separated to phases for running as well as for displaying in the wizard ([see the separation here](https://github.com/concrete-utopia/utopia/pull/6618/files#diff-15c53165ea0f64ccf6f092783f2dce88639666dd1f4fd29e46035dc96ca4f2b3R20-R30)). 2. Main changes are in [editor/src/core/shared/import/project-health-check/utopia-requirements-service.ts](https://github.com/concrete-utopia/utopia/pull/6618/files#diff-9691feb390abe09b4d2b2aca374b464cf8bcb91a10942f8f6f07a2b4e9355fa6), where the split actually happens (`startPreParseValidation` and `startPostParseValidation`). 3. Renaming the requirement phases added changes in several other files, but they are simply refactors. 4. Minor - we also remove the shimmer when pausing the wizard (to make it clearer that the import has stopped). See here - first a successful project and then one with errors: <video src="https://github.com/user-attachments/assets/a907fe6b-68be-44a9-8a01-7e951ed90001"></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 c49b3b9 commit b08b24f

16 files changed

+351
-177
lines changed

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

+32
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
11
import React from 'react'
22
import { Global, css } from '@emotion/react'
33
import { useColorTheme } from '../../uuiui'
4+
import { useEditorState } from '../editor/store/store-hook'
5+
import { Substores } from '../editor/store/store-hook'
46

57
export const CanvasLoadingScreen = React.memo(() => {
68
const colorTheme = useColorTheme()
9+
const importState = useEditorState(
10+
Substores.github,
11+
(store) => store.editor.importState,
12+
'CanvasLoadingScreen importState',
13+
)
14+
const importWizardOpen = useEditorState(
15+
Substores.restOfEditor,
16+
(store) => store.editor.importWizardOpen,
17+
'CanvasLoadingScreen importWizardOpen',
18+
)
19+
20+
const importingStoppedStyleOverride = React.useMemo(
21+
() =>
22+
// if the importing was stopped, we want to pause the shimmer animation
23+
(importWizardOpen && importState.importStatus.status === 'done') ||
24+
importState.importStatus.status === 'paused'
25+
? {
26+
background: colorTheme.codeEditorShimmerPrimary.value,
27+
animation: 'none',
28+
}
29+
: {},
30+
[importWizardOpen, importState.importStatus.status, colorTheme.codeEditorShimmerPrimary.value],
31+
)
32+
733
return (
834
<React.Fragment>
935
<Global
@@ -34,6 +60,11 @@ export const CanvasLoadingScreen = React.memo(() => {
3460
background-size: 1468px 104px;
3561
position: relative;
3662
}
63+
64+
.no-shimmer {
65+
animation: none;
66+
background: ${colorTheme.codeEditorShimmerPrimary.value};
67+
}
3768
`}
3869
/>
3970
<div
@@ -48,6 +79,7 @@ export const CanvasLoadingScreen = React.memo(() => {
4879
top: 0,
4980
width: '100vw',
5081
height: '100vh',
82+
...importingStoppedStyleOverride,
5183
}}
5284
></div>
5385
</div>

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

+38-33
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
import { jsx } from '@emotion/react'
44
import React from 'react'
55
import type {
6-
ImportCheckRequirementAndFix,
76
ImportFetchDependency,
87
ImportOperation,
98
} from '../../../core/shared/import/import-operation-types'
109
import { ImportOperationResult } from '../../../core/shared/import/import-operation-types'
1110
import { assertNever } from '../../../core/shared/utils'
1211
import { Icn, Icons, useColorTheme } from '../../../uuiui'
1312
import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner'
14-
import { RequirementResolutionResult } from '../../../core/shared/import/project-health-check/utopia-requirements-types'
1513

1614
export function OperationLine({ operation }: { operation: ImportOperation }) {
1715
const operationRunningStatus = React.useMemo(() => {
@@ -58,7 +56,18 @@ export function OperationLine({ operation }: { operation: ImportOperation }) {
5856
</div>
5957
) : null}
6058
</OperationLineContent>
61-
{shouldShowChildren && hasChildren ? <OperationChildrenList operation={operation} /> : null}
59+
{shouldShowChildren && hasChildren ? (
60+
<div
61+
className='import-wizard-operation-children'
62+
style={{
63+
display: 'flex',
64+
flexDirection: 'column',
65+
gap: 15,
66+
}}
67+
>
68+
<OperationChildrenList operation={operation} />
69+
</div>
70+
) : null}
6271
</OperationLineWrapper>
6372
)
6473
}
@@ -67,39 +76,31 @@ function OperationChildrenList({ operation }: { operation: ImportOperation }) {
6776
if (operation.children == null || operation.children.length === 0) {
6877
return null
6978
}
79+
// this is a special case where we don't list all of the children
80+
// but we collapse the successful ones to a single line
81+
if (operation.type === 'refreshDependencies') {
82+
return (
83+
<AggregatedChildrenStatus
84+
childOperations={operation.children as ImportFetchDependency[]}
85+
successFn={dependenciesSuccessFn}
86+
successTextFn={dependenciesSuccessTextFn}
87+
/>
88+
)
89+
}
90+
// otherwise, we list all of the children
7091
return (
71-
<div
72-
className='import-wizard-operation-children'
73-
style={{
74-
display: 'flex',
75-
flexDirection: 'column',
76-
gap: 15,
77-
}}
78-
>
79-
{operation.type === 'refreshDependencies' ? (
80-
<AggregatedChildrenStatus
81-
childOperations={operation.children as ImportFetchDependency[]}
82-
successFn={dependenciesSuccessFn}
83-
successTextFn={dependenciesSuccessTextFn}
84-
/>
85-
) : operation.type === 'checkRequirements' ? (
86-
operation.children.map((childOperation) => (
87-
<OperationLine
88-
key={childOperation.id ?? childOperation.type}
89-
operation={childOperation}
90-
/>
91-
))
92-
) : null}
93-
</div>
92+
<React.Fragment>
93+
{operation.children?.map((childOperation) => (
94+
<OperationLine key={childOperation.id ?? childOperation.type} operation={childOperation} />
95+
))}
96+
</React.Fragment>
9497
)
9598
}
99+
96100
const dependenciesSuccessFn = (op: ImportFetchDependency) =>
97101
op.result === ImportOperationResult.Success
98102
const dependenciesSuccessTextFn = (successCount: number) =>
99103
`${successCount} dependencies fetched successfully`
100-
const requirementsSuccessFn = (op: ImportCheckRequirementAndFix) =>
101-
op.resolution === RequirementResolutionResult.Passed
102-
const requirementsSuccessTextFn = (successCount: number) => `${successCount} requirements met`
103104

104105
function AggregatedChildrenStatus<T extends ImportOperation>({
105106
childOperations,
@@ -255,7 +256,7 @@ function getImportOperationText(operation: ImportOperation): React.ReactNode {
255256
if (operation.branchName != null) {
256257
return (
257258
<span>
258-
Loading branch{' '}
259+
Fetching branch{' '}
259260
<strong>
260261
{operation.githubRepo?.owner}/{operation.githubRepo?.repository}@
261262
{operation.branchName}
@@ -265,7 +266,7 @@ function getImportOperationText(operation: ImportOperation): React.ReactNode {
265266
} else {
266267
return (
267268
<span>
268-
Loading repository{' '}
269+
Fetching repository{' '}
269270
<strong>
270271
{operation.githubRepo?.owner}/{operation.githubRepo?.repository}
271272
</strong>
@@ -278,9 +279,13 @@ function getImportOperationText(operation: ImportOperation): React.ReactNode {
278279
return 'Parsing files'
279280
case 'refreshDependencies':
280281
return 'Fetching dependencies'
281-
case 'checkRequirements':
282+
case 'checkRequirementsPreParse':
283+
return 'Validating code'
284+
case 'checkRequirementsPostParse':
282285
return 'Checking Utopia requirements'
283-
case 'checkRequirementAndFix':
286+
case 'checkRequirementAndFixPreParse':
287+
return operation.text
288+
case 'checkRequirementAndFixPostParse':
284289
return operation.text
285290
default:
286291
assertNever(operation)

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

+63-72
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@ export const ImportWizard = React.memo(() => {
4141

4242
const operations = importState.importOperations
4343

44-
const dispatch = useDispatch()
45-
46-
const handleDismiss = React.useCallback(() => {
47-
hideImportWizard(dispatch)
48-
}, [dispatch])
49-
5044
const stopPropagation = React.useCallback((e: React.MouseEvent) => {
5145
e.stopPropagation()
5246
}, [])
@@ -84,7 +78,7 @@ export const ImportWizard = React.memo(() => {
8478
boxShadow: UtopiaStyles.popup.boxShadow,
8579
borderRadius: 10,
8680
width: 600,
87-
height: 450,
81+
height: 480,
8882
position: 'relative',
8983
display: 'flex',
9084
flexDirection: 'column',
@@ -106,20 +100,7 @@ export const ImportWizard = React.memo(() => {
106100
flex: 'none',
107101
}}
108102
>
109-
<div css={{ fontSize: 16, fontWeight: 400 }}>Loading Project</div>
110-
{when(
111-
totalImportResult.importStatus.status === 'in-progress',
112-
<Button
113-
highlight
114-
style={{
115-
padding: 15,
116-
color: colorTheme.fg6.value,
117-
}}
118-
onClick={handleDismiss}
119-
>
120-
Cancel
121-
</Button>,
122-
)}
103+
<div css={{ fontSize: 16, fontWeight: 400 }}>Cloning Project</div>
123104
</FlexRow>
124105
<div
125106
className='import-wizard-body'
@@ -218,56 +199,66 @@ function ActionButtons({ importResult }: { importResult: TotalImportResult }) {
218199
) {
219200
return null
220201
}
221-
if (importResult.result == ImportOperationResult.Success) {
222-
return (
223-
<React.Fragment>
224-
<div style={textStyle}>Project Imported Successfully</div>
225-
<Button onClick={hideWizard} style={buttonStyle}>
226-
Continue To Editor
227-
</Button>
228-
</React.Fragment>
229-
)
230-
}
231-
if (importResult.result == ImportOperationResult.Warn) {
232-
return (
233-
<React.Fragment>
234-
<div style={textStyle}>Project Imported With Warnings</div>
235-
<Button onClick={hideWizard} style={buttonStyle}>
236-
Continue To Editor
237-
</Button>
238-
</React.Fragment>
239-
)
240-
}
241-
if (
242-
importResult.result == ImportOperationResult.Error ||
243-
importResult.result == ImportOperationResult.CriticalError
244-
) {
245-
return (
246-
<React.Fragment>
247-
<div style={textStyle}>
248-
{importResult.importStatus.status !== 'done' ||
249-
importResult.result === ImportOperationResult.CriticalError
250-
? 'Error Importing Project'
251-
: 'Project Imported With Errors'}
252-
</div>
253-
<Button style={{ ...buttonStyle, marginLeft: 'auto' }} onClick={importADifferentProject}>
254-
Import A Different Project
255-
</Button>
256-
{unless(
257-
importResult.result == ImportOperationResult.CriticalError,
258-
<Button
259-
style={{
260-
cursor: 'pointer',
261-
}}
262-
onClick={continueAnyway}
263-
>
264-
{importResult.importStatus.status === 'done'
265-
? 'Continue To Editor'
266-
: 'Continue Importing'}
267-
</Button>,
268-
)}
269-
</React.Fragment>
270-
)
202+
switch (importResult.result) {
203+
case ImportOperationResult.Success:
204+
return (
205+
<React.Fragment>
206+
<div style={textStyle}>Project Imported Successfully</div>
207+
<Button onClick={hideWizard} style={buttonStyle}>
208+
Continue To Editor
209+
</Button>
210+
</React.Fragment>
211+
)
212+
case ImportOperationResult.Warn:
213+
return (
214+
<React.Fragment>
215+
<div style={textStyle}>Project Imported With Warnings</div>
216+
<Button onClick={hideWizard} style={buttonStyle}>
217+
Continue To Editor
218+
</Button>
219+
</React.Fragment>
220+
)
221+
case ImportOperationResult.CriticalError:
222+
return (
223+
<React.Fragment>
224+
<div style={textStyle}>Error Importing Project</div>
225+
<Button style={{ ...buttonStyle, marginLeft: 'auto' }} onClick={importADifferentProject}>
226+
Cancel
227+
</Button>
228+
</React.Fragment>
229+
)
230+
case ImportOperationResult.Error:
231+
return (
232+
<React.Fragment>
233+
<div style={textStyle}>
234+
{importResult.importStatus.status !== 'done'
235+
? 'Error While Importing Project'
236+
: 'Project Imported With Errors'}
237+
</div>
238+
{when(
239+
importResult.importStatus.status !== 'done',
240+
<Button
241+
style={{
242+
cursor: 'pointer',
243+
marginLeft: 'auto',
244+
}}
245+
onClick={continueAnyway}
246+
>
247+
Continue Anyway
248+
</Button>,
249+
)}
250+
{importResult.importStatus.status === 'done' ? (
251+
<Button style={{ ...buttonStyle }} onClick={hideWizard}>
252+
Continue To Editor
253+
</Button>
254+
) : (
255+
<Button style={{ ...buttonStyle }} onClick={importADifferentProject}>
256+
Cancel
257+
</Button>
258+
)}
259+
</React.Fragment>
260+
)
261+
default:
262+
assertNever(importResult.result)
271263
}
272-
return null
273264
}

editor/src/components/editor/store/editor-state.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,13 @@ import type { FancyError } from '../../../core/shared/code-exec-utils'
191191
import type { GridCellCoordinates } from '../../canvas/canvas-strategies/strategies/grid-cell-bounds'
192192
import {
193193
emptyImportState,
194-
type ImportOperation,
195194
type ImportState,
196195
} from '../../../core/shared/import/import-operation-types'
197196
import {
198197
emptyProjectRequirements,
199198
type ProjectRequirements,
200199
} from '../../../core/shared/import/project-health-check/utopia-requirements-types'
200+
import { isFeatureEnabled } from '../../../utils/feature-switches'
201201

202202
const ObjectPathImmutable: any = OPI
203203

@@ -362,6 +362,8 @@ export function githubOperationLocksEditor(op: GithubOperation): boolean {
362362
case 'loadRepositories':
363363
case 'listPullRequestsForBranch':
364364
return false
365+
case 'loadBranch':
366+
return !isFeatureEnabled('Import Wizard')
365367
default:
366368
return true
367369
}

0 commit comments

Comments
 (0)