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) + } +}