UI: onboarding wizard progress bar (#5516)

Onboarding will now display your progress through the chosen tutorials
This commit is contained in:
madalynrose 2018-10-18 15:19:50 -04:00 committed by GitHub
parent 793de3b561
commit 7f430bba8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 521 additions and 103 deletions

View File

@ -22,6 +22,8 @@ const GLYPHS_WITH_SVG_TAG = [
'control-lock',
'edition-enterprise',
'edition-oss',
'check-plain',
'check-circle-fill',
];
export default Component.extend({

View File

@ -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');
},
},
});

View File

@ -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 [
{

View File

@ -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,
};

View File

@ -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: {

View File

@ -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;

View File

@ -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');
}
},

View File

@ -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;
}

View File

@ -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%;
}

View File

@ -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;

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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' },
],

View File

@ -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: [],