diff --git a/src/channel/index.ts b/src/channel/index.ts
index 8284b269..7ef5692f 100644
--- a/src/channel/index.ts
+++ b/src/channel/index.ts
@@ -40,7 +40,7 @@ class Channel implements Channel {
const onError = (error: T.ErrorMessage) => this.send({ type: 'ERROR', payload: { error } })
switch (actionType) {
- case 'ENV_GET':
+ case 'EDITOR_ENV_GET':
this.send({
type: 'ENV_LOAD',
payload: {
diff --git a/typings/index.d.ts b/typings/index.d.ts
index bc07807c..24864c41 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -63,6 +63,7 @@ export interface MachineStateSchema {
states: {
Startup: {}
Authenticate: {}
+ Error: {}
NewOrContinue: {}
SelectTutorial: {}
ContinueTutorial: {}
@@ -73,6 +74,7 @@ export interface MachineStateSchema {
Initialize: {}
Summary: {}
LoadNext: {}
+ Error: {}
Level: {
states: {
Load: {}
diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx
index ed89f5b6..e30c0b8d 100644
--- a/web-app/src/Routes.tsx
+++ b/web-app/src/Routes.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as CR from 'typings'
-import Router from './components/Router'
+import useRouter from './components/Router'
import Workspace from './components/Workspace'
import ContinuePage from './containers/Continue'
import LoadingPage from './containers/LoadingPage'
@@ -9,38 +9,41 @@ import OverviewPage from './containers/Overview'
import CompletedPage from './containers/Tutorial/CompletedPage'
import LevelSummaryPage from './containers/Tutorial/LevelPage'
-const { Route } = Router
-
-const tempSend = (action: any) => console.log('sent')
-
const Routes = () => {
+ const { context, send, Router, Route } = useRouter()
// TODO refactor for typescript to understand send & context passed into React.cloneElement's
return (
-
+
+
+
+ Something went wrong wrong
-
+
-
+
+
+
+ Something went wrong wrong
-
+
-
+
-
+
-
+
-
+
diff --git a/web-app/src/components/Error/index.tsx b/web-app/src/components/Error/index.tsx
index 541e2b27..42b9c029 100644
--- a/web-app/src/components/Error/index.tsx
+++ b/web-app/src/components/Error/index.tsx
@@ -9,20 +9,28 @@ const styles = {
color: '#D8000C',
backgroundColor: '#FFBABA',
padding: '1rem',
+ width: '100%',
+ height: '100%',
},
}
interface Props {
- error: ApolloError
+ error?: ApolloError
}
const ErrorView = ({ error }: Props) => {
// log error
React.useEffect(() => {
- console.log(error)
- onError(error)
+ if (error) {
+ console.log(error)
+ onError(error)
+ }
}, [])
+ if (!error) {
+ return null
+ }
+
return (
Error
diff --git a/web-app/src/components/Router/index.tsx b/web-app/src/components/Router/index.tsx
index 3d7023d9..0e06bb0e 100644
--- a/web-app/src/components/Router/index.tsx
+++ b/web-app/src/components/Router/index.tsx
@@ -1,10 +1,6 @@
import * as React from 'react'
-import * as CR from 'typings'
-import channel from '../../services/channel'
-import messageBusReceiver from '../../services/channel/receiver'
-import machine from '../../services/state/machine'
+import { createMachine } from '../../services/state/machine'
import { useMachine } from '../../services/xstate-react'
-import debuggerWrapper from '../Debugger/debuggerWrapper'
import Route from './Route'
import onError from '../../services/sentry/onError'
@@ -12,46 +8,63 @@ interface Props {
children: any
}
-interface CloneElementProps {
- context: CR.MachineContext
- send(action: CR.Action): void
-}
+declare let acquireVsCodeApi: any
-// router finds first state match of
-const Router = ({ children }: Props): React.ReactElement
| null => {
- const [state, send] = useMachine(machine, {
- interpreterOptions: {
- logger: console.log.bind('XSTATE:'),
- },
- })
+const editor = acquireVsCodeApi()
- channel.setMachineSend(send)
+// router finds first state match of
+const useRouter = () => {
+ const [state, send] = useMachine(createMachine({ editorSend: editor.postMessage }))
// event bus listener
- React.useEffect(messageBusReceiver, [])
-
- const childArray = React.Children.toArray(children)
- for (const child of childArray) {
- const { path } = child.props
- let pathMatch
- if (typeof path === 'string') {
- pathMatch = state.matches(path)
- } else if (Array.isArray(path)) {
- pathMatch = path.some(p => state.matches(p))
- } else {
- throw new Error(`Invalid route path ${JSON.stringify(path)}`)
+ React.useEffect(() => {
+ const listener = 'message'
+ // propograte channel event to state machine
+ const handler = (action: any) => {
+ // NOTE: must call event.data, cannot destructure. VSCode acts odd
+ const event = action.data
+ // ignore browser events from plugins
+ if (event.source) {
+ return
+ }
+ send(event)
+ }
+ window.addEventListener(listener, handler)
+ return () => {
+ window.removeEventListener(listener, handler)
}
- if (pathMatch) {
- const element = React.cloneElement(child.props.children, { send, context: state.context })
- return debuggerWrapper(element, state)
+ }, [])
+
+ const Router = ({ children }: Props): React.ReactElement | null => {
+ const childArray = React.Children.toArray(children)
+ for (const child of childArray) {
+ // match path
+ const { path } = child.props
+ let pathMatch
+ if (typeof path === 'string') {
+ pathMatch = state.matches(path)
+ } else if (Array.isArray(path)) {
+ pathMatch = path.some(p => state.matches(p))
+ } else {
+ throw new Error(`Invalid route path ${JSON.stringify(path)}`)
+ }
+ if (pathMatch) {
+ // @ts-ignore
+ return child.props.children
+ }
}
+ const message = `No Route matches for ${JSON.stringify(state)}`
+ onError(new Error(message))
+ console.warn(message)
+ return null
}
- const message = `No Route matches for ${JSON.stringify(state)}`
- onError(new Error(message))
- console.warn(message)
- return null
-}
-Router.Route = Route
+ return {
+ context: state.context,
+ send,
+ Router,
+ Route,
+ }
+}
-export default Router
+export default useRouter
diff --git a/web-app/src/containers/New/NewPage.tsx b/web-app/src/containers/New/NewPage.tsx
index b2eb55a1..73bbc444 100644
--- a/web-app/src/containers/New/NewPage.tsx
+++ b/web-app/src/containers/New/NewPage.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import * as T from 'typings'
import * as G from 'typings/graphql'
import { css, jsx } from '@emotion/core'
import TutorialList from './TutorialList'
@@ -23,6 +24,7 @@ const styles = {
}
interface Props {
+ send(action: T.Action): void
tutorialList: G.Tutorial[]
}
@@ -34,7 +36,7 @@ const NewPage = (props: Props) => (
Select a Tutorial to Start
-
+
)
diff --git a/web-app/src/containers/New/TutorialList/index.tsx b/web-app/src/containers/New/TutorialList/index.tsx
index f337d5ca..78223e66 100644
--- a/web-app/src/containers/New/TutorialList/index.tsx
+++ b/web-app/src/containers/New/TutorialList/index.tsx
@@ -1,15 +1,16 @@
import * as React from 'react'
+import * as T from 'typings'
import * as G from 'typings/graphql'
-import channel from '../../../services/channel'
import TutorialItem from './TutorialItem'
interface Props {
+ send(action: T.Action): void
tutorialList: G.Tutorial[]
}
const TutorialList = (props: Props) => {
const onSelect = (tutorial: G.Tutorial) => {
- channel.machineSend({
+ props.send({
type: 'TUTORIAL_START',
payload: {
tutorial,
diff --git a/web-app/src/containers/New/index.tsx b/web-app/src/containers/New/index.tsx
index 74472c8a..40385fd5 100644
--- a/web-app/src/containers/New/index.tsx
+++ b/web-app/src/containers/New/index.tsx
@@ -31,7 +31,7 @@ const NewPageContainer = (props: ContainerProps) => {
return null
}
- return
+ return
}
export default NewPageContainer
diff --git a/web-app/src/services/channel/index.ts b/web-app/src/services/channel/index.ts
deleted file mode 100644
index 64bf55c5..00000000
--- a/web-app/src/services/channel/index.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Action } from 'typings'
-
-declare let acquireVsCodeApi: any
-
-interface ReceivedEvent {
- data: Action
-}
-
-class Channel {
- constructor() {
- // setup mock if browser only
- // @ts-ignore
- if (!window.acquireVsCodeApi) {
- // @ts-ignore
- require('./mock')
- }
-
- // Loads VSCode webview connection with editor
- const editor = acquireVsCodeApi()
-
- this.editorSend = editor.postMessage
- }
-
- public machineSend = (action: Action | string) => {
- /* implemented by `setMachineSend` in router on startup */
- }
- public editorSend = (action: Action) => {
- /* */
- }
-
- public setMachineSend = (send: any) => {
- this.machineSend = send
- }
- public receive = (event: ReceivedEvent) => {
- // NOTE: must call event.data, cannot destructure. VSCode acts odd
- const action = event.data
-
- // @ts-ignore // ignore browser events from plugins
- if (action.source) {
- return
- }
-
- // messages from core
- switch (action.type) {
- case 'ENV_LOAD':
- case 'AUTHENTICATED':
- case 'TUTORIAL_LOADED':
- case 'NEW_TUTORIAL':
- case 'TUTORIAL_CONFIGURED':
- case 'CONTINUE_TUTORIAL':
- case 'TEST_PASS':
- case 'TEST_FAIL':
- case 'TEST_RUNNING':
- case 'TEST_ERROR':
- case 'COMMAND_START':
- case 'COMMAND_SUCCESS':
- case 'COMMAND_FAIL':
- case 'ERROR':
- this.machineSend(action)
- return
- default:
- if (action.type) {
- console.warn(`Unknown received action ${action.type}`, action)
- }
- }
- }
-}
-
-export default new Channel()
diff --git a/web-app/src/services/channel/mock.ts b/web-app/src/services/channel/mock.ts
deleted file mode 100644
index 43ab9dfa..00000000
--- a/web-app/src/services/channel/mock.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Action } from 'typings'
-import channel from './index'
-
-const createReceiveEvent = (action: Action) => ({
- data: action,
-})
-
-// mock vscode from client side development
-// @ts-ignore
-window.acquireVsCodeApi = () => ({
- postMessage(action: Action) {
- switch (action.type) {
- case 'TUTORIAL_START':
- return setTimeout(() => {
- const receiveAction: Action = {
- type: 'TUTORIAL_LOADED',
- }
- channel.receive(createReceiveEvent(receiveAction))
- }, 1000)
- case 'TEST_RUN':
- return setTimeout(() => {
- const receiveAction: Action = {
- type: 'TEST_PASS',
- payload: action.payload,
- }
- channel.receive(createReceiveEvent(receiveAction))
- }, 1000)
- default:
- console.warn(`${action.type} not found in post message mock`)
- }
- },
-})
diff --git a/web-app/src/services/channel/receiver.ts b/web-app/src/services/channel/receiver.ts
deleted file mode 100644
index 9cf20789..00000000
--- a/web-app/src/services/channel/receiver.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import channel from './index'
-
-const messageBusReceiver = () => {
- // update state based on response from editor
- const listener = 'message'
- window.addEventListener(listener, channel.receive)
- return () => {
- window.removeEventListener(listener, channel.receive)
- }
-}
-
-export default messageBusReceiver
diff --git a/web-app/src/services/state/actions/api.ts b/web-app/src/services/state/actions/api.ts
deleted file mode 100644
index 1390dccd..00000000
--- a/web-app/src/services/state/actions/api.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import client from '../../apollo'
-import { setAuthToken } from '../../apollo/auth'
-import authenticateMutation from '../../apollo/mutations/authenticate'
-import channel from '../../channel'
-import onError from '../../../services/sentry/onError'
-
-interface AuthenticateData {
- editorLogin: {
- token: string
- user: G.User
- }
-}
-
-interface AuthenticateVariables {
- machineId: string
- sessionId: string
- editor: 'VSCODE'
-}
-
-export default {
- authenticate: async (context: CR.MachineContext): Promise => {
- const result = await client
- .mutate({
- mutation: authenticateMutation,
- variables: {
- machineId: context.env.machineId,
- sessionId: context.env.sessionId,
- editor: 'VSCODE',
- },
- })
- .catch(error => {
- onError(error)
- console.log('ERROR: Authentication failed')
- console.log(error.message)
- let message
- if (error.message.match(/Network error:/)) {
- message = {
- title: 'Network Error',
- description: 'Make sure you have an Internet connection. Restart and try again',
- }
- } else {
- message = {
- title: 'Server Error',
- description: error.message,
- }
- }
- channel.receive({ data: { type: 'ERROR', payload: { error: message } } })
- return
- })
-
- if (!result || !result.data) {
- const error = new Error('Authentication request responded with no data')
- console.log(error)
- onError(error)
- return
- }
- const { token } = result.data.editorLogin
- // add token to headers
- setAuthToken(token)
- // pass authenticated action back to state machine
- channel.receive({ data: { type: 'AUTHENTICATED' } })
- },
- userTutorialComplete(context: CR.MachineContext) {
- console.log('should update user tutorial as complete')
- },
-}
diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts
index 8e1c3df3..be984f52 100644
--- a/web-app/src/services/state/actions/context.ts
+++ b/web-app/src/services/state/actions/context.ts
@@ -1,10 +1,11 @@
import * as CR from 'typings'
import * as G from 'typings/graphql'
-import { assign, send } from 'xstate'
+import { assign, send, ActionFunctionMap } from 'xstate'
import * as selectors from '../../selectors'
import onError from '../../../services/sentry/onError'
-export default {
+const contextActions: ActionFunctionMap = {
+ // @ts-ignore
setEnv: assign({
env: (context: CR.MachineContext, event: CR.MachineEvent) => {
return {
@@ -13,6 +14,7 @@ export default {
}
},
}),
+ // @ts-ignore
continueTutorial: assign({
tutorial: (context: CR.MachineContext, event: CR.MachineEvent) => {
return event.payload.tutorial
@@ -24,6 +26,7 @@ export default {
return event.payload.position
},
}),
+ // @ts-ignore
newTutorial: assign({
tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => {
return event.payload.tutorial
@@ -32,6 +35,7 @@ export default {
return { levels: {}, steps: {}, complete: false }
},
}),
+ // @ts-ignore
initTutorial: assign({
// loads complete tutorial
tutorial: (context: CR.MachineContext, event: CR.MachineEvent): any => {
@@ -201,6 +205,7 @@ export default {
}
},
),
+ // @ts-ignore
reset: assign({
tutorial() {
return null
@@ -221,3 +226,5 @@ export default {
},
}),
}
+
+export default contextActions
diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts
index 4d84d869..0488b439 100644
--- a/web-app/src/services/state/actions/editor.ts
+++ b/web-app/src/services/state/actions/editor.ts
@@ -1,69 +1,22 @@
import * as CR from 'typings'
import * as G from 'typings/graphql'
-import client from '../../apollo'
-import tutorialQuery from '../../apollo/queries/tutorial'
-import channel from '../../channel'
import * as selectors from '../../selectors'
-import onError from '../../../services/sentry/onError'
-interface TutorialData {
- tutorial: G.Tutorial
-}
-
-interface TutorialDataVariables {
- tutorialId: string
- // version: string
-}
-
-export default {
+export default (editorSend: any) => ({
loadEnv(): void {
- channel.editorSend({
- type: 'ENV_GET',
+ editorSend({
+ type: 'EDITOR_ENV_GET',
})
},
loadStoredTutorial(): void {
// send message to editor to see if there is existing tutorial progress
// in local storage on the editor
- channel.editorSend({
+ editorSend({
type: 'EDITOR_TUTORIAL_LOAD',
})
},
- initializeTutorial(context: CR.MachineContext, event: CR.MachineEvent) {
- // setup test runner and git
- if (!context.tutorial) {
- const error = new Error('Tutorial not available to load')
- onError(error)
- throw error
- }
-
- client
- .query({
- query: tutorialQuery,
- variables: {
- tutorialId: context.tutorial.id,
- // version: context.tutorial.version.version, // TODO: reimplement version
- },
- })
- .then(result => {
- if (!result || !result.data || !result.data.tutorial) {
- const message = 'No tutorial returned from tutorial config query'
- onError(new Error(message))
- return Promise.reject(message)
- }
-
- channel.editorSend({
- type: 'EDITOR_TUTORIAL_CONFIG',
- payload: { tutorial: result.data.tutorial },
- })
- })
- .catch((error: Error) => {
- const message = `Failed to load tutorial config ${error.message}`
- onError(new Error(message))
- return Promise.reject(message)
- })
- },
continueConfig(context: CR.MachineContext) {
- channel.editorSend({
+ editorSend({
type: 'EDITOR_TUTORIAL_CONTINUE_CONFIG',
payload: {
// pass position because current stepId or first stepId will be empty
@@ -75,7 +28,7 @@ export default {
const level: G.Level = selectors.currentLevel(context)
if (level.setup) {
// load step actions
- channel.editorSend({
+ editorSend({
type: 'SETUP_ACTIONS',
payload: level.setup,
})
@@ -85,7 +38,7 @@ export default {
const step: G.Step = selectors.currentStep(context)
if (step.setup) {
// load step actions
- channel.editorSend({
+ editorSend({
type: 'SETUP_ACTIONS',
payload: {
stepId: step.id,
@@ -97,7 +50,7 @@ export default {
editorLoadSolution(context: CR.MachineContext): void {
const step: G.Step = selectors.currentStep(context)
// tell editor to load solution commit
- channel.editorSend({
+ editorSend({
type: 'SOLUTION_ACTIONS',
payload: {
stepId: step.id,
@@ -106,6 +59,6 @@ export default {
})
},
clearStorage(): void {
- channel.editorSend({ type: 'TUTORIAL_CLEAR' })
+ editorSend({ type: 'TUTORIAL_CLEAR' })
},
-}
+})
diff --git a/web-app/src/services/state/actions/index.ts b/web-app/src/services/state/actions/index.ts
deleted file mode 100644
index 79cf318e..00000000
--- a/web-app/src/services/state/actions/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import apiActions from './api'
-import commandActions from './command'
-import contextActions from './context'
-import editorActions from './editor'
-
-export default {
- ...editorActions,
- ...contextActions,
- ...apiActions,
- ...commandActions,
-}
diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts
index dc6b37b5..718c50bd 100644
--- a/web-app/src/services/state/machine.ts
+++ b/web-app/src/services/state/machine.ts
@@ -1,215 +1,240 @@
import * as CR from 'typings'
-import { Machine, MachineOptions } from 'xstate'
-import actions from './actions'
+import { assign, Machine, MachineOptions, actions } from 'xstate'
+import editorActions from './actions/editor'
+import commandActions from './actions/command'
+import contextActions from './actions/context'
+import * as services from './services'
-const options: MachineOptions = {
- // @ts-ignore
- actions,
-}
+const createOptions = ({ editorSend }: any): MachineOptions => ({
+ activities: {},
+ actions: {
+ ...editorActions(editorSend),
+ ...contextActions,
+ ...commandActions,
+ },
+ guards: {},
+ services: {},
+ delays: {},
+})
-export const machine = Machine(
- {
- id: 'root',
- initial: 'Start',
- context: {
- error: null,
- env: { machineId: '', sessionId: '', token: '' },
- tutorial: null,
- position: { levelId: '', stepId: '' },
- progress: {
- levels: {},
- steps: {},
- complete: false,
+export const createMachine = (options: any) => {
+ return Machine(
+ {
+ id: 'root',
+ initial: 'Start',
+ context: {
+ error: null,
+ env: { machineId: '', sessionId: '', token: '' },
+ tutorial: null,
+ position: { levelId: '', stepId: '' },
+ progress: {
+ levels: {},
+ steps: {},
+ complete: false,
+ },
+ processes: [],
},
- processes: [],
- },
- states: {
- Start: {
- initial: 'Startup',
- states: {
- Startup: {
- onEntry: ['loadEnv'],
- on: {
- ENV_LOAD: {
- target: 'Authenticate',
- actions: ['setEnv'],
+ states: {
+ Start: {
+ initial: 'Startup',
+ states: {
+ Startup: {
+ onEntry: ['loadEnv'],
+ on: {
+ ENV_LOAD: {
+ target: 'Authenticate',
+ actions: ['setEnv'],
+ },
},
},
- },
- Authenticate: {
- onEntry: ['authenticate'],
- on: {
- AUTHENTICATED: 'NewOrContinue',
- ERROR: {
- actions: ['setError'],
+ Authenticate: {
+ invoke: {
+ src: services.authenticate,
+ onDone: 'NewOrContinue',
+ onError: {
+ target: 'Error',
+ actions: assign({
+ error: (context, event) => event.data,
+ }),
+ },
},
},
- },
- NewOrContinue: {
- onEntry: ['loadStoredTutorial'],
- on: {
- CONTINUE_TUTORIAL: {
- target: 'ContinueTutorial',
- actions: ['continueTutorial'],
- },
- NEW_TUTORIAL: {
- target: 'SelectTutorial',
+ Error: {},
+ NewOrContinue: {
+ onEntry: ['loadStoredTutorial'],
+ on: {
+ CONTINUE_TUTORIAL: {
+ target: 'ContinueTutorial',
+ actions: ['continueTutorial'],
+ },
+ NEW_TUTORIAL: {
+ target: 'SelectTutorial',
+ },
},
},
- },
- SelectTutorial: {
- onEntry: ['clearStorage'],
- id: 'start-new-tutorial',
- on: {
- TUTORIAL_START: {
- target: '#tutorial',
- actions: ['newTutorial'],
+ SelectTutorial: {
+ onEntry: ['clearStorage'],
+ id: 'start-new-tutorial',
+ on: {
+ TUTORIAL_START: {
+ target: '#tutorial',
+ actions: ['newTutorial'],
+ },
},
},
- },
- ContinueTutorial: {
- on: {
- TUTORIAL_START: {
- target: '#tutorial-level',
- actions: ['continueConfig'],
+ ContinueTutorial: {
+ on: {
+ TUTORIAL_START: {
+ target: '#tutorial-level',
+ actions: ['continueConfig'],
+ },
+ TUTORIAL_SELECT: 'SelectTutorial',
},
- TUTORIAL_SELECT: 'SelectTutorial',
},
},
},
- },
- Tutorial: {
- id: 'tutorial',
- initial: 'Initialize',
- on: {
- // track commands
- COMMAND_START: {
- actions: ['commandStart'],
- },
- COMMAND_SUCCESS: {
- actions: ['commandSuccess'],
- },
- COMMAND_FAIL: {
- actions: ['commandFail'],
- },
- ERROR: {
- actions: ['setError'],
- },
- },
- states: {
- // TODO move Initialize into New Tutorial setup
- Initialize: {
- onEntry: ['initializeTutorial'],
- on: {
- TUTORIAL_CONFIGURED: 'Summary',
- // TUTORIAL_CONFIG_ERROR: 'Start' // TODO should handle error
+ Tutorial: {
+ id: 'tutorial',
+ initial: 'Initialize',
+ on: {
+ // track commands
+ COMMAND_START: {
+ actions: ['commandStart'],
},
- },
- Summary: {
- on: {
- LOAD_TUTORIAL: {
- target: 'Level',
- actions: ['initPosition', 'initTutorial'],
- },
+ COMMAND_SUCCESS: {
+ actions: ['commandSuccess'],
+ },
+ COMMAND_FAIL: {
+ actions: ['commandFail'],
+ },
+ ERROR: {
+ actions: ['setError'],
},
},
- LoadNext: {
- id: 'tutorial-load-next',
- onEntry: ['loadNext'],
- on: {
- NEXT_STEP: {
- target: 'Level',
- actions: ['updatePosition'],
+ states: {
+ // TODO move Initialize into New Tutorial setup
+ Initialize: {
+ invoke: {
+ src: services.initialize,
+ onDone: {
+ target: 'Summary',
+ actions: assign({
+ tutorial: (context, event) => event.data,
+ }),
+ },
+ onError: {
+ target: 'Error',
+ actions: assign({
+ error: (context, event) => event.data,
+ }),
+ },
},
- NEXT_LEVEL: {
- target: 'Level', // TODO should return to levels summary page
- actions: ['updatePosition'],
+ },
+ Error: {},
+ Summary: {
+ on: {
+ LOAD_TUTORIAL: {
+ target: 'Level',
+ actions: ['initPosition', 'initTutorial'],
+ },
},
- COMPLETED: '#completed-tutorial',
},
- },
- Level: {
- initial: 'Load',
- states: {
- Load: {
- onEntry: ['loadLevel', 'loadStep'],
- after: {
- 0: 'Normal',
+ LoadNext: {
+ id: 'tutorial-load-next',
+ onEntry: ['loadNext'],
+ on: {
+ NEXT_STEP: {
+ target: 'Level',
+ actions: ['updatePosition'],
},
+ NEXT_LEVEL: {
+ target: 'Level', // TODO should return to levels summary page
+ actions: ['updatePosition'],
+ },
+ COMPLETED: '#completed-tutorial',
},
- Normal: {
- id: 'tutorial-level',
- on: {
- TEST_RUNNING: 'TestRunning',
- STEP_SOLUTION_LOAD: {
- actions: ['editorLoadSolution'],
+ },
+ Level: {
+ initial: 'Load',
+ states: {
+ Load: {
+ onEntry: ['loadLevel', 'loadStep'],
+ after: {
+ 0: 'Normal',
},
},
- },
- TestRunning: {
- onEntry: ['testStart'],
- on: {
- TEST_PASS: {
- target: 'TestPass',
- actions: ['updateStepProgress'],
+ Normal: {
+ id: 'tutorial-level',
+ on: {
+ TEST_RUNNING: 'TestRunning',
+ STEP_SOLUTION_LOAD: {
+ actions: ['editorLoadSolution'],
+ },
},
- TEST_FAIL: 'TestFail',
- TEST_ERROR: 'TestError',
},
- },
- TestError: {
- onEntry: ['testFail'],
- after: {
- 0: 'Normal',
+ TestRunning: {
+ onEntry: ['testStart'],
+ on: {
+ TEST_PASS: {
+ target: 'TestPass',
+ actions: ['updateStepProgress'],
+ },
+ TEST_FAIL: 'TestFail',
+ TEST_ERROR: 'TestError',
+ },
},
- },
- TestPass: {
- onExit: ['updateStepPosition'],
- after: {
- 1000: 'StepNext',
+ TestError: {
+ onEntry: ['testFail'],
+ after: {
+ 0: 'Normal',
+ },
},
- },
- TestFail: {
- onEntry: ['testFail'],
- after: {
- 0: 'Normal',
+ TestPass: {
+ onExit: ['updateStepPosition'],
+ after: {
+ 1000: 'StepNext',
+ },
},
- },
- StepNext: {
- onEntry: ['stepNext'],
- on: {
- LOAD_NEXT_STEP: {
- target: 'Normal',
- actions: ['loadStep'],
+ TestFail: {
+ onEntry: ['testFail'],
+ after: {
+ 0: 'Normal',
},
- LEVEL_COMPLETE: {
- target: 'LevelComplete',
- actions: ['updateLevelProgress'],
+ },
+ StepNext: {
+ onEntry: ['stepNext'],
+ on: {
+ LOAD_NEXT_STEP: {
+ target: 'Normal',
+ actions: ['loadStep'],
+ },
+ LEVEL_COMPLETE: {
+ target: 'LevelComplete',
+ actions: ['updateLevelProgress'],
+ },
},
},
- },
- LevelComplete: {
- on: {
- LEVEL_NEXT: '#tutorial-load-next',
+ LevelComplete: {
+ on: {
+ LEVEL_NEXT: '#tutorial-load-next',
+ },
},
},
},
- },
- Completed: {
- id: 'completed-tutorial',
- onEntry: ['userTutorialComplete'],
- on: {
- SELECT_TUTORIAL: {
- target: '#start-new-tutorial',
- actions: ['reset'],
+ Completed: {
+ id: 'completed-tutorial',
+ onEntry: ['userTutorialComplete'],
+ on: {
+ SELECT_TUTORIAL: {
+ target: '#start-new-tutorial',
+ actions: ['reset'],
+ },
},
},
},
},
},
},
- },
- options,
-)
-
-export default machine
+ createOptions(options),
+ )
+}
diff --git a/web-app/src/services/state/services/authenticate.ts b/web-app/src/services/state/services/authenticate.ts
new file mode 100644
index 00000000..84ab92b6
--- /dev/null
+++ b/web-app/src/services/state/services/authenticate.ts
@@ -0,0 +1,64 @@
+import * as CR from 'typings'
+import * as G from 'typings/graphql'
+import client from '../../apollo'
+import { setAuthToken } from '../../apollo/auth'
+import authenticateMutation from '../../apollo/mutations/authenticate'
+import onError from '../../../services/sentry/onError'
+
+interface AuthenticateData {
+ editorLogin: {
+ token: string
+ user: G.User
+ }
+}
+
+interface AuthenticateVariables {
+ machineId: string
+ sessionId: string
+ editor: 'VSCODE'
+}
+
+export async function authenticate(context: CR.MachineContext): Promise {
+ const result = await client
+ .mutate({
+ mutation: authenticateMutation,
+ variables: {
+ machineId: context.env.machineId,
+ sessionId: context.env.sessionId,
+ editor: 'VSCODE',
+ },
+ })
+ .catch(error => {
+ onError(error)
+ console.log('ERROR: Authentication failed')
+ console.log(error.message)
+ // let message
+ if (error.message.match(/Network error:/)) {
+ return Promise.reject({
+ title: 'Network Error',
+ description: 'Make sure you have an Internet connection. Restart and try again',
+ })
+ } else {
+ return Promise.reject({
+ title: 'Server Error',
+ description: error.message,
+ })
+ }
+ })
+
+ if (!result || !result.data) {
+ const message = 'Authentication request responded with no data'
+ const error = new Error()
+ console.log(error)
+ onError(error)
+ return Promise.reject({
+ title: message,
+ description: 'Something went wrong.',
+ })
+ }
+ const { token } = result.data.editorLogin
+ // add token to headers
+ setAuthToken(token)
+ // pass authenticated action back to state machine
+ return Promise.resolve()
+}
diff --git a/web-app/src/services/state/services/index.ts b/web-app/src/services/state/services/index.ts
new file mode 100644
index 00000000..1e87068f
--- /dev/null
+++ b/web-app/src/services/state/services/index.ts
@@ -0,0 +1,2 @@
+export { authenticate } from './authenticate'
+export { initialize } from './initialize'
diff --git a/web-app/src/services/state/services/initialize.ts b/web-app/src/services/state/services/initialize.ts
new file mode 100644
index 00000000..b4bac48d
--- /dev/null
+++ b/web-app/src/services/state/services/initialize.ts
@@ -0,0 +1,43 @@
+import * as CR from 'typings'
+import * as G from 'typings/graphql'
+import client from '../../apollo'
+import tutorialQuery from '../../apollo/queries/tutorial'
+import onError from '../../../services/sentry/onError'
+
+interface TutorialData {
+ tutorial: G.Tutorial
+}
+
+interface TutorialDataVariables {
+ tutorialId: string
+ // version: string
+}
+
+export async function initialize(context: CR.MachineContext): Promise {
+ // setup test runner and git
+ if (!context.tutorial) {
+ const error = new Error('Tutorial not available to load')
+ onError(error)
+ throw error
+ }
+
+ try {
+ const result = await client.query({
+ query: tutorialQuery,
+ variables: {
+ tutorialId: context.tutorial.id,
+ // version: context.tutorial.version.version, // TODO: reimplement version
+ },
+ })
+ if (!result || !result.data || !result.data.tutorial) {
+ const message = 'No tutorial returned from tutorial config query'
+ onError(new Error(message))
+ return Promise.reject(message)
+ }
+ return Promise.resolve(result.data.tutorial)
+ } catch (error) {
+ const message: CR.ErrorMessage = { title: 'Failed to load tutorial config', description: error.message }
+ onError(error)
+ return Promise.reject(message)
+ }
+}