Skip to content

Commit a4cf331

Browse files
authored
feat(import): send import results to discord (#6662)
This PR adds the ability to send github import results (errors/warnings) to Discord (to the channel `#site-import-webhook`). **Details:** 1. Added a generic API endpoint for sending messages to Discord from our code. We use `discord-webhook-node` to format the message. The generic logic is here: [utopia-remix/app/util/discordWebhookUtils.ts](https://github.com/concrete-utopia/utopia/pull/6662/files#diff-77fb1f38c2e0b74476d2dac0b094d0c3f56cd598ca46f5ba2f348dd1afd8c4ae) 2. The code in the client that sends the results, here: [editor/src/core/shared/import/import-operation-service.ts](https://github.com/concrete-utopia/utopia/pull/6662/files#diff-426bff74c9da628f136029668cad91728508ef49a539c96a379c8c1e3f70e150R250-R265) 3. The code in the server that builds the textual site import message here: [utopia-remix/app/handlers/discordMessageBuilder.ts](https://github.com/concrete-utopia/utopia/pull/6662/files#diff-d2102489b75734896e5f1e06678c3f0ca926650eb08cdd14e00e79315f947302R22-R80) No other logic was changed, the rest of the code changes are mainly refactors / types. The code is generic and we can later [add](https://github.com/concrete-utopia/utopia/pull/6662/files#diff-77fb1f38c2e0b74476d2dac0b094d0c3f56cd598ca46f5ba2f348dd1afd8c4aeR20-R22) more Discord webhooks for other functionalities, easily. The webhook url for the specific channel is hidden in an env var - `DISCORD_WEBHOOK_SITE_IMPORT`, it is already set on our staging/production servers, and currently not on local machines. **Messages Example:** <img width="434" alt="image" src="https://github.com/user-attachments/assets/3b991dc1-f315-48b0-bccf-1d3df66591af"> **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 80561fc commit a4cf331

19 files changed

+545
-76
lines changed

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

+11-5
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,10 @@ import { canCondenseJSXElementChild } from '../../../utils/can-condense'
627627
import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-utils'
628628
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
629629
import { styleP } from '../../inspector/inspector-common'
630-
import { getUpdateOperationResult } from '../../../core/shared/import/import-operation-service'
630+
import {
631+
getUpdateOperationResult,
632+
notifyImportStatusToDiscord,
633+
} from '../../../core/shared/import/import-operation-service'
631634
import { updateRequirements } from '../../../core/shared/import/project-health-check/utopia-requirements-service'
632635
import {
633636
applyValuesAtPath,
@@ -2191,12 +2194,15 @@ export const UPDATE_FNS = {
21912194
}
21922195
},
21932196
UPDATE_IMPORT_STATUS: (action: UpdateImportStatus, editor: EditorModel): EditorModel => {
2197+
const newImportState = {
2198+
...editor.importState,
2199+
importStatus: action.importStatus,
2200+
}
2201+
// side effect ☢️
2202+
notifyImportStatusToDiscord(newImportState, editor.projectName)
21942203
return {
21952204
...editor,
2196-
importState: {
2197-
...editor.importState,
2198-
importStatus: action.importStatus,
2199-
},
2205+
importState: newImportState,
22002206
}
22012207
},
22022208
UPDATE_PROJECT_REQUIREMENTS: (

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

+2-47
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type {
77
ImportOperation,
88
} from '../../../core/shared/import/import-operation-types'
99
import { ImportOperationResult } from '../../../core/shared/import/import-operation-types'
10-
import { assertNever } from '../../../core/shared/utils'
1110
import { Icn, Icons, useColorTheme } from '../../../uuiui'
1211
import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner'
12+
import { getImportOperationTextAsJsx } from './import-wizard-helpers'
1313

1414
export function OperationLine({ operation }: { operation: ImportOperation }) {
1515
const operationRunningStatus = React.useMemo(() => {
@@ -46,7 +46,7 @@ export function OperationLine({ operation }: { operation: ImportOperation }) {
4646
>
4747
<OperationLineContent textColor={textColor}>
4848
<OperationIcon runningStatus={operationRunningStatus} result={operation.result} />
49-
<div>{getImportOperationText(operation)}</div>
49+
<div>{getImportOperationTextAsJsx(operation)}</div>
5050
<div>
5151
<TimeFromInSeconds operation={operation} runningStatus={operationRunningStatus} />
5252
</div>
@@ -246,48 +246,3 @@ function OperationLineContent({
246246
</div>
247247
)
248248
}
249-
250-
function getImportOperationText(operation: ImportOperation): React.ReactNode {
251-
if (operation.text != null) {
252-
return operation.text
253-
}
254-
switch (operation.type) {
255-
case 'loadBranch':
256-
if (operation.branchName != null) {
257-
return (
258-
<span>
259-
Fetching branch{' '}
260-
<strong>
261-
{operation.githubRepo?.owner}/{operation.githubRepo?.repository}@
262-
{operation.branchName}
263-
</strong>
264-
</span>
265-
)
266-
} else {
267-
return (
268-
<span>
269-
Fetching repository{' '}
270-
<strong>
271-
{operation.githubRepo?.owner}/{operation.githubRepo?.repository}
272-
</strong>
273-
</span>
274-
)
275-
}
276-
case 'fetchDependency':
277-
return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}`
278-
case 'parseFiles':
279-
return 'Parsing files'
280-
case 'refreshDependencies':
281-
return 'Fetching dependencies'
282-
case 'checkRequirementsPreParse':
283-
return 'Validating code'
284-
case 'checkRequirementsPostParse':
285-
return 'Checking Utopia requirements'
286-
case 'checkRequirementAndFixPreParse':
287-
return operation.text
288-
case 'checkRequirementAndFixPostParse':
289-
return operation.text
290-
default:
291-
assertNever(operation)
292-
}
293-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react'
2+
import {
3+
ImportOperationResult,
4+
type ImportOperation,
5+
} from '../../../core/shared/import/import-operation-types'
6+
import { assertNever } from '../../../core/shared/utils'
7+
8+
export function getImportOperationText(operation: ImportOperation): string {
9+
if (operation.text != null) {
10+
return operation.text
11+
}
12+
switch (operation.type) {
13+
case 'loadBranch':
14+
const action =
15+
operation.result == ImportOperationResult.Error ||
16+
operation.result == ImportOperationResult.CriticalError
17+
? 'Error Fetching'
18+
: 'Fetching'
19+
if (operation.branchName != null) {
20+
return `${action} branch **${operation.githubRepo?.owner}/${operation.githubRepo?.repository}@${operation.branchName}**`
21+
} else {
22+
return `${action} repository **${operation.githubRepo?.owner}/${operation.githubRepo?.repository}**`
23+
}
24+
case 'fetchDependency':
25+
return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}`
26+
case 'parseFiles':
27+
return 'Parsing files'
28+
case 'refreshDependencies':
29+
return 'Fetching dependencies'
30+
case 'checkRequirementsPreParse':
31+
return 'Validating code'
32+
case 'checkRequirementsPostParse':
33+
return 'Checking Utopia requirements'
34+
case 'checkRequirementAndFixPreParse':
35+
return operation.text
36+
case 'checkRequirementAndFixPostParse':
37+
return operation.text
38+
default:
39+
assertNever(operation)
40+
}
41+
}
42+
43+
export function getImportOperationTextAsJsx(operation: ImportOperation): React.ReactNode {
44+
const text = getImportOperationText(operation)
45+
const nodes = text.split('**').map((part, index) => {
46+
return index % 2 == 0 ? part : <strong key={part}>{part}</strong>
47+
})
48+
return <span>{nodes}</span>
49+
}

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

+24-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { unless, when } from '../../../utils/react-conditionals'
99
import {
1010
getTotalImportStatusAndResult,
1111
hideImportWizard,
12+
notifyImportStatusToDiscord,
1213
updateProjectImportStatus,
1314
} from '../../../core/shared/import/import-operation-service'
1415
import { OperationLine } from './components'
@@ -45,11 +46,6 @@ export const ImportWizard = React.memo(() => {
4546
e.stopPropagation()
4647
}, [])
4748

48-
const totalImportResult: TotalImportResult = React.useMemo(
49-
() => getTotalImportStatusAndResult(importState),
50-
[importState],
51-
)
52-
5349
if (projectId == null) {
5450
return null
5551
}
@@ -130,7 +126,7 @@ export const ImportWizard = React.memo(() => {
130126
gap: 10,
131127
}}
132128
>
133-
<ActionButtons importResult={totalImportResult} />
129+
<ActionButtons />
134130
</div>
135131
</div>,
136132
)}
@@ -139,7 +135,23 @@ export const ImportWizard = React.memo(() => {
139135
})
140136
ImportWizard.displayName = 'ImportWizard'
141137

142-
function ActionButtons({ importResult }: { importResult: TotalImportResult }) {
138+
function ActionButtons() {
139+
const importState = useEditorState(
140+
Substores.github,
141+
(store) => store.editor.importState,
142+
'ImportWizard importState',
143+
)
144+
145+
const projectName = useEditorState(
146+
Substores.restOfEditor,
147+
(store) => store.editor.projectName,
148+
'ImportWizard projectName',
149+
)
150+
151+
const importResult: TotalImportResult = React.useMemo(
152+
() => getTotalImportStatusAndResult(importState),
153+
[importState],
154+
)
143155
const colorTheme = useColorTheme()
144156
const dispatch = useDispatch()
145157
const result = importResult.result
@@ -174,6 +186,10 @@ function ActionButtons({ importResult }: { importResult: TotalImportResult }) {
174186
}
175187
}, [dispatch, hideWizard, importResult.importStatus])
176188
const importADifferentProject = React.useCallback(() => {
189+
if (importResult.importStatus.status !== 'done') {
190+
// force a notification to discord that the import was exited in the middle
191+
notifyImportStatusToDiscord(importState, projectName, true)
192+
}
177193
dispatch(
178194
[
179195
setImportWizardOpen(false),
@@ -182,7 +198,7 @@ function ActionButtons({ importResult }: { importResult: TotalImportResult }) {
182198
],
183199
'everyone',
184200
)
185-
}, [dispatch])
201+
}, [dispatch, importResult.importStatus.status, importState, projectName])
186202
const textStyle = {
187203
color: textColor,
188204
fontSize: 14,

editor/src/components/editor/server.ts

+17
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { assertNever } from '../../core/shared/utils'
4747
import { checkOnlineState } from './online-status'
4848
import type { GithubOperationContext } from '../../core/shared/github/operations/github-operation-context'
4949
import { GithubEndpoints } from '../../core/shared/github/endpoints'
50+
import type { DiscordEndpointPayload } from 'utopia-shared/src/types'
5051

5152
export { fetchProjectList, fetchShowcaseProjects, getLoginState } from '../../common/server'
5253

@@ -761,3 +762,19 @@ export function getBranchProjectContents(operationContext: GithubOperationContex
761762
return response.json()
762763
}
763764
}
765+
766+
export async function sendDiscordMessage(payload: DiscordEndpointPayload) {
767+
try {
768+
const response = await fetch(`/internal/discord/webhook`, {
769+
method: 'POST',
770+
credentials: 'include',
771+
mode: MODE,
772+
body: JSON.stringify(payload),
773+
})
774+
if (!response.ok) {
775+
console.error(`Send Discord message failed (${response.status}): ${response.statusText}`)
776+
}
777+
} catch (e) {
778+
console.error(`Send Discord message failed: ${e}`)
779+
}
780+
}

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import { OperationContext } from '../../core/shared/github/operations/github-ope
2828
import { notice } from '../common/notice'
2929
import { isFeatureEnabled } from '../../utils/feature-switches'
3030
import {
31+
notifyOperationCriticalError,
3132
notifyOperationFinished,
3233
startImportProcess,
34+
updateProjectImportStatus,
3335
} from '../../core/shared/import/import-operation-service'
3436
import { ImportOperationResult } from '../../core/shared/import/import-operation-types'
3537

@@ -131,11 +133,11 @@ async function cloneGithubRepo(
131133
if (repositoryEntry == null) {
132134
if (isFeatureEnabled('Import Wizard')) {
133135
startImportProcess(dispatch)
134-
notifyOperationFinished(
135-
dispatch,
136-
{ type: 'loadBranch', branchName: githubRepo.branch ?? undefined, githubRepo: githubRepo },
137-
ImportOperationResult.CriticalError,
138-
)
136+
notifyOperationCriticalError(dispatch, {
137+
type: 'loadBranch',
138+
branchName: githubRepo.branch ?? undefined,
139+
githubRepo: githubRepo,
140+
})
139141
} else {
140142
dispatch([showToast(notice('Cannot find repository', 'ERROR'))])
141143
}

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

+4-10
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ import { GithubOperations } from '.'
4444
import { assertNever } from '../../utils'
4545
import { updateProjectContentsWithParseResults } from '../../parser-projectcontents-utils'
4646
import {
47+
notifyOperationCriticalError,
4748
notifyOperationFinished,
4849
notifyOperationStarted,
4950
pauseImport,
5051
startImportProcess,
52+
updateProjectImportStatus,
5153
} from '../../import/import-operation-service'
5254
import {
5355
RequirementResolutionResult,
@@ -165,21 +167,13 @@ export const updateProjectWithBranchContent =
165167
switch (responseBody.type) {
166168
case 'FAILURE':
167169
if (isFeatureEnabled('Import Wizard')) {
168-
notifyOperationFinished(
169-
dispatch,
170-
{ type: 'loadBranch' },
171-
ImportOperationResult.CriticalError,
172-
)
170+
notifyOperationCriticalError(dispatch, { type: 'loadBranch' })
173171
}
174172
throw githubAPIError(operation, responseBody.failureReason)
175173
case 'SUCCESS':
176174
if (responseBody.branch == null) {
177175
if (isFeatureEnabled('Import Wizard')) {
178-
notifyOperationFinished(
179-
dispatch,
180-
{ type: 'loadBranch' },
181-
ImportOperationResult.CriticalError,
182-
)
176+
notifyOperationCriticalError(dispatch, { type: 'loadBranch' })
183177
}
184178
throw githubAPIError(operation, `Could not find branch ${branchName}`)
185179
}

0 commit comments

Comments
 (0)