From d7a5e42c9c7ea777a5c3d1a41202a3d4222754a3 Mon Sep 17 00:00:00 2001 From: shmck Date: Thu, 9 Apr 2020 20:47:23 -0700 Subject: [PATCH 1/9] checkRemoteConnects Signed-off-by: shmck --- src/channel/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index 55126faa..bdafaafe 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -8,7 +8,7 @@ import tutorialConfig from '../actions/tutorialConfig' import { COMMANDS } from '../editor/commands' import logger from '../services/logger' import Context from './context' -import { version as gitVersion } from '../services/git' +import { version as gitVersion, checkRemoteConnects } from '../services/git' import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' interface Channel { @@ -88,6 +88,12 @@ class Channel implements Channel { await tutorialConfig({ config: data.config }, onError) + try { + await checkRemoteConnects(data.config.repo) + } catch (error) { + this.send({ type: 'GIT_REMOTE_FAILED', payload: { message: error.message } }) + } + // report back to the webview that setup is complete this.send({ type: 'TUTORIAL_CONFIGURED' }) return From 98932e0b975848d4d74ab7f28793794365b46e36 Mon Sep 17 00:00:00 2001 From: shmck Date: Thu, 9 Apr 2020 21:00:47 -0700 Subject: [PATCH 2/9] add git remote check Signed-off-by: shmck --- src/services/git/index.ts | 19 ++++++++ typings/index.d.ts | 2 + web-app/src/Routes.tsx | 8 +++- .../src/containers/Check/GitRemoteFailed.tsx | 48 +++++++++++++++++++ web-app/src/services/state/machine.ts | 16 ++++++- 5 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 web-app/src/containers/Check/GitRemoteFailed.tsx diff --git a/src/services/git/index.ts b/src/services/git/index.ts index c35ee77d..2769e3c7 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,3 +1,4 @@ +import * as TT from 'typings/tutorial' import node from '../node' import logger from '../logger' import onError from '../sentry/onError' @@ -98,6 +99,24 @@ export async function initIfNotExists(): Promise { } } +export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { + // check for git repo + const externalRepoExists = await node.exec(`git ls-remote --exit-code --heads ${repo.uri}`) + if (externalRepoExists.stderr) { + // no repo found or no internet connection + throw new Error(externalRepoExists.stderr) + } + // check for git repo branch + const { stderr, stdout } = await node.exec(`git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}`) + if (stderr) { + throw new Error(stderr) + } + if (!stdout || !stdout.length) { + throw new Error('Tutorial branch does not exist') + } + return true +} + export async function addRemote(repo: string): Promise { const { stderr } = await node.exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`) if (stderr) { diff --git a/typings/index.d.ts b/typings/index.d.ts index 71d26e9f..cc1ed32d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -75,8 +75,10 @@ export interface MachineStateSchema { ValidateSetup: {} NonEmptyWorkspace: {} GitNotInstalled: {} + GitRemoteFailed: {} SelectTutorial: {} SetupNewTutorial: {} + StartNewTutorial: {} } } Tutorial: { diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 11ec399f..e33dc850 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -8,6 +8,7 @@ import CompletedPage from './containers/Tutorial/CompletedPage' import LevelSummaryPage from './containers/Tutorial/LevelPage' import SelectEmptyWorkspace from './containers/Check/SelectWorkspace' import GitInstalled from './containers/Check/GitInstalled' +import GitRemoteFailed from './containers/Check/GitRemoteFailed' const Routes = () => { const { context, send, Router, Route } = useRouter() @@ -15,7 +16,7 @@ const Routes = () => { {/* Setup */} - + @@ -36,9 +37,12 @@ const Routes = () => { - + + + + {/* Tutorial */} diff --git a/web-app/src/containers/Check/GitRemoteFailed.tsx b/web-app/src/containers/Check/GitRemoteFailed.tsx new file mode 100644 index 00000000..1e365d4e --- /dev/null +++ b/web-app/src/containers/Check/GitRemoteFailed.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import * as T from 'typings' +import { css, jsx } from '@emotion/core' +import Button from '../../components/Button' + +const styles = { + container: { + padding: '1rem', + }, +} + +type Props = { + error: T.ErrorMessage | null + send: (action: T.Action) => void +} + +const GitRemoteFailed = (props: Props) => { + const onTryAgain = () => props.send({ type: 'TRY_AGAIN' }) + return ( +
+

Git Remote Failed

+

Something went wrong when connecting to the Git repo.

+

+ There may be a problem with: (1) your internet, (2) your access to a private repo, or (3) the tutorial may not + have the correct repo name or branch. +

+ + {props.error && ( +
+

See the following error below for help:

+
+            
+              {props.error.title}
+              {props.error.description}
+            
+          
+
+ )} + +
+ +
+ ) +} + +export default GitRemoteFailed diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 4fe5c517..3c80f277 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -85,6 +85,11 @@ export const createMachine = (options: any) => { TRY_AGAIN: 'ValidateSetup', }, }, + GitRemoteFailed: { + on: { + TRY_AGAIN: 'SetupNewTutorial', + }, + }, SelectTutorial: { onEntry: ['clearStorage'], id: 'select-new-tutorial', @@ -96,9 +101,16 @@ export const createMachine = (options: any) => { }, }, SetupNewTutorial: { - onEntry: ['configureNewTutorial', 'startNewTutorial'], + onEntry: ['configureNewTutorial'], on: { - TUTORIAL_CONFIGURED: '#tutorial', + GIT_REMOTE_FAILED: 'GitRemoteFailed', + TUTORIAL_CONFIGURED: 'StartNewTutorial', + }, + }, + StartNewTutorial: { + onEntry: ['startNewTutorial'], + after: { + 0: '#tutorial', }, }, }, From 13292c887be01093be22d57dcf573e500b8f8be4 Mon Sep 17 00:00:00 2001 From: shmck Date: Thu, 9 Apr 2020 21:10:26 -0700 Subject: [PATCH 3/9] run remote check after init Signed-off-by: shmck --- src/actions/tutorialConfig.ts | 7 +++++++ src/channel/index.ts | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 5d01b1b5..6b68fb98 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -27,6 +27,13 @@ const tutorialConfig = async ( }) }) + try { + await git.checkRemoteConnects(config.repo) + } catch (error) { + onError(error) + handleError({ title: 'Error connecting to Git repo', description: error.message }) + } + // TODO if remote not already set await git.setupRemote(config.repo.uri).catch((error) => { onError(error) diff --git a/src/channel/index.ts b/src/channel/index.ts index bdafaafe..1783fd41 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -8,7 +8,7 @@ import tutorialConfig from '../actions/tutorialConfig' import { COMMANDS } from '../editor/commands' import logger from '../services/logger' import Context from './context' -import { version as gitVersion, checkRemoteConnects } from '../services/git' +import { version as gitVersion } from '../services/git' import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' interface Channel { @@ -86,12 +86,12 @@ class Channel implements Channel { // setup tutorial config (save watcher, test runner, etc) await this.context.setTutorial(this.workspaceState, data) - await tutorialConfig({ config: data.config }, onError) - try { - await checkRemoteConnects(data.config.repo) + // TODO: better handle errors + await tutorialConfig({ config: data.config }, onError) } catch (error) { - this.send({ type: 'GIT_REMOTE_FAILED', payload: { message: error.message } }) + // TODO send failure messages back to client + // to show errors in the webview } // report back to the webview that setup is complete From a71d857f588416419d20b8fda03fef3ea31cbe3e Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 09:55:47 -0700 Subject: [PATCH 4/9] respond with Tutorial configure error Signed-off-by: shmck --- src/channel/index.ts | 6 +++--- typings/index.d.ts | 1 - web-app/src/Routes.tsx | 3 --- web-app/src/services/state/machine.ts | 6 ++++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index 1783fd41..04f8931f 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -88,10 +88,10 @@ class Channel implements Channel { try { // TODO: better handle errors - await tutorialConfig({ config: data.config }, onError) + await tutorialConfig({ config: data.config }) } catch (error) { - // TODO send failure messages back to client - // to show errors in the webview + this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error: error.message } }) + return } // report back to the webview that setup is complete diff --git a/typings/index.d.ts b/typings/index.d.ts index cc1ed32d..dcddf6f2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -69,7 +69,6 @@ export interface MachineStateSchema { Setup: { states: { Startup: {} - Error: {} LoadStoredTutorial: {} Start: {} ValidateSetup: {} diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index e33dc850..0c4a9803 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -31,9 +31,6 @@ const Routes = () => { - - - diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 3c80f277..f5f84b00 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -42,7 +42,6 @@ export const createMachine = (options: any) => { }, }, }, - Error: {}, LoadStoredTutorial: { onEntry: ['loadStoredTutorial'], on: { @@ -103,7 +102,10 @@ export const createMachine = (options: any) => { SetupNewTutorial: { onEntry: ['configureNewTutorial'], on: { - GIT_REMOTE_FAILED: 'GitRemoteFailed', + TUTORIAL_CONFIGURE_FAIL: { + actions: ['setError'], + }, + TRY_AGAIN: 'SetupNewTutorial', TUTORIAL_CONFIGURED: 'StartNewTutorial', }, }, From 9fdb5dfa108c66bc92dca6c7098a06d4b3d8a2a1 Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 10:34:36 -0700 Subject: [PATCH 5/9] setup better tutorial config errors Signed-off-by: shmck --- src/actions/tutorialConfig.ts | 42 +++++++++---------- src/channel/index.ts | 14 ++++--- src/services/git/index.ts | 4 +- tsconfig.json | 3 +- typings/error.d.ts | 9 ++++ .../src/errors/FailedToConnectToGitRepo.md | 7 ++++ web-app/src/errors/GitNotFound.md | 3 ++ web-app/src/errors/GitProjectAlreadyExists.md | 5 +++ web-app/src/errors/UnknownError.md | 5 +++ web-app/tsconfig.paths.json | 3 +- 10 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 typings/error.d.ts create mode 100644 web-app/src/errors/FailedToConnectToGitRepo.md create mode 100644 web-app/src/errors/GitNotFound.md create mode 100644 web-app/src/errors/GitProjectAlreadyExists.md create mode 100644 web-app/src/errors/UnknownError.md diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 6b68fb98..4e0fa019 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -1,9 +1,8 @@ -import * as T from 'typings' +import * as E from 'typings/error' import * as TT from 'typings/tutorial' import * as vscode from 'vscode' import { COMMANDS } from '../editor/commands' import * as git from '../services/git' -import onError from '../services/sentry/onError' interface TutorialConfigParams { config: TT.TutorialConfig @@ -11,33 +10,30 @@ interface TutorialConfigParams { onComplete?(): void } -const tutorialConfig = async ( - { config, alreadyConfigured }: TutorialConfigParams, - handleError: (msg: T.ErrorMessage) => void, -) => { +const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParams): Promise => { if (!alreadyConfigured) { // setup git, add remote - await git.initIfNotExists().catch((error) => { - onError(new Error('Git not found')) - // failed to setup git - handleError({ - title: error.message, - description: - 'Make sure you install Git. See the docs for help https://git-scm.com/book/en/v2/Getting-Started-Installing-Git', - }) + await git.initIfNotExists().catch((error: Error) => { + return { + type: 'GitNotFound', + message: error.message, + } }) - try { - await git.checkRemoteConnects(config.repo) - } catch (error) { - onError(error) - handleError({ title: 'Error connecting to Git repo', description: error.message }) - } + // verify that internet is connected, remote exists and branch exists + await git.checkRemoteConnects(config.repo).catch((error: Error) => { + return { + type: 'FailedToConnectToGitRepo', + message: error.message, + } + }) // TODO if remote not already set - await git.setupRemote(config.repo.uri).catch((error) => { - onError(error) - handleError({ title: error.message, description: 'Remove your current Git project and reload the editor' }) + await git.setupCodeRoadRemote(config.repo.uri).catch((error: Error) => { + return { + type: 'GitRemoteAlreadyExists', + message: error.message, + } }) } diff --git a/src/channel/index.ts b/src/channel/index.ts index 04f8931f..3ad394e1 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -1,5 +1,6 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' +import * as E from 'typings/error' import * as vscode from 'vscode' import saveCommit from '../actions/saveCommit' import setupActions from '../actions/setupActions' @@ -86,11 +87,14 @@ class Channel implements Channel { // setup tutorial config (save watcher, test runner, etc) await this.context.setTutorial(this.workspaceState, data) - try { - // TODO: better handle errors - await tutorialConfig({ config: data.config }) - } catch (error) { - this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error: error.message } }) + const error: E.ErrorMessage | void = await tutorialConfig({ config: data.config }).catch((error: Error) => ({ + type: 'UnknownError', + message: `Location: tutorial config.\n\n${error.message}`, + })) + + // has error + if (error && error.type) { + this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } }) return } diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 2769e3c7..32c6094b 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -145,7 +145,7 @@ export async function checkRemoteExists(): Promise { } } -export async function setupRemote(repo: string): Promise { +export async function setupCodeRoadRemote(repo: string): Promise { // check coderoad remote not taken const hasRemote = await checkRemoteExists() // git remote add coderoad tutorial @@ -153,6 +153,6 @@ export async function setupRemote(repo: string): Promise { if (!hasRemote) { await addRemote(repo) } else { - throw new Error('A Remote is already configured') + throw new Error('A CodeRoad remote is already configured') } } diff --git a/tsconfig.json b/tsconfig.json index 9c8ae641..2e5271a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "emitDecoratorMetadata": true, "paths": { "typings": ["../typings/index.d.ts"], - "typings/tutorial": ["../typings/tutorial.d.ts"] + "typings/tutorial": ["../typings/tutorial.d.ts"], + "typings/error": ["../typings/error.d.ts"] }, "allowJs": true, "removeComments": true diff --git a/typings/error.d.ts b/typings/error.d.ts new file mode 100644 index 00000000..b1bbf530 --- /dev/null +++ b/typings/error.d.ts @@ -0,0 +1,9 @@ +export type ErrorMessageView = 'FULL_PAGE' | 'NOTIFY' | 'NONE' + +export type ErrorMessageType = 'UnknownError' | 'GitNotFound' | 'FailedToConnectToGitRepo' | 'GitProjectAlreadyExists' + +export type ErrorMessage = { + type: ErrorMessageType + message: string + display?: ErrorMessageView +} diff --git a/web-app/src/errors/FailedToConnectToGitRepo.md b/web-app/src/errors/FailedToConnectToGitRepo.md new file mode 100644 index 00000000..fef26ef2 --- /dev/null +++ b/web-app/src/errors/FailedToConnectToGitRepo.md @@ -0,0 +1,7 @@ +### Failed to Connect to Git Repo + +There are several possible causes: + +- you may not be connected to the internet or have an unstable connection. +- you may not have access permission to the remote tutorial repo. +- the remote tutorial repo may not exist at the provided location diff --git a/web-app/src/errors/GitNotFound.md b/web-app/src/errors/GitNotFound.md new file mode 100644 index 00000000..a0b24e52 --- /dev/null +++ b/web-app/src/errors/GitNotFound.md @@ -0,0 +1,3 @@ +### Git Not Found + +Make sure you install Git. See the [Git docs](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) for help. diff --git a/web-app/src/errors/GitProjectAlreadyExists.md b/web-app/src/errors/GitProjectAlreadyExists.md new file mode 100644 index 00000000..f2e31560 --- /dev/null +++ b/web-app/src/errors/GitProjectAlreadyExists.md @@ -0,0 +1,5 @@ +### Git Project Already Exists + +CodeRoad requires an empty Git project. + +Open a new workspace to start a tutorial. diff --git a/web-app/src/errors/UnknownError.md b/web-app/src/errors/UnknownError.md new file mode 100644 index 00000000..d64fe8d4 --- /dev/null +++ b/web-app/src/errors/UnknownError.md @@ -0,0 +1,5 @@ +### Unknown Error + +Sorry! An unknown error occurred. + +Please help out by posting an issue at github.com/coderoad/coderoad-vscode/issues/new/choose! diff --git a/web-app/tsconfig.paths.json b/web-app/tsconfig.paths.json index 3adaa5e2..c54e0407 100644 --- a/web-app/tsconfig.paths.json +++ b/web-app/tsconfig.paths.json @@ -2,7 +2,8 @@ "compilerOptions": { "paths": { "typings": ["../../typings/index.d.ts"], - "typings/tutorial": ["../../typings/tutorial.d.ts"] + "typings/tutorial": ["../../typings/tutorial.d.ts"], + "typings/error": ["../../typings/error.d.ts"] }, "allowSyntheticDefaultImports": true }, From 345770f169fd3bd8e57f6acfa7f7a6c85b31030f Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 12:23:15 -0700 Subject: [PATCH 6/9] load error markdown in middleware Signed-off-by: shmck --- .../FailedToConnectToGitRepo.md | 0 {web-app/src/errors => errors}/GitNotFound.md | 0 .../GitProjectAlreadyExists.md | 0 .../src/errors => errors}/UnknownError.md | 0 src/actions/tutorialConfig.ts | 36 ++++++++++------ src/channel/index.ts | 42 ++++++++++++++----- src/services/git/index.ts | 31 ++++++-------- src/services/workspace/index.ts | 5 ++- typings/error.d.ts | 7 +++- web-app/src/components/Error/index.tsx | 16 +++---- 10 files changed, 87 insertions(+), 50 deletions(-) rename {web-app/src/errors => errors}/FailedToConnectToGitRepo.md (100%) rename {web-app/src/errors => errors}/GitNotFound.md (100%) rename {web-app/src/errors => errors}/GitProjectAlreadyExists.md (100%) rename {web-app/src/errors => errors}/UnknownError.md (100%) diff --git a/web-app/src/errors/FailedToConnectToGitRepo.md b/errors/FailedToConnectToGitRepo.md similarity index 100% rename from web-app/src/errors/FailedToConnectToGitRepo.md rename to errors/FailedToConnectToGitRepo.md diff --git a/web-app/src/errors/GitNotFound.md b/errors/GitNotFound.md similarity index 100% rename from web-app/src/errors/GitNotFound.md rename to errors/GitNotFound.md diff --git a/web-app/src/errors/GitProjectAlreadyExists.md b/errors/GitProjectAlreadyExists.md similarity index 100% rename from web-app/src/errors/GitProjectAlreadyExists.md rename to errors/GitProjectAlreadyExists.md diff --git a/web-app/src/errors/UnknownError.md b/errors/UnknownError.md similarity index 100% rename from web-app/src/errors/UnknownError.md rename to errors/UnknownError.md diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 4e0fa019..746e2605 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -13,28 +13,40 @@ interface TutorialConfigParams { const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParams): Promise => { if (!alreadyConfigured) { // setup git, add remote - await git.initIfNotExists().catch((error: Error) => { - return { + const initError: E.ErrorMessage | void = await git.initIfNotExists().catch( + (error: Error): E.ErrorMessage => ({ type: 'GitNotFound', message: error.message, - } - }) + }), + ) + + if (initError) { + return initError + } // verify that internet is connected, remote exists and branch exists - await git.checkRemoteConnects(config.repo).catch((error: Error) => { - return { + const remoteConnectError: E.ErrorMessage | void = await git.checkRemoteConnects(config.repo).catch( + (error: Error): E.ErrorMessage => ({ type: 'FailedToConnectToGitRepo', message: error.message, - } - }) + }), + ) + + if (remoteConnectError) { + return remoteConnectError + } // TODO if remote not already set - await git.setupCodeRoadRemote(config.repo.uri).catch((error: Error) => { - return { + const coderoadRemoteError: E.ErrorMessage | void = await git.setupCodeRoadRemote(config.repo.uri).catch( + (error: Error): E.ErrorMessage => ({ type: 'GitRemoteAlreadyExists', message: error.message, - } - }) + }), + ) + + if (coderoadRemoteError) { + return coderoadRemoteError + } } await vscode.commands.executeCommand(COMMANDS.CONFIG_TEST_RUNNER, config.testRunner) diff --git a/src/channel/index.ts b/src/channel/index.ts index 3ad394e1..a8caedba 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -11,6 +11,11 @@ import logger from '../services/logger' import Context from './context' import { version as gitVersion } from '../services/git' import { openWorkspace, checkWorkspaceEmpty } from '../services/workspace' +import { readFile } from 'fs' +import { join } from 'path' +import { promisify } from 'util' + +const readFileAsync = promisify(readFile) interface Channel { receive(action: T.Action): Promise @@ -40,7 +45,9 @@ class Channel implements Channel { public receive = async (action: T.Action) => { // action may be an object.type or plain string const actionType: string = typeof action === 'string' ? action : action.type - const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } }) + // const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } }) + + // console.log(`ACTION: ${actionType}`) switch (actionType) { case 'EDITOR_ENV_GET': @@ -107,13 +114,10 @@ class Channel implements Channel { throw new Error('Invalid tutorial to continue') } const continueConfig: TT.TutorialConfig = tutorialContinue.config - await tutorialConfig( - { - config: continueConfig, - alreadyConfigured: true, - }, - onError, - ) + await tutorialConfig({ + config: continueConfig, + alreadyConfigured: true, + }) // update the current stepId on startup vscode.commands.executeCommand(COMMANDS.SET_CURRENT_STEP, action.payload) return @@ -156,6 +160,23 @@ class Channel implements Channel { } // send to webview public send = async (action: T.Action) => { + // Error middleware + if (action?.payload?.error?.type) { + // load error markdown message + const error = action.payload.error + const errorMarkdownFile = join(__dirname, '..', '..', 'errors', `${action.payload.error.type}.md`) + const errorMarkdown = await readFileAsync(errorMarkdownFile).catch(() => { + // onError(new Error(`Error Markdown file not found for ${action.type}`)) + }) + + console.log(`ERROR:\n ${errorMarkdown}`) + + if (errorMarkdown) { + // add a clearer error message for the user + error.message = `${errorMarkdown}\n${error.message}` + } + } + // action may be an object.type or plain string const actionType: string = typeof action === 'string' ? action : action.type switch (actionType) { @@ -170,8 +191,9 @@ class Channel implements Channel { saveCommit() } - const success = await this.postMessage(action) - if (!success) { + // send message + const sentToClient = await this.postMessage(action) + if (!sentToClient) { throw new Error(`Message post failure: ${JSON.stringify(action)}`) } } diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 32c6094b..d66bd4e5 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,11 +1,10 @@ import * as TT from 'typings/tutorial' import node from '../node' import logger from '../logger' -import onError from '../sentry/onError' const gitOrigin = 'coderoad' -const stashAllFiles = async () => { +const stashAllFiles = async (): Promise => { // stash files including untracked (eg. newly created file) const { stdout, stderr } = await node.exec(`git stash --include-untracked`) if (stderr) { @@ -14,7 +13,7 @@ const stashAllFiles = async () => { } } -const cherryPickCommit = async (commit: string, count = 0): Promise => { +const cherryPickCommit = async (commit: string, count = 0): Promise => { if (count > 1) { console.warn('cherry-pick failed') return @@ -38,7 +37,7 @@ const cherryPickCommit = async (commit: string, count = 0): Promise => { SINGLE git cherry-pick %COMMIT% if fails, will stash all and retry */ -export function loadCommit(commit: string): Promise { +export function loadCommit(commit: string): Promise { return cherryPickCommit(commit) } @@ -47,7 +46,7 @@ export function loadCommit(commit: string): Promise { git commit -am '${level}/${step} complete' */ -export async function saveCommit(message: string): Promise { +export async function saveCommit(message: string): Promise { const { stdout, stderr } = await node.exec(`git commit -am '${message}'`) if (stderr) { console.error(stderr) @@ -56,7 +55,7 @@ export async function saveCommit(message: string): Promise { logger(['save with commit & continue stdout', stdout]) } -export async function clear(): Promise { +export async function clear(): Promise { try { // commit progress to git const { stderr } = await node.exec('git reset HEAD --hard && git clean -fd') @@ -83,23 +82,21 @@ export async function version(): Promise { return null } -async function init(): Promise { +async function init(): Promise { const { stderr } = await node.exec('git init') if (stderr) { - const error = new Error('Error initializing Git') - onError(error) - throw error + throw new Error('Error initializing Git') } } -export async function initIfNotExists(): Promise { +export async function initIfNotExists(): Promise { const hasGitInit = node.exists('.git') if (!hasGitInit) { await init() } } -export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { +export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { // check for git repo const externalRepoExists = await node.exec(`git ls-remote --exit-code --heads ${repo.uri}`) if (externalRepoExists.stderr) { @@ -114,10 +111,9 @@ export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise { +export async function addRemote(repo: string): Promise { const { stderr } = await node.exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`) if (stderr) { const alreadyExists = stderr.match(`${gitOrigin} already exists.`) @@ -145,14 +141,13 @@ export async function checkRemoteExists(): Promise { } } -export async function setupCodeRoadRemote(repo: string): Promise { +export async function setupCodeRoadRemote(repo: string): Promise { // check coderoad remote not taken const hasRemote = await checkRemoteExists() // git remote add coderoad tutorial // git fetch coderoad - if (!hasRemote) { - await addRemote(repo) - } else { + if (hasRemote) { throw new Error('A CodeRoad remote is already configured') } + await addRemote(repo) } diff --git a/src/services/workspace/index.ts b/src/services/workspace/index.ts index 0a6f9e9e..b62b17a9 100644 --- a/src/services/workspace/index.ts +++ b/src/services/workspace/index.ts @@ -1,5 +1,8 @@ import * as vscode from 'vscode' import * as fs from 'fs' +import { promisify } from 'util' + +const readDir = promisify(fs.readdir) export const openWorkspace = () => { const openInNewWindow = false @@ -9,7 +12,7 @@ export const openWorkspace = () => { export const checkWorkspaceEmpty = async (dirname: string) => { let files try { - files = await fs.promises.readdir(dirname) + files = await readDir(dirname) } catch (error) { throw new Error('Failed to check workspace') } diff --git a/typings/error.d.ts b/typings/error.d.ts index b1bbf530..10a3ceca 100644 --- a/typings/error.d.ts +++ b/typings/error.d.ts @@ -1,6 +1,11 @@ export type ErrorMessageView = 'FULL_PAGE' | 'NOTIFY' | 'NONE' -export type ErrorMessageType = 'UnknownError' | 'GitNotFound' | 'FailedToConnectToGitRepo' | 'GitProjectAlreadyExists' +export type ErrorMessageType = + | 'UnknownError' + | 'GitNotFound' + | 'FailedToConnectToGitRepo' + | 'GitProjectAlreadyExists' + | 'GitRemoteAlreadyExists' export type ErrorMessage = { type: ErrorMessageType diff --git a/web-app/src/components/Error/index.tsx b/web-app/src/components/Error/index.tsx index 2bb26eef..606896a3 100644 --- a/web-app/src/components/Error/index.tsx +++ b/web-app/src/components/Error/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' +import * as E from 'typings/error' import { css, jsx } from '@emotion/core' -import onError from '../../services/sentry/onError' +import Markdown from '../Markdown' const styles = { container: { @@ -13,17 +14,16 @@ const styles = { } interface Props { - error?: Error + error?: E.ErrorMessage } -const ErrorView = ({ error }: Props) => { - // log error +const ErrorMarkdown = ({ error }: Props) => { React.useEffect(() => { if (error) { + // log error console.log(error) - onError(error) } - }, []) + }, [error]) if (!error) { return null @@ -32,9 +32,9 @@ const ErrorView = ({ error }: Props) => { return (

Error

-
{JSON.stringify(error)}
+ {error.message}
) } -export default ErrorView +export default ErrorMarkdown From 2b734845aaebf8e5ef7a5b34c4cc6391c340863c Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 20:56:16 -0700 Subject: [PATCH 7/9] standardize error messages Signed-off-by: shmck --- errors/WorkspaceNotEmpty.md | 5 ++ src/actions/tutorialConfig.ts | 2 + src/channel/index.ts | 16 ++++++- typings/error.d.ts | 7 +++ typings/index.d.ts | 11 +---- web-app/src/Routes.tsx | 31 ++++++------ web-app/src/components/Error/index.tsx | 14 +++++- web-app/src/containers/Check/GitInstalled.tsx | 36 -------------- .../src/containers/Check/GitRemoteFailed.tsx | 48 ------------------- .../src/containers/Check/SelectWorkspace.tsx | 31 ------------ .../src/containers/Loading/LoadingPage.tsx | 11 +---- web-app/src/containers/Loading/index.tsx | 10 +--- web-app/src/services/state/machine.ts | 24 ++-------- 13 files changed, 65 insertions(+), 181 deletions(-) create mode 100644 errors/WorkspaceNotEmpty.md delete mode 100644 web-app/src/containers/Check/GitInstalled.tsx delete mode 100644 web-app/src/containers/Check/GitRemoteFailed.tsx delete mode 100644 web-app/src/containers/Check/SelectWorkspace.tsx diff --git a/errors/WorkspaceNotEmpty.md b/errors/WorkspaceNotEmpty.md new file mode 100644 index 00000000..f6d7f2f5 --- /dev/null +++ b/errors/WorkspaceNotEmpty.md @@ -0,0 +1,5 @@ +### Select An Empty VSCode Workspace + +Start a project in an empty folder. + +Once selected, the extension will close and need to be re-started. diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 746e2605..9785c6ae 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -17,6 +17,7 @@ const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParam (error: Error): E.ErrorMessage => ({ type: 'GitNotFound', message: error.message, + actions: [{ label: 'Retry', transition: '' }], }), ) @@ -29,6 +30,7 @@ const tutorialConfig = async ({ config, alreadyConfigured }: TutorialConfigParam (error: Error): E.ErrorMessage => ({ type: 'FailedToConnectToGitRepo', message: error.message, + actions: [{ label: 'Retry', transition: '' }], }), ) diff --git a/src/channel/index.ts b/src/channel/index.ts index a8caedba..7035db07 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -125,7 +125,21 @@ class Channel implements Channel { // 1. check workspace is selected const isEmptyWorkspace = await checkWorkspaceEmpty(this.workspaceRoot.uri.path) if (!isEmptyWorkspace) { - this.send({ type: 'NOT_EMPTY_WORKSPACE' }) + const error: E.ErrorMessage = { + type: 'WorkspaceNotEmpty', + message: '', + actions: [ + { + label: 'Open Workspace', + transition: 'REQUEST_WORKSPACE', + }, + { + label: 'Check Again', + transition: 'RETRY', + }, + ], + } + this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) return } // 2. check Git is installed. diff --git a/typings/error.d.ts b/typings/error.d.ts index 10a3ceca..79abc3c6 100644 --- a/typings/error.d.ts +++ b/typings/error.d.ts @@ -6,9 +6,16 @@ export type ErrorMessageType = | 'FailedToConnectToGitRepo' | 'GitProjectAlreadyExists' | 'GitRemoteAlreadyExists' + | 'WorkspaceNotEmpty' + +export type ErrorAction = { + label: string + transition: string +} export type ErrorMessage = { type: ErrorMessageType message: string display?: ErrorMessageView + actions?: ErrorAction[] } diff --git a/typings/index.d.ts b/typings/index.d.ts index dcddf6f2..2b8c8d22 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,4 @@ +import * as E from './error' import * as TT from './tutorial' export type ProgressStatus = 'ACTIVE' | 'COMPLETE' | 'INCOMPLETE' @@ -37,11 +38,6 @@ export interface Environment { token: string } -export interface ErrorMessage { - title: string - description?: string -} - export interface TestStatus { type: 'success' | 'warning' | 'error' | 'loading' title: string @@ -50,7 +46,7 @@ export interface TestStatus { export interface MachineContext { env: Environment - error: ErrorMessage | null + error: E.ErrorMessage | null tutorial: TT.Tutorial | null position: Position progress: Progress @@ -72,9 +68,6 @@ export interface MachineStateSchema { LoadStoredTutorial: {} Start: {} ValidateSetup: {} - NonEmptyWorkspace: {} - GitNotInstalled: {} - GitRemoteFailed: {} SelectTutorial: {} SetupNewTutorial: {} StartNewTutorial: {} diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 0c4a9803..4754bf62 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -1,48 +1,47 @@ import * as React from 'react' import useRouter from './components/Router' import Workspace from './components/Workspace' +import ErrorView from './components/Error' import LoadingPage from './containers/Loading' import StartPage from './containers/Start' import SelectTutorialPage from './containers/SelectTutorial' import CompletedPage from './containers/Tutorial/CompletedPage' import LevelSummaryPage from './containers/Tutorial/LevelPage' -import SelectEmptyWorkspace from './containers/Check/SelectWorkspace' -import GitInstalled from './containers/Check/GitInstalled' -import GitRemoteFailed from './containers/Check/GitRemoteFailed' const Routes = () => { const { context, send, Router, Route } = useRouter() + + // TODO: handle only full page errors + if (context.error) { + return ( + + + + ) + } + return ( {/* Setup */} - + - - - - - - - + - - - - + {/* Tutorial */} - + diff --git a/web-app/src/components/Error/index.tsx b/web-app/src/components/Error/index.tsx index 606896a3..8da521fb 100644 --- a/web-app/src/components/Error/index.tsx +++ b/web-app/src/components/Error/index.tsx @@ -1,7 +1,9 @@ import * as React from 'react' import * as E from 'typings/error' +import * as T from 'typings' import { css, jsx } from '@emotion/core' import Markdown from '../Markdown' +import Button from '../../components/Button' const styles = { container: { @@ -14,10 +16,11 @@ const styles = { } interface Props { - error?: E.ErrorMessage + error: E.ErrorMessage + send: (action: T.Action) => void } -const ErrorMarkdown = ({ error }: Props) => { +const ErrorMarkdown = ({ error, send }: Props) => { React.useEffect(() => { if (error) { // log error @@ -33,6 +36,13 @@ const ErrorMarkdown = ({ error }: Props) => {

Error

{error.message} + {/* Actions */} + {error.actions && + error.actions.map((a) => ( + + ))}
) } diff --git a/web-app/src/containers/Check/GitInstalled.tsx b/web-app/src/containers/Check/GitInstalled.tsx deleted file mode 100644 index 0904dec7..00000000 --- a/web-app/src/containers/Check/GitInstalled.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react' -import * as T from 'typings' -import { css, jsx } from '@emotion/core' -import Button from '../../components/Button' - -const styles = { - container: { - padding: '1rem', - }, -} - -type Props = { - send: (action: T.Action) => void -} - -const GitInstalled = (props: Props) => { - const onTryAgain = () => props.send({ type: 'TRY_AGAIN' }) - return ( -
-

Git Not Installed

-

- Git is required for CodeRun to run. Git is a free open-source distributed version control system. Basically, Git - helps you easily save your file system changes. -

-

- Learn how to install Git -

-
- -
- ) -} - -export default GitInstalled diff --git a/web-app/src/containers/Check/GitRemoteFailed.tsx b/web-app/src/containers/Check/GitRemoteFailed.tsx deleted file mode 100644 index 1e365d4e..00000000 --- a/web-app/src/containers/Check/GitRemoteFailed.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react' -import * as T from 'typings' -import { css, jsx } from '@emotion/core' -import Button from '../../components/Button' - -const styles = { - container: { - padding: '1rem', - }, -} - -type Props = { - error: T.ErrorMessage | null - send: (action: T.Action) => void -} - -const GitRemoteFailed = (props: Props) => { - const onTryAgain = () => props.send({ type: 'TRY_AGAIN' }) - return ( -
-

Git Remote Failed

-

Something went wrong when connecting to the Git repo.

-

- There may be a problem with: (1) your internet, (2) your access to a private repo, or (3) the tutorial may not - have the correct repo name or branch. -

- - {props.error && ( -
-

See the following error below for help:

-
-            
-              {props.error.title}
-              {props.error.description}
-            
-          
-
- )} - -
- -
- ) -} - -export default GitRemoteFailed diff --git a/web-app/src/containers/Check/SelectWorkspace.tsx b/web-app/src/containers/Check/SelectWorkspace.tsx deleted file mode 100644 index 613b1659..00000000 --- a/web-app/src/containers/Check/SelectWorkspace.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react' -import * as T from 'typings' -import { css, jsx } from '@emotion/core' -import Button from '../../components/Button' - -const styles = { - container: { - padding: '1rem', - }, -} - -type Props = { - send: (action: T.Action) => void -} - -const SelectWorkspace = (props: Props) => { - const onOpenWorkspace = () => props.send({ type: 'REQUEST_WORKSPACE' }) - return ( -
-

Select An Empty VSCode Workspace

-

Start a project in an empty folder.

-

Once selected, the extension will close and need to be re-started.

-
- -
- ) -} - -export default SelectWorkspace diff --git a/web-app/src/containers/Loading/LoadingPage.tsx b/web-app/src/containers/Loading/LoadingPage.tsx index 8452bbba..b9a15228 100644 --- a/web-app/src/containers/Loading/LoadingPage.tsx +++ b/web-app/src/containers/Loading/LoadingPage.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import * as T from 'typings' import { css, jsx } from '@emotion/core' import Loading from '../../components/Loading' -import Message from '../../components/Message' interface Props { text: string @@ -20,15 +19,7 @@ const styles = { }, } -const LoadingPage = ({ text, context }: Props) => { - const { error } = context - if (error) { - return ( -
- -
- ) - } +const LoadingPage = ({ text }: Props) => { return (
diff --git a/web-app/src/containers/Loading/index.tsx b/web-app/src/containers/Loading/index.tsx index a57079bd..b539bd47 100644 --- a/web-app/src/containers/Loading/index.tsx +++ b/web-app/src/containers/Loading/index.tsx @@ -20,7 +20,7 @@ const styles = { }, } -const LoadingPage = ({ text, context }: Props) => { +const LoadingPage = ({ text }: Props) => { const [showLoading, setShowHiding] = React.useState(false) React.useEffect(() => { @@ -33,14 +33,6 @@ const LoadingPage = ({ text, context }: Props) => { } }, []) - if (context && context.error) { - return ( -
- -
- ) - } - // don't flash loader if (!showLoading) { return null diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index f5f84b00..5caaca1e 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -64,29 +64,15 @@ export const createMachine = (options: any) => { ValidateSetup: { onEntry: ['validateSetup'], on: { - NOT_EMPTY_WORKSPACE: 'NonEmptyWorkspace', - GIT_NOT_INSTALLED: 'GitNotInstalled', - SETUP_VALIDATED: 'SelectTutorial', - }, - }, - NonEmptyWorkspace: { - on: { + VALIDATE_SETUP_FAILED: { + actions: ['setError'], + }, + RETRY: 'ValidateSetup', REQUEST_WORKSPACE: { - target: 'NonEmptyWorkspace', actions: 'requestWorkspaceSelect', }, WORKSPACE_LOADED: 'ValidateSetup', - }, - }, - // validation 2: git installed - GitNotInstalled: { - on: { - TRY_AGAIN: 'ValidateSetup', - }, - }, - GitRemoteFailed: { - on: { - TRY_AGAIN: 'SetupNewTutorial', + SETUP_VALIDATED: 'SelectTutorial', }, }, SelectTutorial: { From 99b86048bc2a49be7859349a080fae14b50d0fef Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 21:02:26 -0700 Subject: [PATCH 8/9] clear errors on success Signed-off-by: shmck --- web-app/src/services/state/actions/context.ts | 4 ++++ web-app/src/services/state/machine.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts index e60d8881..8d9f08e9 100644 --- a/web-app/src/services/state/actions/context.ts +++ b/web-app/src/services/state/actions/context.ts @@ -220,6 +220,10 @@ const contextActions: ActionFunctionMap = { }, }), // @ts-ignore + clearError: assign({ + error: (): any => null, + }), + // @ts-ignore checkEmptySteps: send((context: T.MachineContext) => { // no step id indicates no steps to complete return { diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 5caaca1e..25d89669 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -63,6 +63,7 @@ export const createMachine = (options: any) => { }, ValidateSetup: { onEntry: ['validateSetup'], + onExit: ['clearError'], on: { VALIDATE_SETUP_FAILED: { actions: ['setError'], @@ -87,6 +88,7 @@ export const createMachine = (options: any) => { }, SetupNewTutorial: { onEntry: ['configureNewTutorial'], + onExit: ['clearError'], on: { TUTORIAL_CONFIGURE_FAIL: { actions: ['setError'], @@ -118,6 +120,7 @@ export const createMachine = (options: any) => { actions: ['commandFail'], }, ERROR: { + // TODO: missing clearError actions: ['setError'], }, }, From 3bbf2be42b129b83d3ec6f6a8a7399b7a65f6660 Mon Sep 17 00:00:00 2001 From: shmck Date: Fri, 10 Apr 2020 21:06:36 -0700 Subject: [PATCH 9/9] fix git not installed error Signed-off-by: shmck --- src/channel/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index 7035db07..3781f71c 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -146,13 +146,22 @@ class Channel implements Channel { // Should wait for workspace before running otherwise requires access to root folder const isGitInstalled = await gitVersion() if (!isGitInstalled) { - this.send({ type: 'GIT_NOT_INSTALLED' }) + const error: E.ErrorMessage = { + type: 'GitNotFound', + message: '', + actions: [ + { + label: 'Check Again', + transition: 'RETRY', + }, + ], + } + this.send({ type: 'VALIDATE_SETUP_FAILED', payload: { error } }) return } this.send({ type: 'SETUP_VALIDATED' }) return case 'EDITOR_REQUEST_WORKSPACE': - console.log('request workspace') openWorkspace() return // load step actions (git commits, commands, open files) @@ -183,6 +192,7 @@ class Channel implements Channel { // onError(new Error(`Error Markdown file not found for ${action.type}`)) }) + // log error to console for safe keeping console.log(`ERROR:\n ${errorMarkdown}`) if (errorMarkdown) {