UI: onboarding wizard progress bar (#5516)
Onboarding will now display your progress through the chosen tutorials
This commit is contained in:
parent
793de3b561
commit
7f430bba8f
|
@ -22,6 +22,8 @@ const GLYPHS_WITH_SVG_TAG = [
|
|||
'control-lock',
|
||||
'edition-enterprise',
|
||||
'edition-oss',
|
||||
'check-plain',
|
||||
'check-circle-fill',
|
||||
];
|
||||
|
||||
export default Component.extend({
|
||||
|
|
|
@ -1,13 +1,100 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { FEATURE_MACHINE_STEPS, INIT_STEPS } from 'vault/helpers/wizard-constants';
|
||||
|
||||
export default Component.extend({
|
||||
wizard: service(),
|
||||
classNames: ['ui-wizard'],
|
||||
glyph: null,
|
||||
headerText: null,
|
||||
selectProgress: null,
|
||||
currentMachine: computed.alias('wizard.currentMachine'),
|
||||
tutorialState: computed.alias('wizard.currentState'),
|
||||
tutorialComponent: computed.alias('wizard.tutorialComponent'),
|
||||
showProgress: computed('wizard.featureComponent', 'tutorialState', function() {
|
||||
return (
|
||||
this.tutorialComponent.includes('active') &&
|
||||
(this.tutorialState.includes('init.active') ||
|
||||
(this.wizard.featureComponent && this.wizard.featureMachineHistory))
|
||||
);
|
||||
}),
|
||||
featureMachineHistory: computed.alias('wizard.featureMachineHistory'),
|
||||
totalFeatures: computed('wizard.featureList', function() {
|
||||
return this.wizard.featureList.length;
|
||||
}),
|
||||
completedFeatures: computed('wizard.currentMachine', function() {
|
||||
return this.wizard.getCompletedFeatures();
|
||||
}),
|
||||
currentFeatureProgress: computed('featureMachineHistory.[]', function() {
|
||||
if (this.tutorialState.includes('active.feature')) {
|
||||
let totalSteps = FEATURE_MACHINE_STEPS[this.currentMachine];
|
||||
if (this.currentMachine === 'secrets') {
|
||||
if (this.featureMachineHistory.includes('secret')) {
|
||||
totalSteps = totalSteps['secret']['secret'];
|
||||
}
|
||||
if (this.featureMachineHistory.includes('list')) {
|
||||
totalSteps = totalSteps['secret']['list'];
|
||||
}
|
||||
if (this.featureMachineHistory.includes('encryption')) {
|
||||
totalSteps = totalSteps['encryption'];
|
||||
}
|
||||
if (this.featureMachineHistory.includes('role') || typeof totalSteps === 'object') {
|
||||
totalSteps = totalSteps['role'];
|
||||
}
|
||||
}
|
||||
return {
|
||||
percentage: (this.featureMachineHistory.length / totalSteps) * 100,
|
||||
feature: this.currentMachine,
|
||||
text: `Step ${this.featureMachineHistory.length} of ${totalSteps}`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
currentTutorialProgress: computed('tutorialState', function() {
|
||||
if (this.tutorialState.includes('init.active')) {
|
||||
let currentStepName = this.tutorialState.split('.')[2];
|
||||
let currentStepNumber = INIT_STEPS.indexOf(currentStepName) + 1;
|
||||
return {
|
||||
percentage: (currentStepNumber / INIT_STEPS.length) * 100,
|
||||
text: `Step ${currentStepNumber} of ${INIT_STEPS.length}`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
progressBar: computed('currentFeatureProgress', 'currentFeature', 'currentTutorialProgress', function() {
|
||||
let bar = [];
|
||||
if (this.currentTutorialProgress) {
|
||||
bar.push({
|
||||
style: `width:${this.currentTutorialProgress.percentage}%;`,
|
||||
completed: false,
|
||||
showIcon: true,
|
||||
});
|
||||
} else {
|
||||
if (this.currentFeatureProgress) {
|
||||
this.completedFeatures.forEach(feature => {
|
||||
bar.push({ style: 'width:100%;', completed: true, feature: feature, showIcon: true });
|
||||
});
|
||||
this.wizard.featureList.forEach(feature => {
|
||||
if (feature === this.currentMachine) {
|
||||
bar.push({
|
||||
style: `width:${this.currentFeatureProgress.percentage}%;`,
|
||||
completed: this.currentFeatureProgress.percentage == 100 ? true : false,
|
||||
feature: feature,
|
||||
showIcon: true,
|
||||
});
|
||||
} else {
|
||||
bar.push({ style: 'width:0%;', completed: false, feature: feature, showIcon: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return bar;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
dismissWizard() {
|
||||
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'DISMISS');
|
||||
this.wizard.transitionTutorialMachine(this.wizard.currentState, 'DISMISS');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { FEATURE_MACHINE_TIME } from 'vault/helpers/wizard-constants';
|
||||
|
||||
export default Component.extend({
|
||||
wizard: service(),
|
||||
version: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.maybeHideFeatures();
|
||||
|
@ -16,7 +18,24 @@ export default Component.extend({
|
|||
feature.show = false;
|
||||
}
|
||||
},
|
||||
|
||||
estimatedTime: computed('selectedFeatures', function() {
|
||||
let time = 0;
|
||||
for (let feature of Object.keys(FEATURE_MACHINE_TIME)) {
|
||||
if (this.selectedFeatures.includes(feature)) {
|
||||
time += FEATURE_MACHINE_TIME[feature];
|
||||
}
|
||||
}
|
||||
return time;
|
||||
}),
|
||||
selectProgress: computed('selectedFeatures', function() {
|
||||
let bar = this.selectedFeatures.map(feature => {
|
||||
return { style: 'width:0%;', completed: false, showIcon: true, feature: feature };
|
||||
});
|
||||
if (bar.length === 0) {
|
||||
bar = [{ style: 'width:0%;', showIcon: false }];
|
||||
}
|
||||
return bar;
|
||||
}),
|
||||
allFeatures: computed(function() {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@ export const STORAGE_KEYS = {
|
|||
TUTORIAL_STATE: 'vault:ui-tutorial-state',
|
||||
FEATURE_LIST: 'vault:ui-feature-list',
|
||||
FEATURE_STATE: 'vault:ui-feature-state',
|
||||
FEATURE_STATE_HISTORY: 'vault:ui-feature-state-history',
|
||||
COMPLETED_FEATURES: 'vault:ui-completed-list',
|
||||
COMPONENT_STATE: 'vault:ui-component-state',
|
||||
RESUME_URL: 'vault:ui-tutorial-resume-url',
|
||||
|
@ -36,4 +37,30 @@ export const DEFAULTS = {
|
|||
componentState: null,
|
||||
nextFeature: null,
|
||||
nextStep: null,
|
||||
featureMachineHistory: null,
|
||||
};
|
||||
|
||||
export const FEATURE_MACHINE_STEPS = {
|
||||
secrets: {
|
||||
encryption: 5,
|
||||
secret: {
|
||||
list: 4,
|
||||
secret: 5,
|
||||
},
|
||||
role: 7,
|
||||
},
|
||||
policies: 5,
|
||||
replication: 2,
|
||||
tools: 8,
|
||||
authentication: 4,
|
||||
};
|
||||
|
||||
export const INIT_STEPS = ['setup', 'save', 'unseal', 'login'];
|
||||
|
||||
export const FEATURE_MACHINE_TIME = {
|
||||
secrets: 7,
|
||||
policies: 5,
|
||||
replication: 5,
|
||||
tools: 8,
|
||||
authentication: 5,
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ export default {
|
|||
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' },
|
||||
{ type: 'render', level: 'feature', component: null },
|
||||
],
|
||||
onExit: ['showTutorialWhenAuthenticated'],
|
||||
onExit: ['showTutorialWhenAuthenticated', 'clearFeatureData'],
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
|
|
|
@ -26,11 +26,8 @@ export default Route.extend({
|
|||
actions: {
|
||||
didTransition() {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
if (this.get('wizard.currentMachine') === 'tools') {
|
||||
this.get('wizard').transitionFeatureMachine(
|
||||
this.get('wizard.featureState'),
|
||||
params.selectedAction.toUpperCase()
|
||||
);
|
||||
if (this.wizard.currentMachine === 'tools') {
|
||||
this.wizard.transitionFeatureMachine(this.wizard.featureState, params.selected_action.toUpperCase());
|
||||
}
|
||||
this.controller.setProperties(params);
|
||||
return true;
|
||||
|
|
|
@ -5,56 +5,73 @@ import { Machine } from 'xstate';
|
|||
|
||||
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(STORAGE_KEYS.TUTORIAL_STATE)) {
|
||||
if (!this.storageHasKey(TUTORIAL_STATE)) {
|
||||
let state = TutorialMachine.initialState;
|
||||
this.saveState('currentState', state.value);
|
||||
this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, state.value);
|
||||
this.saveExtState(TUTORIAL_STATE, state.value);
|
||||
}
|
||||
this.saveState('currentState', this.getExtState(STORAGE_KEYS.TUTORIAL_STATE));
|
||||
if (this.storageHasKey(STORAGE_KEYS.COMPONENT_STATE)) {
|
||||
this.set('componentState', this.getExtState(STORAGE_KEYS.COMPONENT_STATE));
|
||||
this.saveState('currentState', this.getExtState(TUTORIAL_STATE));
|
||||
if (this.storageHasKey(COMPONENT_STATE)) {
|
||||
this.set('componentState', this.getExtState(COMPONENT_STATE));
|
||||
}
|
||||
let stateNodes = TutorialMachine.getStateNodes(this.get('currentState'));
|
||||
let stateNodes = TutorialMachine.getStateNodes(this.currentState);
|
||||
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'tutorial');
|
||||
if (this.storageHasKey(STORAGE_KEYS.FEATURE_LIST)) {
|
||||
this.set('featureList', this.getExtState(STORAGE_KEYS.FEATURE_LIST));
|
||||
if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) {
|
||||
this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE));
|
||||
} else {
|
||||
if (FeatureMachine != null) {
|
||||
this.saveState('featureState', FeatureMachine.initialState);
|
||||
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState'));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
restartGuide() {
|
||||
clearFeatureData() {
|
||||
let storage = this.storage();
|
||||
// empty storage
|
||||
[
|
||||
STORAGE_KEYS.TUTORIAL_STATE,
|
||||
STORAGE_KEYS.FEATURE_LIST,
|
||||
STORAGE_KEYS.FEATURE_STATE,
|
||||
STORAGE_KEYS.COMPLETED_FEATURES,
|
||||
STORAGE_KEYS.COMPONENT_STATE,
|
||||
STORAGE_KEYS.RESUME_URL,
|
||||
STORAGE_KEYS.RESUME_ROUTE,
|
||||
].forEach(key => storage.removeItem(key));
|
||||
[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();
|
||||
let 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
|
||||
|
@ -63,6 +80,32 @@ export default Service.extend(DEFAULTS, {
|
|||
this.transitionTutorialMachine('idle', 'AUTH');
|
||||
},
|
||||
|
||||
saveFeatureHistory(state) {
|
||||
if (
|
||||
this.getCompletedFeatures().length === 0 &&
|
||||
this.featureMachineHistory === null &&
|
||||
(state === 'idle' || state === 'wrap')
|
||||
) {
|
||||
let newHistory = [state];
|
||||
this.set('featureMachineHistory', newHistory);
|
||||
} else {
|
||||
if (this.featureMachineHistory) {
|
||||
if (!this.featureMachineHistory.includes(state)) {
|
||||
let newHistory = this.featureMachineHistory.addObject(state);
|
||||
this.set('featureMachineHistory', newHistory);
|
||||
} else {
|
||||
//we're repeating steps
|
||||
let stepIndex = this.featureMachineHistory.indexOf(state);
|
||||
let 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;
|
||||
|
@ -75,40 +118,44 @@ export default Service.extend(DEFAULTS, {
|
|||
}
|
||||
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(STORAGE_KEYS.COMPONENT_STATE, extendedState);
|
||||
this.saveExtState(COMPONENT_STATE, extendedState);
|
||||
}
|
||||
let { actions, value } = TutorialMachine.transition(currentState, event);
|
||||
this.saveState('currentState', value);
|
||||
this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, this.get('currentState'));
|
||||
this.saveExtState(TUTORIAL_STATE, this.currentState);
|
||||
this.executeActions(actions, event, 'tutorial');
|
||||
},
|
||||
|
||||
transitionFeatureMachine(currentState, event, extendedState) {
|
||||
if (!FeatureMachine || !this.get('currentState').includes('active')) {
|
||||
if (!FeatureMachine || !this.currentState.includes('active')) {
|
||||
return;
|
||||
}
|
||||
if (extendedState) {
|
||||
this.set('componentState', extendedState);
|
||||
this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState);
|
||||
this.saveExtState(COMPONENT_STATE, extendedState);
|
||||
}
|
||||
|
||||
let { actions, value } = FeatureMachine.transition(currentState, event, this.get('componentState'));
|
||||
this.saveState('featureState', value);
|
||||
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, 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.get('currentMachine') === 'secrets' && value === 'display') {
|
||||
next = FeatureMachine.transition(value, 'REPEAT', this.get('componentState'));
|
||||
if (this.currentMachine === 'secrets' && value === 'display') {
|
||||
next = FeatureMachine.transition(value, 'REPEAT', this.componentState);
|
||||
} else {
|
||||
next = FeatureMachine.transition(value, 'CONTINUE', this.get('componentState'));
|
||||
next = FeatureMachine.transition(value, 'CONTINUE', this.componentState);
|
||||
}
|
||||
this.saveState('nextStep', next.value);
|
||||
}
|
||||
|
@ -129,7 +176,7 @@ export default Service.extend(DEFAULTS, {
|
|||
executeActions(actions, event, machineType) {
|
||||
let transitionURL;
|
||||
let expectedRouteName;
|
||||
let router = this.get('router');
|
||||
let router = this.router;
|
||||
|
||||
for (let action of actions) {
|
||||
let type = action;
|
||||
|
@ -168,8 +215,11 @@ export default Service.extend(DEFAULTS, {
|
|||
case 'showTutorialAlways':
|
||||
this.set('showWhenUnauthenticated', true);
|
||||
break;
|
||||
case 'clearFeatureData':
|
||||
this.clearFeatureData();
|
||||
break;
|
||||
case 'continueFeature':
|
||||
this.transitionFeatureMachine(this.get('featureState'), 'CONTINUE', this.get('componentState'));
|
||||
this.transitionFeatureMachine(this.featureState, 'CONTINUE', this.componentState);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -192,94 +242,107 @@ export default Service.extend(DEFAULTS, {
|
|||
},
|
||||
|
||||
handlePaused() {
|
||||
let expected = this.get('expectedURL');
|
||||
let expected = this.expectedURL;
|
||||
if (expected) {
|
||||
this.saveExtState(STORAGE_KEYS.RESUME_URL, this.get('expectedURL'));
|
||||
this.saveExtState(STORAGE_KEYS.RESUME_ROUTE, this.get('expectedRouteName'));
|
||||
this.saveExtState(RESUME_URL, this.expectedURL);
|
||||
this.saveExtState(RESUME_ROUTE, this.expectedRouteName);
|
||||
}
|
||||
},
|
||||
|
||||
handleResume() {
|
||||
let resumeURL = this.storage().getItem(STORAGE_KEYS.RESUME_URL);
|
||||
let resumeURL = this.storage().getItem(RESUME_URL);
|
||||
if (!resumeURL) {
|
||||
return;
|
||||
}
|
||||
this.get('router').transitionTo(resumeURL).followRedirects().then(() => {
|
||||
this.set('expectedRouteName', this.storage().getItem(STORAGE_KEYS.RESUME_ROUTE));
|
||||
this.set('expectedURL', resumeURL);
|
||||
this.initializeMachines();
|
||||
this.storage().removeItem(STORAGE_KEYS.RESUME_URL);
|
||||
});
|
||||
this.get('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(STORAGE_KEYS.FEATURE_STATE);
|
||||
this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST);
|
||||
this.storage().removeItem(STORAGE_KEYS.COMPONENT_STATE);
|
||||
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(STORAGE_KEYS.FEATURE_LIST, this.get('featureList'));
|
||||
this.saveExtState(FEATURE_LIST, this.featureList);
|
||||
this.buildFeatureMachine();
|
||||
},
|
||||
|
||||
buildFeatureMachine() {
|
||||
if (this.get('featureList') === null) {
|
||||
if (this.featureList === null) {
|
||||
return;
|
||||
}
|
||||
this.startFeature();
|
||||
if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) {
|
||||
this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE));
|
||||
}
|
||||
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState'));
|
||||
let nextFeature =
|
||||
this.get('featureList').length > 1
|
||||
? this.get('featureList')
|
||||
.objectAt(1)
|
||||
.capitalize()
|
||||
: 'Finish';
|
||||
let nextFeature = this.featureList.length > 1 ? this.featureList.objectAt(1).capitalize() : 'Finish';
|
||||
this.set('nextFeature', nextFeature);
|
||||
let next;
|
||||
if (this.get('currentMachine') === 'secrets' && this.get('featureState') === 'display') {
|
||||
next = FeatureMachine.transition(this.get('featureState'), 'REPEAT', this.get('componentState'));
|
||||
if (this.currentMachine === 'secrets' && this.featureState === 'display') {
|
||||
next = FeatureMachine.transition(this.featureState, 'REPEAT', this.componentState);
|
||||
} else {
|
||||
next = FeatureMachine.transition(this.get('featureState'), 'CONTINUE', this.get('componentState'));
|
||||
next = FeatureMachine.transition(this.featureState, 'CONTINUE', this.componentState);
|
||||
}
|
||||
this.saveState('nextStep', next.value);
|
||||
let stateNodes = FeatureMachine.getStateNodes(this.get('featureState'));
|
||||
let stateNodes = FeatureMachine.getStateNodes(this.featureState);
|
||||
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'feature');
|
||||
},
|
||||
|
||||
startFeature() {
|
||||
const FeatureMachineConfig = MACHINES[this.get('featureList').objectAt(0)];
|
||||
const FeatureMachineConfig = MACHINES[this.featureList.objectAt(0)];
|
||||
FeatureMachine = Machine(FeatureMachineConfig);
|
||||
this.set('currentMachine', this.get('featureList').objectAt(0));
|
||||
this.saveState('featureState', FeatureMachine.initialState);
|
||||
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() {
|
||||
let features = this.get('featureList');
|
||||
let features = this.featureList;
|
||||
let done = features.shift();
|
||||
if (!this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES)) {
|
||||
if (!this.getExtState(COMPLETED_FEATURES)) {
|
||||
let completed = [];
|
||||
completed.push(done);
|
||||
this.saveExtState(STORAGE_KEYS.COMPLETED_FEATURES, completed);
|
||||
this.saveExtState(COMPLETED_FEATURES, completed);
|
||||
} else {
|
||||
this.saveExtState(
|
||||
STORAGE_KEYS.COMPLETED_FEATURES,
|
||||
this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES).toArray().addObject(done)
|
||||
COMPLETED_FEATURES,
|
||||
this.getExtState(COMPLETED_FEATURES)
|
||||
.toArray()
|
||||
.addObject(done)
|
||||
);
|
||||
}
|
||||
|
||||
this.saveExtState(STORAGE_KEYS.FEATURE_LIST, features.length ? features : null);
|
||||
this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE);
|
||||
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(STORAGE_KEYS.FEATURE_LIST);
|
||||
this.storage().removeItem(FEATURE_LIST);
|
||||
FeatureMachine = null;
|
||||
this.transitionTutorialMachine(this.get('currentState'), 'DONE');
|
||||
this.transitionTutorialMachine(this.currentState, 'DONE');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
.wizard-header {
|
||||
border-bottom: $light-border;
|
||||
padding: $size-8 $size-4 $size-8 2rem;
|
||||
padding: 0 $size-4 $size-8 2rem;
|
||||
margin: $size-4 0;
|
||||
position: relative;
|
||||
|
||||
|
@ -79,7 +79,7 @@
|
|||
.title .icon {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0.7rem;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,3 +173,91 @@
|
|||
.wizard-instructions {
|
||||
margin: $size-4 0;
|
||||
}
|
||||
|
||||
.selection-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.time-estimate {
|
||||
align-items: center;
|
||||
color: $grey;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
align-items: center;
|
||||
background: $white;
|
||||
bottom: 0;
|
||||
height: $wizard-progress-bar-height;
|
||||
display: flex;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
transform: translateY(50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: $ui-gray-100;
|
||||
box-shadow: inset 0 0 0 1px $ui-gray-200;
|
||||
display: flex;
|
||||
height: $wizard-progress-bar-height;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-progress-container {
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
padding: 0 ($wizard-progress-check-size / 4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-progress {
|
||||
background: $green;
|
||||
border-radius: $wizard-progress-bar-height;
|
||||
height: $wizard-progress-bar-height;
|
||||
}
|
||||
|
||||
.feature-check {
|
||||
height: $wizard-progress-check-size;
|
||||
left: $wizard-progress-check-size / 2;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: $wizard-progress-check-size;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.feature-progress-container .feature-check {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.feature-progress-container:first-child {
|
||||
padding-left: 0;
|
||||
|
||||
.progress-bar,
|
||||
.feature-progress {
|
||||
border-radius: $wizard-progress-bar-height 0 0 $wizard-progress-bar-height;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-progress-container:first-child:last-child {
|
||||
.progress-bar,
|
||||
.feature-progress {
|
||||
border-radius: $wizard-progress-bar-height;
|
||||
}
|
||||
}
|
||||
|
||||
.incomplete-check svg {
|
||||
fill: $ui-gray-200;
|
||||
}
|
||||
|
||||
.completed-check svg {
|
||||
fill: $green;
|
||||
}
|
||||
|
|
|
@ -217,14 +217,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
|
|||
}
|
||||
|
||||
.button.next-feature-step {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: $white;
|
||||
color: $blue;
|
||||
box-shadow: none;
|
||||
display: block;
|
||||
border: 1px solid $grey-light;
|
||||
border-radius: $radius;
|
||||
box-shadow: none;
|
||||
color: $blue;
|
||||
display: flex;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
padding: $size-8;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -84,3 +84,7 @@ $box-link-hover-shadow: 0 0 0 1px $grey-light;
|
|||
// animations
|
||||
$speed: 150ms;
|
||||
$speed-slow: $speed * 2;
|
||||
|
||||
// Wizard
|
||||
$wizard-progress-bar-height: 6px;
|
||||
$wizard-progress-check-size: 16px;
|
||||
|
|
|
@ -15,5 +15,26 @@
|
|||
<h1 class="title is-5">
|
||||
<ICon @glyph={{glyph}} @size="21" /> {{headerText}}
|
||||
</h1>
|
||||
{{#if showProgress}}
|
||||
<ToolTip @verticalPosition="below" as |T|>
|
||||
<T.trigger @tabindex=false>
|
||||
<WizardProgress @currentFeatureProgress={{currentFeatureProgress}} @progressBar={{progressBar}} />
|
||||
</T.trigger>
|
||||
<T.content @class="tool-tip">
|
||||
<div class="box">
|
||||
{{#if currentTutorialProgress}}
|
||||
{{currentTutorialProgress.text}}
|
||||
{{else}}
|
||||
<p>{{capitalize currentFeatureProgress.feature}}</p>
|
||||
{{currentFeatureProgress.text}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</T.content>
|
||||
</ToolTip>
|
||||
{{else}}
|
||||
{{#if selectProgress}}
|
||||
<WizardProgress @noProgress={{true}} @progressBar={{selectProgress}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{yield}}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<div class="progress-container">
|
||||
{{#each progressBar as |bar|}}
|
||||
<div class="feature-progress-container">
|
||||
<span class="progress-bar">
|
||||
<span class="feature-progress" style={{bar.style}} {{! template-lint-disable }}></span>
|
||||
</span>
|
||||
{{#if bar.showIcon}}
|
||||
<ICon class="feature-check {{if bar.completed 'completed-check' 'incomplete-check'}}" @glyph="check-circle-fill" @size="16" @excludeIconClass={{true}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -12,9 +12,9 @@
|
|||
@class="wizard-details"
|
||||
>
|
||||
<button type="button" class="button next-feature-step" {{action onReset}}>
|
||||
Enable another auth method <ICon @glyph="loop" @size=13 @class="is-pulled-right" />
|
||||
Enable another auth method <ICon @glyph="loop" @size=13 />
|
||||
</button>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</WizardSection>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<WizardContent @headerText="Vault Web UI" @glyph="tour">
|
||||
<WizardContent @headerText="Vault Web UI" @glyph="tour" @selectProgress={{selectProgress}}>
|
||||
<h2 class="title is-6">
|
||||
Choosing where to go
|
||||
</h2>
|
||||
|
@ -36,6 +36,11 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
<button type="submit" class="button is-primary">Start</button>
|
||||
<span class="selection-summary">
|
||||
<button type="submit" class="button is-primary">Start</button>
|
||||
{{#if selectedFeatures}}
|
||||
<span class="time-estimate"><ICon @glyph="stopwatch" @class="has-text-grey auto-width is-paddingless is-flex-column"/>About {{estimatedTime}} minutes</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</form>
|
||||
</WizardContent>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
@class="wizard-details"
|
||||
>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</WizardSection>
|
||||
</WizardContent>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
Ready to move on?
|
||||
</h3>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</div>
|
||||
</WizardContent>
|
||||
|
|
|
@ -30,9 +30,9 @@
|
|||
</button>
|
||||
{{/if}}
|
||||
<button type="button" class="button next-feature-step" {{action onReset}}>
|
||||
Enable another secrets engine <ICon @glyph="loop" @size=13 @class="is-pulled-right" />
|
||||
Enable another secrets engine <ICon @glyph="loop" @size=13 />
|
||||
</button>
|
||||
<button type="button" class="button next-feature-step" {{action onDone}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</WizardSection>
|
|
@ -9,6 +9,6 @@
|
|||
</p>
|
||||
</WizardSection>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</WizardContent>
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
</WizardSection>
|
||||
<WizardSection>
|
||||
<button type="button" class="button next-feature-step" {{action onReset}}>
|
||||
Go Back <ICon @glyph="reply" @size=12 @class="is-pulled-right" />
|
||||
Go Back <ICon @glyph="reply" @size=12 />
|
||||
</button>
|
||||
<button type="button" class="button next-feature-step" {{action onAdvance}}>
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
|
||||
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
|
||||
</button>
|
||||
</WizardSection>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="{{size}}" height="{{size}}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 278 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="{{size}}" height="{{size}}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 210 B |
|
@ -119,6 +119,7 @@ module('Unit | Machine | tutorial-machine', function() {
|
|||
},
|
||||
actions: [
|
||||
'showTutorialWhenAuthenticated',
|
||||
'clearFeatureData',
|
||||
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' },
|
||||
{ type: 'render', level: 'feature', component: 'wizard/features-selection' },
|
||||
],
|
||||
|
|
|
@ -146,6 +146,7 @@ module('Unit | Service | wizard', function(hooks) {
|
|||
],
|
||||
storage: [
|
||||
{ key: STORAGE_KEYS.FEATURE_STATE, value: undefined },
|
||||
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined },
|
||||
{ key: STORAGE_KEYS.FEATURE_LIST, value: undefined },
|
||||
{ key: STORAGE_KEYS.COMPONENT_STATE, value: undefined },
|
||||
{ key: STORAGE_KEYS.TUTORIAL_STATE, value: 'active.select' },
|
||||
|
@ -155,6 +156,19 @@ module('Unit | Service | wizard', function(hooks) {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'clearFeatureData',
|
||||
args: [],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'currentMachine', value: null }, { prop: 'featureMachineHistory', value: null }],
|
||||
storage: [
|
||||
{ key: STORAGE_KEYS.FEATURE_STATE, value: undefined },
|
||||
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined },
|
||||
{ key: STORAGE_KEYS.FEATURE_LIST, value: undefined },
|
||||
{ key: STORAGE_KEYS.COMPONENT_STATE, value: undefined },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveState',
|
||||
args: [
|
||||
|
@ -194,6 +208,75 @@ module('Unit | Service | wizard', function(hooks) {
|
|||
props: [{ prop: 'currentState', value: 'login' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['idle'],
|
||||
properties: { featureList: ['policies', 'tools'] },
|
||||
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: null }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['idle'],
|
||||
properties: { featureList: ['policies', 'tools'] },
|
||||
storage: [],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['idle'],
|
||||
properties: { featureList: ['policies', 'tools'] },
|
||||
storage: [],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['idle'],
|
||||
properties: { featureMachineHistory: [], featureList: ['policies', 'tools'] },
|
||||
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
|
||||
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['idle'],
|
||||
properties: { featureMachineHistory: null, featureList: ['policies', 'tools'] },
|
||||
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: null }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['create'],
|
||||
properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] },
|
||||
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }],
|
||||
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'saveFeatureHistory',
|
||||
args: ['create'],
|
||||
properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] },
|
||||
storage: [
|
||||
{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] },
|
||||
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] },
|
||||
],
|
||||
expectedResults: {
|
||||
props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }],
|
||||
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'startFeature',
|
||||
args: [],
|
||||
|
|
Loading…
Reference in New Issue