| /** |
| * Copyright (c) HashiCorp, Inc. |
| * SPDX-License-Identifier: MPL-2.0 |
| */ |
| |
| import { next } from '@ember/runloop'; |
| import { typeOf } from '@ember/utils'; |
| import Service, { inject as service } from '@ember/service'; |
| import { Machine } from 'xstate'; |
| import { capitalize } from '@ember/string'; |
| |
| import getStorage from 'vault/lib/token-storage'; |
| import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants'; |
| const { |
| TUTORIAL_STATE, |
| COMPONENT_STATE, |
| FEATURE_STATE, |
| FEATURE_LIST, |
| FEATURE_STATE_HISTORY, |
| COMPLETED_FEATURES, |
| RESUME_URL, |
| RESUME_ROUTE, |
| } = STORAGE_KEYS; |
| const TutorialMachine = Machine(MACHINES.tutorial); |
| let FeatureMachine = null; |
| |
| export default Service.extend(DEFAULTS, { |
| router: service(), |
| showWhenUnauthenticated: false, |
| featureMachineHistory: null, |
| init() { |
| this._super(...arguments); |
| this.initializeMachines(); |
| }, |
| |
| initializeMachines() { |
| if (!this.storageHasKey(TUTORIAL_STATE)) { |
| const state = TutorialMachine.initialState; |
| this.saveState('currentState', state.value); |
| this.saveExtState(TUTORIAL_STATE, state.value); |
| } |
| this.saveState('currentState', this.getExtState(TUTORIAL_STATE)); |
| if (this.storageHasKey(COMPONENT_STATE)) { |
| this.set('componentState', this.getExtState(COMPONENT_STATE)); |
| } |
| const stateNodes = TutorialMachine.getStateNodes(this.currentState); |
| this.executeActions( |
| stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), |
| null, |
| 'tutorial' |
| ); |
| |
| if (this.storageHasKey(FEATURE_LIST)) { |
| this.set('featureList', this.getExtState(FEATURE_LIST)); |
| if (this.storageHasKey(FEATURE_STATE_HISTORY)) { |
| this.set('featureMachineHistory', this.getExtState(FEATURE_STATE_HISTORY)); |
| } |
| this.saveState( |
| 'featureState', |
| this.getExtState(FEATURE_STATE) || (FeatureMachine ? FeatureMachine.initialState : null) |
| ); |
| this.saveExtState(FEATURE_STATE, this.featureState); |
| this.buildFeatureMachine(); |
| } |
| }, |
| |
| clearFeatureData() { |
| const storage = this.storage(); |
| // empty storage |
| [FEATURE_LIST, FEATURE_STATE, FEATURE_STATE_HISTORY, COMPLETED_FEATURES].forEach((key) => |
| storage.removeItem(key) |
| ); |
| |
| this.set('currentMachine', null); |
| this.set('featureMachineHistory', null); |
| this.set('featureState', null); |
| this.set('featureList', null); |
| }, |
| |
| restartGuide() { |
| this.clearFeatureData(); |
| const storage = this.storage(); |
| // empty storage |
| [TUTORIAL_STATE, COMPONENT_STATE, RESUME_URL, RESUME_ROUTE].forEach((key) => storage.removeItem(key)); |
| // reset wizard state |
| this.setProperties(DEFAULTS); |
| // restart machines from blank state |
| this.initializeMachines(); |
| // progress machine to 'active.select' |
| this.transitionTutorialMachine('idle', 'AUTH'); |
| }, |
| |
| saveFeatureHistory(state) { |
| if ( |
| this.getCompletedFeatures().length === 0 && |
| this.featureMachineHistory === null && |
| (state === 'idle' || state === 'wrap') |
| ) { |
| const newHistory = [state]; |
| this.set('featureMachineHistory', newHistory); |
| } else { |
| if (this.featureMachineHistory) { |
| if (!this.featureMachineHistory.includes(state)) { |
| const newHistory = this.featureMachineHistory.addObject(state); |
| this.set('featureMachineHistory', newHistory); |
| } else { |
| //we're repeating steps |
| const stepIndex = this.featureMachineHistory.indexOf(state); |
| const newHistory = this.featureMachineHistory.splice(0, stepIndex + 1); |
| this.set('featureMachineHistory', newHistory); |
| } |
| } |
| } |
| if (this.featureMachineHistory) { |
| this.saveExtState(FEATURE_STATE_HISTORY, this.featureMachineHistory); |
| } |
| }, |
| |
| saveState(stateType, state) { |
| if (state.value) { |
| state = state.value; |
| } |
| let stateKey = ''; |
| while (typeOf(state) === 'object') { |
| const newState = Object.keys(state); |
| stateKey += newState + '.'; |
| state = state[newState]; |
| } |
| stateKey += state; |
| this.set(stateType, stateKey); |
| if (stateType === 'featureState') { |
| //only track progress if we are on the first step of the first feature |
| this.saveFeatureHistory(state); |
| } |
| }, |
| |
| transitionTutorialMachine(currentState, event, extendedState) { |
| if (extendedState) { |
| this.set('componentState', extendedState); |
| this.saveExtState(COMPONENT_STATE, extendedState); |
| } |
| const { actions, value } = TutorialMachine.transition(currentState, event); |
| this.saveState('currentState', value); |
| this.saveExtState(TUTORIAL_STATE, this.currentState); |
| this.executeActions(actions, event, 'tutorial'); |
| }, |
| |
| transitionFeatureMachine(currentState, event, extendedState) { |
| if (!FeatureMachine || !this.currentState.includes('active')) { |
| return; |
| } |
| if (extendedState) { |
| this.set('componentState', extendedState); |
| this.saveExtState(COMPONENT_STATE, extendedState); |
| } |
| |
| const { actions, value } = FeatureMachine.transition(currentState, event, this.componentState); |
| this.saveState('featureState', value); |
| this.saveExtState(FEATURE_STATE, value); |
| this.executeActions(actions, event, 'feature'); |
| // if all features were completed, the FeatureMachine gets nulled |
| // out and won't exist here as there is no next step |
| if (FeatureMachine) { |
| let next; |
| if (this.currentMachine === 'secrets' && value === 'display') { |
| next = FeatureMachine.transition(value, 'REPEAT', this.componentState); |
| } else { |
| next = FeatureMachine.transition(value, 'CONTINUE', this.componentState); |
| } |
| this.saveState('nextStep', next.value); |
| } |
| }, |
| |
| saveExtState(key, value) { |
| this.storage().setItem(key, value); |
| }, |
| |
| getExtState(key) { |
| return this.storage().getItem(key); |
| }, |
| |
| storageHasKey(key) { |
| return Boolean(this.getExtState(key)); |
| }, |
| |
| executeActions(actions, event, machineType) { |
| let transitionURL; |
| let expectedRouteName; |
| const router = this.router; |
| |
| for (const action of actions) { |
| let type = action; |
| if (action.type) { |
| type = action.type; |
| } |
| switch (type) { |
| case 'render': |
| this.set(`${action.level}Component`, action.component); |
| break; |
| case 'routeTransition': |
| expectedRouteName = action.params[0]; |
| transitionURL = router.urlFor(...action.params).replace(/^\/ui/, ''); |
| next(() => { |
| router.transitionTo(...action.params); |
| }); |
| break; |
| case 'saveFeatures': |
| this.saveFeatures(event.features); |
| break; |
| case 'completeFeature': |
| this.completeFeature(); |
| break; |
| case 'handleDismissed': |
| this.handleDismissed(); |
| break; |
| case 'handlePaused': |
| this.handlePaused(); |
| return; |
| case 'handleResume': |
| this.handleResume(); |
| break; |
| case 'showTutorialWhenAuthenticated': |
| this.set('showWhenUnauthenticated', false); |
| break; |
| case 'showTutorialAlways': |
| this.set('showWhenUnauthenticated', true); |
| break; |
| case 'clearFeatureData': |
| this.clearFeatureData(); |
| break; |
| case 'continueFeature': |
| this.transitionFeatureMachine(this.featureState, 'CONTINUE', this.componentState); |
| break; |
| default: |
| break; |
| } |
| } |
| if (machineType === 'tutorial') { |
| return; |
| } |
| // if we're transitioning in the actions, we want that url, |
| // else we want the URL we land on in didTransition in the |
| // application route - we'll notify the application route to |
| // update the route |
| if (transitionURL) { |
| this.set('expectedURL', transitionURL); |
| this.set('expectedRouteName', expectedRouteName); |
| this.set('setURLAfterTransition', false); |
| } else { |
| this.set('setURLAfterTransition', true); |
| } |
| }, |
| |
| handlePaused() { |
| const expected = this.expectedURL; |
| if (expected) { |
| this.saveExtState(RESUME_URL, this.expectedURL); |
| this.saveExtState(RESUME_ROUTE, this.expectedRouteName); |
| } |
| }, |
| |
| handleResume() { |
| const resumeURL = this.storage().getItem(RESUME_URL); |
| if (!resumeURL) { |
| return; |
| } |
| this.router |
| .transitionTo(resumeURL) |
| .followRedirects() |
| .then(() => { |
| this.set('expectedRouteName', this.storage().getItem(RESUME_ROUTE)); |
| this.set('expectedURL', resumeURL); |
| this.initializeMachines(); |
| this.storage().removeItem(RESUME_URL); |
| }); |
| }, |
| |
| handleDismissed() { |
| this.storage().removeItem(FEATURE_STATE); |
| this.storage().removeItem(FEATURE_LIST); |
| this.storage().removeItem(FEATURE_STATE_HISTORY); |
| this.storage().removeItem(COMPONENT_STATE); |
| }, |
| |
| saveFeatures(features) { |
| this.set('featureList', features); |
| this.saveExtState(FEATURE_LIST, this.featureList); |
| this.buildFeatureMachine(); |
| }, |
| |
| buildFeatureMachine() { |
| if (this.featureList === null) { |
| return; |
| } |
| this.startFeature(); |
| const nextFeature = this.featureList.length > 1 ? capitalize(this.featureList.objectAt(1)) : 'Finish'; |
| this.set('nextFeature', nextFeature); |
| let next; |
| if (this.currentMachine === 'secrets' && this.featureState === 'display') { |
| next = FeatureMachine.transition(this.featureState, 'REPEAT', this.componentState); |
| } else { |
| next = FeatureMachine.transition(this.featureState, 'CONTINUE', this.componentState); |
| } |
| this.saveState('nextStep', next.value); |
| const stateNodes = FeatureMachine.getStateNodes(this.featureState); |
| this.executeActions( |
| stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), |
| null, |
| 'feature' |
| ); |
| }, |
| |
| startFeature() { |
| const FeatureMachineConfig = MACHINES[this.featureList.objectAt(0)]; |
| FeatureMachine = Machine(FeatureMachineConfig); |
| this.set('currentMachine', this.featureList.objectAt(0)); |
| if (this.storageHasKey(FEATURE_STATE)) { |
| this.saveState('featureState', this.getExtState(FEATURE_STATE)); |
| } else { |
| this.saveState('featureState', FeatureMachine.initialState); |
| } |
| this.saveExtState(FEATURE_STATE, this.featureState); |
| }, |
| |
| getCompletedFeatures() { |
| if (this.storageHasKey(COMPLETED_FEATURES)) { |
| return this.getExtState(COMPLETED_FEATURES).toArray(); |
| } |
| return []; |
| }, |
| |
| completeFeature() { |
| const features = this.featureList; |
| const done = features.shift(); |
| if (!this.getExtState(COMPLETED_FEATURES)) { |
| const completed = []; |
| completed.push(done); |
| this.saveExtState(COMPLETED_FEATURES, completed); |
| } else { |
| this.saveExtState(COMPLETED_FEATURES, this.getExtState(COMPLETED_FEATURES).toArray().addObject(done)); |
| } |
| |
| this.saveExtState(FEATURE_LIST, features.length ? features : null); |
| this.storage().removeItem(FEATURE_STATE); |
| if (this.featureMachineHistory) { |
| this.set('featureMachineHistory', []); |
| this.saveExtState(FEATURE_STATE_HISTORY, []); |
| } |
| if (features.length > 0) { |
| this.buildFeatureMachine(); |
| } else { |
| this.storage().removeItem(FEATURE_LIST); |
| FeatureMachine = null; |
| this.transitionTutorialMachine(this.currentState, 'DONE'); |
| } |
| }, |
| |
| storage() { |
| return getStorage(); |
| }, |
| }); |