From 242df4f4d7c46e85fc793870fe32265d735670f9 Mon Sep 17 00:00:00 2001
From: Vishal Nayak
Date: Thu, 18 Oct 2018 07:53:12 -0700
Subject: [PATCH 01/50] Remove lookup check during alias removal (#5524)
* Possible fix for 5348
* Fix compilation
---
vault/identity_store_util.go | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/vault/identity_store_util.go b/vault/identity_store_util.go
index 2df22e9ea5..6fe3d0daa0 100644
--- a/vault/identity_store_util.go
+++ b/vault/identity_store_util.go
@@ -1072,14 +1072,7 @@ func (i *IdentityStore) deleteAliasesInEntityInTxn(txn *memdb.Txn, entity *ident
// Remove identity indices from aliases table for those that needs to
// be removed
for _, alias := range removeList {
- aliasToBeRemoved, err := i.MemDBAliasByIDInTxn(txn, alias.ID, false, false)
- if err != nil {
- return err
- }
- if aliasToBeRemoved == nil {
- return fmt.Errorf("alias was not indexed")
- }
- err = i.MemDBDeleteAliasByIDInTxn(txn, aliasToBeRemoved.ID, false)
+ err := i.MemDBDeleteAliasByIDInTxn(txn, alias.ID, false)
if err != nil {
return err
}
From 70d35c8d1dafd7b5cca1bdad0836abb687cec61e Mon Sep 17 00:00:00 2001
From: vishalnayak
Date: Thu, 18 Oct 2018 10:54:27 -0400
Subject: [PATCH 02/50] changelog++
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ef8eed4b4..7ef231cbf6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ BUG FIXES:
* secret/pki: Fix regression in 0.11.2+ causing the NotBefore value of
generated certificates to be set to the Unix epoch if the role value was not
set, instead of using the default of 30 seconds [GH-5481]
+ * identity: Remove lookup check during alias removal from entity [GH-5524]
## 0.11.3 (October 8th, 2018)
From 46f1a3e2f02d9364b63a8c1119a7481e81c126f7 Mon Sep 17 00:00:00 2001
From: Pouyan Azari
Date: Thu, 18 Oct 2018 17:51:07 +0200
Subject: [PATCH 03/50] Added link to list of all extensions for the ssh.
(#5542)
Added a link to the OpenSSH extension list, this is not documented anywhere in vault documentation website.
---
website/source/api/secret/ssh/index.html.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/website/source/api/secret/ssh/index.html.md b/website/source/api/secret/ssh/index.html.md
index 090ef7feef..b8fe4d6328 100644
--- a/website/source/api/secret/ssh/index.html.md
+++ b/website/source/api/secret/ssh/index.html.md
@@ -164,6 +164,10 @@ This endpoint creates or updates a named role.
- `allowed_extensions` `(string: "")` – Specifies a comma-separated list of
extensions that certificates can have when signed. To allow any critical
options, set this to an empty string. Will default to allowing any extensions.
+ For the list of extensions, take a look at
+ [sshd manual](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT)
+ `AUTHORIZED_KEYS FILE FORMAT` part. You should add a `permit-` before the
+ name of extension to allow it.
- `default_critical_options` `(map: "")` – Specifies a map of
critical options certificates should have if none are provided when signing.
From 51bb46302a34db4902f2d6e0b71230e00885a987 Mon Sep 17 00:00:00 2001
From: Jeff Mitchell
Date: Thu, 18 Oct 2018 13:32:42 -0400
Subject: [PATCH 04/50] Update some grammar
---
website/source/api/secret/ssh/index.html.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/website/source/api/secret/ssh/index.html.md b/website/source/api/secret/ssh/index.html.md
index b8fe4d6328..bdc0b110b8 100644
--- a/website/source/api/secret/ssh/index.html.md
+++ b/website/source/api/secret/ssh/index.html.md
@@ -163,10 +163,10 @@ This endpoint creates or updates a named role.
- `allowed_extensions` `(string: "")` – Specifies a comma-separated list of
extensions that certificates can have when signed. To allow any critical
- options, set this to an empty string. Will default to allowing any extensions.
- For the list of extensions, take a look at
- [sshd manual](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT)
- `AUTHORIZED_KEYS FILE FORMAT` part. You should add a `permit-` before the
+ options, set this to an empty string. Will default to allowing any
+ extensions. For the list of extensions, take a look at the [sshd
+ manual's](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT)
+ `AUTHORIZED_KEYS FILE FORMAT` section. You should add a `permit-` before the
name of extension to allow it.
- `default_critical_options` `(map: "")` – Specifies a map of
From e8d435340d8a051180b2a5810ccf75a5f34af254 Mon Sep 17 00:00:00 2001
From: dle-fr
Date: Thu, 18 Oct 2018 19:35:04 +0200
Subject: [PATCH 05/50] [5529] Mysql HA: table creation error using InnoDB and
utf8 charset (#5543)
---
physical/mysql/mysql.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/physical/mysql/mysql.go b/physical/mysql/mysql.go
index db078af5d2..de63d1307b 100644
--- a/physical/mysql/mysql.go
+++ b/physical/mysql/mysql.go
@@ -150,7 +150,7 @@ func NewMySQLBackend(conf map[string]string, logger log.Logger) (physical.Backen
// Create the required table if it doesn't exists.
if !lockTableExist {
create_query := "CREATE TABLE IF NOT EXISTS " + dbLockTable +
- " (node_job varchar(512), current_leader varchar(512), PRIMARY KEY (node_job))"
+ " (node_job varbinary(512), current_leader varbinary(512), PRIMARY KEY (node_job))"
if _, err := db.Exec(create_query); err != nil {
return nil, errwrap.Wrapf("failed to create mysql table: {{err}}", err)
}
From c15276c136b88517810655eddbe77b4553ea769c Mon Sep 17 00:00:00 2001
From: Jeff Mitchell
Date: Thu, 18 Oct 2018 13:36:27 -0400
Subject: [PATCH 06/50] changelog++
---
CHANGELOG.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ef231cbf6..793cd5195a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,10 +19,12 @@ BUG FIXES:
* agent: Fix potential hang during agent shutdown [GH-5026]
* core: Fix generate-root operations requiring empty `otp` to be provided
instead of an empty body [GH-5495]
+ * identity: Remove lookup check during alias removal from entity [GH-5524]
* secret/pki: Fix regression in 0.11.2+ causing the NotBefore value of
generated certificates to be set to the Unix epoch if the role value was not
set, instead of using the default of 30 seconds [GH-5481]
- * identity: Remove lookup check during alias removal from entity [GH-5524]
+ * storage/mysql: Use `varbinary` instead of `varchar` when creating HA tables
+ [GH-5529]
## 0.11.3 (October 8th, 2018)
From bad2f6d073055ed0a7ca12af0596ee2e684d7960 Mon Sep 17 00:00:00 2001
From: Evgeniy Zakharochkin
Date: Thu, 18 Oct 2018 20:41:14 +0300
Subject: [PATCH 07/50] ability to add NAS Identifier header to radius request
(#5465)
---
builtin/credential/radius/path_config.go | 14 ++++++++++++++
builtin/credential/radius/path_login.go | 3 +++
ui/app/models/auth-config/radius.js | 6 +++++-
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/builtin/credential/radius/path_config.go b/builtin/credential/radius/path_config.go
index 4a3f72e33b..be31877d71 100644
--- a/builtin/credential/radius/path_config.go
+++ b/builtin/credential/radius/path_config.go
@@ -46,6 +46,11 @@ func pathConfig(b *backend) *framework.Path {
Default: 10,
Description: "RADIUS NAS port field (default: 10)",
},
+ "nas_identifier": &framework.FieldSchema{
+ Type: framework.TypeString,
+ Default: "",
+ Description: "RADIUS NAS Identifier field (optional)",
+ },
},
ExistenceCheck: b.configExistenceCheck,
@@ -110,6 +115,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
"dial_timeout": cfg.DialTimeout,
"read_timeout": cfg.ReadTimeout,
"nas_port": cfg.NasPort,
+ "nas_identifier": cfg.NasIdentifier,
},
}
return resp, nil
@@ -190,6 +196,13 @@ func (b *backend) pathConfigCreateUpdate(ctx context.Context, req *logical.Reque
cfg.NasPort = d.Get("nas_port").(int)
}
+ nasIdentifier, ok := d.GetOk("nas_identifier")
+ if ok {
+ cfg.NasIdentifier = nasIdentifier.(string)
+ } else if req.Operation == logical.CreateOperation {
+ cfg.NasIdentifier = d.Get("nas_identifier").(string)
+ }
+
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
@@ -209,6 +222,7 @@ type ConfigEntry struct {
DialTimeout int `json:"dial_timeout" structs:"dial_timeout" mapstructure:"dial_timeout"`
ReadTimeout int `json:"read_timeout" structs:"read_timeout" mapstructure:"read_timeout"`
NasPort int `json:"nas_port" structs:"nas_port" mapstructure:"nas_port"`
+ NasIdentifier string `json:"nas_identifier" structs:"nas_identifier" mapstructure:"nas_identifier"`
}
const pathConfigHelpSyn = `
diff --git a/builtin/credential/radius/path_login.go b/builtin/credential/radius/path_login.go
index ef0c185d88..2cf2fa2f61 100644
--- a/builtin/credential/radius/path_login.go
+++ b/builtin/credential/radius/path_login.go
@@ -144,6 +144,9 @@ func (b *backend) RadiusLogin(ctx context.Context, req *logical.Request, usernam
packet := radius.New(radius.CodeAccessRequest, []byte(cfg.Secret))
UserName_SetString(packet, username)
UserPassword_SetString(packet, password)
+ if cfg.NasIdentifier != "" {
+ NASIdentifier_AddString(packet, cfg.NasIdentifier)
+ }
packet.Add(5, radius.NewInteger(uint32(cfg.NasPort)))
client := radius.Client{
diff --git a/ui/app/models/auth-config/radius.js b/ui/app/models/auth-config/radius.js
index c47351f3d5..d88088a3f4 100644
--- a/ui/app/models/auth-config/radius.js
+++ b/ui/app/models/auth-config/radius.js
@@ -27,13 +27,17 @@ export default AuthConfig.extend({
label: 'NAS Port',
}),
+ nasIdentifier: attr('string', {
+ label: 'NAS Identifier',
+ }),
+
fieldGroups: computed(function() {
const groups = [
{
default: ['host', 'secret'],
},
{
- 'RADIUS Options': ['port', 'nasPort', 'dialTimeout', 'unregisteredUserPolicies'],
+ 'RADIUS Options': ['port', 'nasPort', 'nasIdentifier', 'dialTimeout', 'unregisteredUserPolicies'],
},
];
return fieldToAttrs(this, groups);
From d725421bc29287afb338314a4d2b473b5247385e Mon Sep 17 00:00:00 2001
From: Jeff Mitchell
Date: Thu, 18 Oct 2018 13:41:54 -0400
Subject: [PATCH 08/50] changelog++
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 793cd5195a..4964f6c476 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ IMPROVEMENTS:
* secret/database: Allow Cassandra user to be non-superuser so long as it has
role creation permissions [GH-5402]
+ * secret/radius: Allow setting the NAS Identifier value in the generated
+ packet [GH-5465]
BUG FIXES:
From 34c815b37a3c187b93a04fcf0b9050a4c7081b71 Mon Sep 17 00:00:00 2001
From: madalynrose
Date: Thu, 18 Oct 2018 15:19:50 -0400
Subject: [PATCH 09/50] UI: onboarding wizard progress bar (#5516)
Onboarding will now display your progress through the chosen tutorials
---
ui/app/components/i-con.js | 2 +
ui/app/components/wizard-content.js | 89 ++++++-
.../components/wizard/features-selection.js | 21 +-
ui/app/helpers/wizard-constants.js | 27 +++
ui/app/machines/tutorial-machine.js | 2 +-
ui/app/routes/vault/cluster/tools/tool.js | 7 +-
ui/app/services/wizard.js | 217 +++++++++++-------
ui/app/styles/components/ui-wizard.scss | 92 +++++++-
ui/app/styles/core/buttons.scss | 13 +-
ui/app/styles/utils/_bulma_variables.scss | 4 +
.../templates/components/wizard-content.hbs | 21 ++
.../templates/components/wizard-progress.hbs | 12 +
.../components/wizard/auth-details.hbs | 4 +-
.../components/wizard/features-selection.hbs | 9 +-
.../components/wizard/policies-others.hbs | 2 +-
.../components/wizard/replication-details.hbs | 2 +-
.../components/wizard/secrets-display.hbs | 4 +-
.../components/wizard/tools-unwrapped.hbs | 2 +-
.../components/wizard/tutorial-error.hbs | 4 +-
.../templates/svg/icons/check-circle-fill.hbs | 3 +
ui/app/templates/svg/icons/check-plain.hbs | 3 +
.../unit/machines/tutorial-machine-test.js | 1 +
ui/tests/unit/services/wizard-test.js | 83 +++++++
23 files changed, 521 insertions(+), 103 deletions(-)
create mode 100644 ui/app/templates/components/wizard-progress.hbs
create mode 100644 ui/app/templates/svg/icons/check-circle-fill.hbs
create mode 100644 ui/app/templates/svg/icons/check-plain.hbs
diff --git a/ui/app/components/i-con.js b/ui/app/components/i-con.js
index e57f1fe447..4482fe1c84 100644
--- a/ui/app/components/i-con.js
+++ b/ui/app/components/i-con.js
@@ -22,6 +22,8 @@ const GLYPHS_WITH_SVG_TAG = [
'control-lock',
'edition-enterprise',
'edition-oss',
+ 'check-plain',
+ 'check-circle-fill',
];
export default Component.extend({
diff --git a/ui/app/components/wizard-content.js b/ui/app/components/wizard-content.js
index 7d1112fd88..bf2e036212 100644
--- a/ui/app/components/wizard-content.js
+++ b/ui/app/components/wizard-content.js
@@ -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');
},
},
});
diff --git a/ui/app/components/wizard/features-selection.js b/ui/app/components/wizard/features-selection.js
index 6e6f7f590f..f43bd4b456 100644
--- a/ui/app/components/wizard/features-selection.js
+++ b/ui/app/components/wizard/features-selection.js
@@ -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 [
{
diff --git a/ui/app/helpers/wizard-constants.js b/ui/app/helpers/wizard-constants.js
index f3b1e13b23..93b070887f 100644
--- a/ui/app/helpers/wizard-constants.js
+++ b/ui/app/helpers/wizard-constants.js
@@ -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,
};
diff --git a/ui/app/machines/tutorial-machine.js b/ui/app/machines/tutorial-machine.js
index 8ed5ca5adf..9c66d5e9c1 100644
--- a/ui/app/machines/tutorial-machine.js
+++ b/ui/app/machines/tutorial-machine.js
@@ -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: {
diff --git a/ui/app/routes/vault/cluster/tools/tool.js b/ui/app/routes/vault/cluster/tools/tool.js
index 98b0f6d72a..2d7fb47782 100644
--- a/ui/app/routes/vault/cluster/tools/tool.js
+++ b/ui/app/routes/vault/cluster/tools/tool.js
@@ -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;
diff --git a/ui/app/services/wizard.js b/ui/app/services/wizard.js
index 4b892f9857..56e96ceefe 100644
--- a/ui/app/services/wizard.js
+++ b/ui/app/services/wizard.js
@@ -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');
}
},
diff --git a/ui/app/styles/components/ui-wizard.scss b/ui/app/styles/components/ui-wizard.scss
index 7a5a8c7c86..8648f8bd78 100644
--- a/ui/app/styles/components/ui-wizard.scss
+++ b/ui/app/styles/components/ui-wizard.scss
@@ -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;
+}
diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss
index d317eaa902..f802a935f6 100644
--- a/ui/app/styles/core/buttons.scss
+++ b/ui/app/styles/core/buttons.scss
@@ -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%;
}
diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss
index af61e9b863..16852bf611 100644
--- a/ui/app/styles/utils/_bulma_variables.scss
+++ b/ui/app/styles/utils/_bulma_variables.scss
@@ -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;
diff --git a/ui/app/templates/components/wizard-content.hbs b/ui/app/templates/components/wizard-content.hbs
index d69cb6b750..47001a2a44 100644
--- a/ui/app/templates/components/wizard-content.hbs
+++ b/ui/app/templates/components/wizard-content.hbs
@@ -15,5 +15,26 @@
{{headerText}}
+ {{#if showProgress}}
+
+
+
+
+
+
+ {{#if currentTutorialProgress}}
+ {{currentTutorialProgress.text}}
+ {{else}}
+
{{capitalize currentFeatureProgress.feature}}
+ {{currentFeatureProgress.text}}
+ {{/if}}
+
+
+
+ {{else}}
+ {{#if selectProgress}}
+
+ {{/if}}
+ {{/if}}
{{yield}}
diff --git a/ui/app/templates/components/wizard-progress.hbs b/ui/app/templates/components/wizard-progress.hbs
new file mode 100644
index 0000000000..fcef853292
--- /dev/null
+++ b/ui/app/templates/components/wizard-progress.hbs
@@ -0,0 +1,12 @@
+
+ {{#each progressBar as |bar|}}
+
+
+
+
+ {{#if bar.showIcon}}
+
+ {{/if}}
+
+ {{/each}}
+
diff --git a/ui/app/templates/components/wizard/auth-details.hbs b/ui/app/templates/components/wizard/auth-details.hbs
index 955280f2ae..1d22d91c64 100644
--- a/ui/app/templates/components/wizard/auth-details.hbs
+++ b/ui/app/templates/components/wizard/auth-details.hbs
@@ -12,9 +12,9 @@
@class="wizard-details"
>
diff --git a/ui/app/templates/components/wizard/features-selection.hbs b/ui/app/templates/components/wizard/features-selection.hbs
index db99f44b36..c3b4356422 100644
--- a/ui/app/templates/components/wizard/features-selection.hbs
+++ b/ui/app/templates/components/wizard/features-selection.hbs
@@ -1,4 +1,4 @@
-
+
Choosing where to go
@@ -36,6 +36,11 @@
{{/if}}
{{/each}}
-
+
+
+ {{#if selectedFeatures}}
+ About {{estimatedTime}} minutes
+ {{/if}}
+
diff --git a/ui/app/templates/components/wizard/policies-others.hbs b/ui/app/templates/components/wizard/policies-others.hbs
index 25eac94450..66fbeb9a5e 100644
--- a/ui/app/templates/components/wizard/policies-others.hbs
+++ b/ui/app/templates/components/wizard/policies-others.hbs
@@ -13,7 +13,7 @@
@class="wizard-details"
>
diff --git a/ui/app/templates/components/wizard/replication-details.hbs b/ui/app/templates/components/wizard/replication-details.hbs
index 6baf51649f..ffff094549 100644
--- a/ui/app/templates/components/wizard/replication-details.hbs
+++ b/ui/app/templates/components/wizard/replication-details.hbs
@@ -13,7 +13,7 @@
Ready to move on?
diff --git a/ui/app/templates/components/wizard/secrets-display.hbs b/ui/app/templates/components/wizard/secrets-display.hbs
index c69c7cc309..371d14ac2a 100644
--- a/ui/app/templates/components/wizard/secrets-display.hbs
+++ b/ui/app/templates/components/wizard/secrets-display.hbs
@@ -30,9 +30,9 @@
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/wizard/tools-unwrapped.hbs b/ui/app/templates/components/wizard/tools-unwrapped.hbs
index b605be3421..aefa155e2a 100644
--- a/ui/app/templates/components/wizard/tools-unwrapped.hbs
+++ b/ui/app/templates/components/wizard/tools-unwrapped.hbs
@@ -9,6 +9,6 @@
diff --git a/ui/app/templates/components/wizard/tutorial-error.hbs b/ui/app/templates/components/wizard/tutorial-error.hbs
index 5e76e9c249..f15d898152 100644
--- a/ui/app/templates/components/wizard/tutorial-error.hbs
+++ b/ui/app/templates/components/wizard/tutorial-error.hbs
@@ -7,9 +7,9 @@
diff --git a/ui/app/templates/svg/icons/check-circle-fill.hbs b/ui/app/templates/svg/icons/check-circle-fill.hbs
new file mode 100644
index 0000000000..77922e3055
--- /dev/null
+++ b/ui/app/templates/svg/icons/check-circle-fill.hbs
@@ -0,0 +1,3 @@
+
diff --git a/ui/app/templates/svg/icons/check-plain.hbs b/ui/app/templates/svg/icons/check-plain.hbs
new file mode 100644
index 0000000000..1373b01c81
--- /dev/null
+++ b/ui/app/templates/svg/icons/check-plain.hbs
@@ -0,0 +1,3 @@
+
diff --git a/ui/tests/unit/machines/tutorial-machine-test.js b/ui/tests/unit/machines/tutorial-machine-test.js
index aa4e33c469..f589d23eb5 100644
--- a/ui/tests/unit/machines/tutorial-machine-test.js
+++ b/ui/tests/unit/machines/tutorial-machine-test.js
@@ -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' },
],
diff --git a/ui/tests/unit/services/wizard-test.js b/ui/tests/unit/services/wizard-test.js
index a78e8ba5f1..a3734d7e81 100644
--- a/ui/tests/unit/services/wizard-test.js
+++ b/ui/tests/unit/services/wizard-test.js
@@ -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: [],
From 11d671fab1aa5886aaa09b914f123735a50ec290 Mon Sep 17 00:00:00 2001
From: madalynrose
Date: Thu, 18 Oct 2018 15:24:59 -0400
Subject: [PATCH 10/50] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4964f6c476..269f8dc057 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ IMPROVEMENTS:
role creation permissions [GH-5402]
* secret/radius: Allow setting the NAS Identifier value in the generated
packet [GH-5465]
+ * ui: Allow viewing and updating Vault license via the UI
+ * ui: Onboarding will now display your progress through the chosen tutorials
BUG FIXES:
From 89c6ab6dd6dee1c62ce1a09b59cef5700a3a5e9d Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Wed, 3 Oct 2018 23:31:23 -0500
Subject: [PATCH 11/50] don't leave new lines on the end of files
---
ui/.eslintrc.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
index ea6c16123d..e2ea8c32ec 100644
--- a/ui/.eslintrc.js
+++ b/ui/.eslintrc.js
@@ -15,6 +15,7 @@ module.exports = {
},
rules: {
'no-unused-vars': ['error', { ignoreRestSiblings: true }],
+ 'eol-last': 'never',
},
globals: {
TextEncoderLite: true,
From cae9f6fffe804ce8ff68706531f8566bf8773402 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Wed, 3 Oct 2018 23:32:55 -0500
Subject: [PATCH 12/50] new model things for secret-v2 and secret-v2 versions:
get list, queryRecord, and version find working
---
ui/app/adapters/secret-v2-version.js | 23 +++++++++
ui/app/adapters/secret-v2.js | 47 +++++++++++--------
ui/app/models/secret-v2-version.js | 8 ++++
ui/app/models/secret-v2.js | 14 +++++-
.../cluster/secrets/backend/secret-edit.js | 9 +++-
ui/app/serializers/secret-v2-version.js | 25 ++++++++++
ui/app/serializers/secret-v2.js | 41 ++++++++++++++--
7 files changed, 142 insertions(+), 25 deletions(-)
create mode 100644 ui/app/adapters/secret-v2-version.js
create mode 100644 ui/app/models/secret-v2-version.js
create mode 100644 ui/app/serializers/secret-v2-version.js
diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js
new file mode 100644
index 0000000000..3927748bb4
--- /dev/null
+++ b/ui/app/adapters/secret-v2-version.js
@@ -0,0 +1,23 @@
+import { isEmpty } from '@ember/utils';
+import ApplicationAdapter from './application';
+
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+ _url(backend, id) {
+ let url = `${this.buildURL()}/${backend}/data/`;
+ if (!isEmpty(id)) {
+ url = url + id;
+ }
+ return url;
+ },
+
+ urlForFindRecord(id) {
+ let [backend, path, version] = JSON.parse(id);
+ return this._url(backend, path) + `?version=${version}`;
+ },
+
+ deleteRecord(store, type, snapshot) {
+ // use adapterOptions to determine if it's delete or destroy for the version
+ return this._super(...arguments);
+ },
+});
diff --git a/ui/app/adapters/secret-v2.js b/ui/app/adapters/secret-v2.js
index d76fb2438c..aa12c2169f 100644
--- a/ui/app/adapters/secret-v2.js
+++ b/ui/app/adapters/secret-v2.js
@@ -1,34 +1,41 @@
+/* eslint-disable */
import { isEmpty } from '@ember/utils';
-import SecretAdapter from './secret';
+import ApplicationAdapter from './application';
-export default SecretAdapter.extend({
- createOrUpdate(store, type, snapshot) {
- const serializer = store.serializerFor(type.modelName);
- const data = serializer.serialize(snapshot);
- const { id } = snapshot;
-
- return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', {
- data: { data },
- });
- },
-
- urlForSecret(backend, id, infix = 'data') {
- let url = `${this.buildURL()}/${backend}/${infix}/`;
+export default ApplicationAdapter.extend({
+ namespace: 'v1',
+ _url(backend, id) {
+ let url = `${this.buildURL()}/${backend}/metadata/`;
if (!isEmpty(id)) {
url = url + id;
}
return url;
},
- fetchByQuery(query, methodCall) {
+ // we override query here because the query object has a bunch of client-side
+ // concerns and we only want to send "list" to the server
+ query(store, type, query) {
+ let { backend, id } = query;
+ return this.ajax(this._url(backend, id), 'GET', { data: { list: true } });
+ },
+
+ urlForQueryRecord(query) {
let { id, backend } = query;
- let args = [backend, id];
- if (methodCall === 'query') {
- args.push('metadata');
- }
- return this.ajax(this.urlForSecret(...args), 'GET', this.optionsForQuery(id, methodCall)).then(resp => {
+ return this._url(backend) + id;
+ },
+
+ queryRecord(store, type, query) {
+ let { backend, id } = query;
+ return this._super(...arguments).then(resp => {
resp.id = id;
+ resp.backend = backend;
return resp;
});
},
+
+ urlForDeleteRecord(store, type, snapshot) {
+ let backend = snapshot.belongsTo('secret-engine', { id: true });
+ let { id } = snapshot;
+ return this.urlForQueryRecord({ id, backend });
+ },
});
diff --git a/ui/app/models/secret-v2-version.js b/ui/app/models/secret-v2-version.js
new file mode 100644
index 0000000000..3c7ca467ac
--- /dev/null
+++ b/ui/app/models/secret-v2-version.js
@@ -0,0 +1,8 @@
+import Secret from './secret';
+import DS from 'ember-data';
+
+const { attr } = DS;
+
+export default Secret.extend({
+ version: attr('number'),
+});
diff --git a/ui/app/models/secret-v2.js b/ui/app/models/secret-v2.js
index fcbf908caf..cfaef24fd2 100644
--- a/ui/app/models/secret-v2.js
+++ b/ui/app/models/secret-v2.js
@@ -1,3 +1,15 @@
import Secret from './secret';
+import DS from 'ember-data';
-export default Secret.extend();
+const { attr, hasMany, belongsTo, Model } = DS;
+
+export default Model.extend({
+ engine: belongsTo('secret-engine'),
+ versions: hasMany('secret-v2-version', { async: false }),
+ createdTime: attr(),
+ updatedTime: attr(),
+ currentVersion: attr('number'),
+ oldestVersion: attr('number'),
+ maxVersions: attr('number'),
+ casRequired: attr('boolean'),
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
index fb0cbb64ac..67aec2bf6c 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
@@ -72,7 +72,14 @@ export default Route.extend(UnloadModelRoute, {
secret = secret.replace('cert/', '');
}
return hash({
- secret: this.store.queryRecord(modelType, { id: secret, backend }),
+ secret: this.store.queryRecord(modelType, { id: secret, backend }).then(resp => {
+ if (modelType === 'secret-v2') {
+ // TODO, find by query param to enable viewing versions
+ let version = resp.versions.findBy('version', resp.currentVersion);
+ version.reload();
+ }
+ return resp;
+ }),
capabilities: this.capabilities(secret),
});
},
diff --git a/ui/app/serializers/secret-v2-version.js b/ui/app/serializers/secret-v2-version.js
new file mode 100644
index 0000000000..31ffc9c902
--- /dev/null
+++ b/ui/app/serializers/secret-v2-version.js
@@ -0,0 +1,25 @@
+import { get } from '@ember/object';
+import ApplicationSerializer from './application';
+
+export default ApplicationSerializer.extend({
+ secretDataPath: 'data.data',
+ normalizeItems(payload, requestType) {
+ let path = this.secretDataPath;
+ // move response that is the contents of the secret from the dataPath
+ // to `secret_data` so it will be `secretData` in the model
+ payload.secret_data = get(payload, path);
+ payload = Object.assign({}, payload, payload.data.metadata);
+ delete payload.data;
+ // return the payload if it's expecting a single object or wrap
+ // it as an array if not
+ return payload;
+ },
+ serialize(snapshot) {
+ return {
+ data: snapshot.attr('secretData'),
+ options: {
+ cas: snapshot.attr('currentVerion'),
+ },
+ };
+ },
+});
diff --git a/ui/app/serializers/secret-v2.js b/ui/app/serializers/secret-v2.js
index 9a43c82b08..4dbbfd305d 100644
--- a/ui/app/serializers/secret-v2.js
+++ b/ui/app/serializers/secret-v2.js
@@ -1,5 +1,40 @@
-import SecretSerializer from './secret';
+import ApplicationSerializer from './application';
+import DS from 'ember-data';
-export default SecretSerializer.extend({
- secretDataPath: 'data.data',
+export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
+ attrs: {
+ versions: { embedded: 'always' },
+ },
+ secretDataPath: 'data',
+ normalizeItems(payload, requestType) {
+ if (payload.data.keys && Array.isArray(payload.data.keys)) {
+ // if we have data.keys, it's a list of ids, so we map over that
+ // and create objects with id's
+ return payload.data.keys.map(secret => {
+ // secrets don't have an id in the response, so we need to concat the full
+ // path of the secret here - the id in the payload is added
+ // in the adapter after making the request
+ let fullSecretPath = payload.id ? payload.id + secret : secret;
+
+ // if there is no path, it's a "top level" secret, so add
+ // a unicode space for the id
+ // https://github.com/hashicorp/vault/issues/3348
+ if (!fullSecretPath) {
+ fullSecretPath = '\u0020';
+ }
+ return { id: fullSecretPath };
+ });
+ }
+ if (payload.data.versions) {
+ payload.data.versions = Object.keys(payload.data.versions).map(version => {
+ let body = payload.data.versions[version];
+ body.version = version;
+ body.id = JSON.stringify([payload.backend, payload.id, version]);
+ return body;
+ });
+ console.log(payload);
+ }
+ payload.data.id = payload.id;
+ return requestType === 'queryRecord' ? payload.data : [payload.data];
+ },
});
From 3bd0250fc803e96e595ca96f0215b7dc88d3cf59 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Thu, 4 Oct 2018 01:06:01 -0500
Subject: [PATCH 13/50] get rid of v2 incomplete warning
---
ui/app/routes/vault/cluster/secrets/backend.js | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/ui/app/routes/vault/cluster/secrets/backend.js b/ui/app/routes/vault/cluster/secrets/backend.js
index c7769c9f5a..b7964a59b1 100644
--- a/ui/app/routes/vault/cluster/secrets/backend.js
+++ b/ui/app/routes/vault/cluster/secrets/backend.js
@@ -16,16 +16,8 @@ export default Route.extend({
},
afterModel(model, transition) {
- let target = transition.targetName;
let path = model && model.get('path');
- let type = model && model.get('type');
- if (type === 'kv' && model.get('options.version') === 2) {
- this.get('flashMessages').stickyInfo(
- `"${path}" is a newer version of the KV backend. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.`
- );
- }
-
- if (target === this.routeName) {
+ if (transition.targetName === this.routeName) {
return this.replaceWith('vault.cluster.secrets.backend.list-root', path);
}
},
From 7e09c692b4489ab503c55e58a49d42c8195b8397 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 21:58:54 -0500
Subject: [PATCH 14/50] move to es5 getter usage for csp service
---
ui/app/services/csp-event.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/ui/app/services/csp-event.js b/ui/app/services/csp-event.js
index 7cfceae0f4..df1a5779d2 100644
--- a/ui/app/services/csp-event.js
+++ b/ui/app/services/csp-event.js
@@ -12,19 +12,19 @@ export default Service.extend({
connectionViolations: filterBy('events', 'violatedDirective', 'connect-src'),
attach() {
- this.get('monitor').perform();
+ this.monitor.perform();
},
remove() {
- this.get('monitor').cancelAll();
+ this.monitor.cancelAll();
},
monitor: task(function*() {
- this.get('events').clear();
+ this.events.clear();
while (true) {
let event = yield waitForEvent(window.document, 'securitypolicyviolation');
- this.get('events').addObject(event);
+ this.events.addObject(event);
}
}),
});
From f31b7f9ade33bd7cff78c7a2b40c7593a75459f3 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 21:59:35 -0500
Subject: [PATCH 15/50] user correct rule for eol
---
ui/.eslintrc.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
index e2ea8c32ec..ea6c16123d 100644
--- a/ui/.eslintrc.js
+++ b/ui/.eslintrc.js
@@ -15,7 +15,6 @@ module.exports = {
},
rules: {
'no-unused-vars': ['error', { ignoreRestSiblings: true }],
- 'eol-last': 'never',
},
globals: {
TextEncoderLite: true,
From 14a410ecf40e1ee1170324b47a2e363c1ce569c1 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 22:01:02 -0500
Subject: [PATCH 16/50] fix navigate component
---
ui/app/components/navigate-input.js | 9 ++++----
.../templates/components/navigate-input.hbs | 21 ++++++++++---------
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/ui/app/components/navigate-input.js b/ui/app/components/navigate-input.js
index f642b6d1b1..cf774ecf98 100644
--- a/ui/app/components/navigate-input.js
+++ b/ui/app/components/navigate-input.js
@@ -163,24 +163,25 @@ export default Component.extend(FocusOnInsertMixin, {
},
actions: {
- handleInput: function(event) {
- var filter = event.target.value;
+ handleInput: function(filter) {
this.get('filterDidChange')(filter);
debounce(this, 'filterUpdated', filter, 200);
},
setFilterFocused: function(isFocused) {
this.get('filterFocusDidChange')(isFocused);
+ console.log(isFocused);
},
- handleKeyPress: function(val, event) {
+ handleKeyPress: function(event) {
if (event.keyCode === keys.TAB) {
this.onTab(event);
}
},
- handleKeyUp: function(val, event) {
+ handleKeyUp: function(event) {
var keyCode = event.keyCode;
+ let val = event.target.value;
if (keyCode === keys.ENTER) {
this.onEnter(val);
}
diff --git a/ui/app/templates/components/navigate-input.hbs b/ui/app/templates/components/navigate-input.hbs
index e1d1874bef..3b7adf392e 100644
--- a/ui/app/templates/components/navigate-input.hbs
+++ b/ui/app/templates/components/navigate-input.hbs
@@ -1,16 +1,17 @@
- {{input
- value=filter
- placeholder=(or placeholder "Filter keys")
+
{{i-con glyph="ios-search-strong" class="is-left has-text-grey" size=18}}
From 7e295b9b7732285d8144e03ff5d3bdffa20e858a Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 22:02:23 -0500
Subject: [PATCH 17/50] finish v2 model layer and add some unit tests for
adapters
---
ui/app/adapters/secret-v2-version.js | 34 +++++++-
ui/app/adapters/secret-v2.js | 21 +++--
ui/app/models/key-mixin.js | 31 +++++---
ui/app/models/secret-v2-version.js | 6 +-
ui/app/models/secret-v2.js | 6 +-
ui/app/serializers/secret-v2-version.js | 15 ++--
ui/app/serializers/secret-v2.js | 3 +-
ui/tests/unit/adapters/secret-v2-test.js | 76 ++++++++++++++----
.../unit/adapters/secret-v2-version-test.js | 79 +++++++++++++++++++
9 files changed, 227 insertions(+), 44 deletions(-)
create mode 100644 ui/tests/unit/adapters/secret-v2-version-test.js
diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js
index 3927748bb4..daae091768 100644
--- a/ui/app/adapters/secret-v2-version.js
+++ b/ui/app/adapters/secret-v2-version.js
@@ -1,10 +1,11 @@
+/* eslint-disable */
import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
namespace: 'v1',
- _url(backend, id) {
- let url = `${this.buildURL()}/${backend}/data/`;
+ _url(backend, id, infix = 'data') {
+ let url = `${this.buildURL()}/${backend}/${infix}/`;
if (!isEmpty(id)) {
url = url + id;
}
@@ -16,8 +17,37 @@ export default ApplicationAdapter.extend({
return this._url(backend, path) + `?version=${version}`;
},
+ urlForCreateRecord(modelName, snapshot) {
+ let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
+ let path = snapshot.attr('path');
+ return this._url(backend, path);
+ },
+
+ createRecord(store, modelName, snapshot) {
+ let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
+ let path = snapshot.attr('path');
+ return this._super(...arguments).then(resp => {
+ resp.id = JSON.stringify([backend, path, resp.version]);
+ return resp;
+ });
+ },
+
+ urlForUpdateRecord(id) {
+ let [backend, path] = JSON.parse(id);
+ return this._url(backend, path);
+ },
+
deleteRecord(store, type, snapshot) {
// use adapterOptions to determine if it's delete or destroy for the version
+ // deleteType should be 'delete', 'destroy', 'undelete'
+ let infix = snapshot.adapterOptions.deleteType;
+ let [backend, path, version] = JSON.parse(snapshot.id);
+
+ return this.ajax(this._url(backend, path, infix), 'POST', { data: { versions: [version] } });
+ },
+
+ handleResponse(/*status, headers, payload, requestData*/) {
+ // the body of the 404 will have some relevant information
return this._super(...arguments);
},
});
diff --git a/ui/app/adapters/secret-v2.js b/ui/app/adapters/secret-v2.js
index aa12c2169f..4b7937ffac 100644
--- a/ui/app/adapters/secret-v2.js
+++ b/ui/app/adapters/secret-v2.js
@@ -16,26 +16,35 @@ export default ApplicationAdapter.extend({
// concerns and we only want to send "list" to the server
query(store, type, query) {
let { backend, id } = query;
- return this.ajax(this._url(backend, id), 'GET', { data: { list: true } });
+ return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then(resp => {
+ resp.id = id;
+ return resp;
+ });
},
urlForQueryRecord(query) {
let { id, backend } = query;
- return this._url(backend) + id;
+ return this._url(backend, id);
},
queryRecord(store, type, query) {
let { backend, id } = query;
- return this._super(...arguments).then(resp => {
+ return this.ajax(this._url(backend, id), 'GET').then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
},
- urlForDeleteRecord(store, type, snapshot) {
- let backend = snapshot.belongsTo('secret-engine', { id: true });
+ urlForUpdateRecord(store, type, snapshot) {
+ let backend = snapshot.belongsTo('engine', { id: true });
let { id } = snapshot;
- return this.urlForQueryRecord({ id, backend });
+ return this._url(backend, id);
+ },
+
+ urlForDeleteRecord(store, type, snapshot) {
+ let backend = snapshot.belongsTo('engine', { id: true });
+ let { id } = snapshot;
+ return this._url(backend, id);
},
});
diff --git a/ui/app/models/key-mixin.js b/ui/app/models/key-mixin.js
index 8b505ece50..ec0c1dc035 100644
--- a/ui/app/models/key-mixin.js
+++ b/ui/app/models/key-mixin.js
@@ -3,6 +3,9 @@ import Mixin from '@ember/object/mixin';
import utils from '../lib/key-utils';
export default Mixin.create({
+ // what attribute has the path for the key
+ // will.be 'path' for v2 or 'id' v1
+ pathAttr: 'id',
flags: null,
initialParentKey: null,
@@ -11,33 +14,39 @@ export default Mixin.create({
return this.get('initialParentKey') != null;
}),
- isFolder: computed('id', function() {
- return utils.keyIsFolder(this.get('id'));
+ pathVal() {
+ return this.get(this.pathAttr);
+ },
+
+ // rather than using defineProperty for all of these,
+ // we're just going to hardcode the known keys for the path ('id' and 'path')
+ isFolder: computed('id', 'path', function() {
+ return utils.keyIsFolder(this.pathVal());
}),
- keyParts: computed('id', function() {
- return utils.keyPartsForKey(this.get('id'));
+ keyParts: computed('id', 'path', function() {
+ return utils.keyPartsForKey(this.pathVal());
}),
- parentKey: computed('id', 'isCreating', {
+ parentKey: computed('id', 'path', 'isCreating', {
get: function() {
- return this.get('isCreating') ? this.get('initialParentKey') : utils.parentKeyForKey(this.get('id'));
+ return this.isCreating ? this.initialParentKey : utils.parentKeyForKey(this.pathVal());
},
set: function(_, value) {
return value;
},
}),
- keyWithoutParent: computed('id', 'parentKey', {
+ keyWithoutParent: computed('id', 'path', 'parentKey', {
get: function() {
- var key = this.get('id');
- return key ? key.replace(this.get('parentKey'), '') : null;
+ var key = this.pathVal();
+ return key ? key.replace(this.parentKey, '') : null;
},
set: function(_, value) {
if (value && value.trim()) {
- this.set('id', this.get('parentKey') + value);
+ this.set(this.pathAttr, this.parentKey + value);
} else {
- this.set('id', null);
+ this.set(this.pathAttr, null);
}
return value;
},
diff --git a/ui/app/models/secret-v2-version.js b/ui/app/models/secret-v2-version.js
index 3c7ca467ac..19665fc063 100644
--- a/ui/app/models/secret-v2-version.js
+++ b/ui/app/models/secret-v2-version.js
@@ -1,8 +1,12 @@
import Secret from './secret';
import DS from 'ember-data';
-const { attr } = DS;
+const { attr, belongsTo } = DS;
export default Secret.extend({
+ pathAttr: 'path',
version: attr('number'),
+ secret: belongsTo('secret-v2'),
+ path: attr('string'),
+ currentVersion: attr('number'),
});
diff --git a/ui/app/models/secret-v2.js b/ui/app/models/secret-v2.js
index cfaef24fd2..2f2924a267 100644
--- a/ui/app/models/secret-v2.js
+++ b/ui/app/models/secret-v2.js
@@ -1,15 +1,17 @@
-import Secret from './secret';
import DS from 'ember-data';
+import { match } from '@ember/object/computed';
const { attr, hasMany, belongsTo, Model } = DS;
export default Model.extend({
engine: belongsTo('secret-engine'),
- versions: hasMany('secret-v2-version', { async: false }),
+ versions: hasMany('secret-v2-version', { async: false, inverse: null }),
+ selectedVersion: belongsTo('secret-v2-version', { inverse: 'secret' }),
createdTime: attr(),
updatedTime: attr(),
currentVersion: attr('number'),
oldestVersion: attr('number'),
maxVersions: attr('number'),
casRequired: attr('boolean'),
+ isFolder: match('id', /\/$/),
});
diff --git a/ui/app/serializers/secret-v2-version.js b/ui/app/serializers/secret-v2-version.js
index 31ffc9c902..68b3354b0c 100644
--- a/ui/app/serializers/secret-v2-version.js
+++ b/ui/app/serializers/secret-v2-version.js
@@ -3,23 +3,28 @@ import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
secretDataPath: 'data.data',
- normalizeItems(payload, requestType) {
+ normalizeItems(payload) {
let path = this.secretDataPath;
// move response that is the contents of the secret from the dataPath
// to `secret_data` so it will be `secretData` in the model
payload.secret_data = get(payload, path);
payload = Object.assign({}, payload, payload.data.metadata);
delete payload.data;
+ payload.path = payload.id;
// return the payload if it's expecting a single object or wrap
// it as an array if not
return payload;
},
serialize(snapshot) {
- return {
+ let data = {
data: snapshot.attr('secretData'),
- options: {
- cas: snapshot.attr('currentVerion'),
- },
};
+ if (snapshot.attr('currentVersion')) {
+ data.options = {
+ cas: snapshot.attr('currentVerion'),
+ };
+ }
+
+ return data;
},
});
diff --git a/ui/app/serializers/secret-v2.js b/ui/app/serializers/secret-v2.js
index 4dbbfd305d..8afabceed5 100644
--- a/ui/app/serializers/secret-v2.js
+++ b/ui/app/serializers/secret-v2.js
@@ -25,14 +25,15 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
return { id: fullSecretPath };
});
}
+ // transform versions to an array with composite IDs
if (payload.data.versions) {
payload.data.versions = Object.keys(payload.data.versions).map(version => {
let body = payload.data.versions[version];
body.version = version;
+ body.path = payload.id;
body.id = JSON.stringify([payload.backend, payload.id, version]);
return body;
});
- console.log(payload);
}
payload.data.id = payload.id;
return requestType === 'queryRecord' ? payload.data : [payload.data];
diff --git a/ui/tests/unit/adapters/secret-v2-test.js b/ui/tests/unit/adapters/secret-v2-test.js
index 3ddd45c23c..9502c4c7d8 100644
--- a/ui/tests/unit/adapters/secret-v2-test.js
+++ b/ui/tests/unit/adapters/secret-v2-test.js
@@ -1,26 +1,70 @@
-import { resolve } from 'rsvp';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
+import apiStub from 'vault/tests/helpers/noop-all-api-requests';
module('Unit | Adapter | secret-v2', function(hooks) {
setupTest(hooks);
- test('secret api urls', function(assert) {
- let url, method, options;
- let adapter = this.owner.factoryFor('adapter:secret-v2').create({
- ajax: (...args) => {
- [url, method, options] = args;
- return resolve({});
+ hooks.beforeEach(function() {
+ this.server = apiStub();
+ });
+
+ hooks.afterEach(function() {
+ this.server.shutdown();
+ });
+
+ [
+ ['query', null, {}, { id: '', backend: 'secret' }, 'GET', '/v1/secret/metadata/?list=true'],
+ ['queryRecord', null, {}, { id: 'foo', backend: 'secret' }, 'GET', '/v1/secret/metadata/foo'],
+ [
+ 'updateRecord',
+ {
+ serializerFor() {
+ return {
+ serializeIntoHash() {},
+ };
+ },
},
+ {},
+ {
+ id: 'foo',
+ belongsTo() {
+ return 'secret';
+ },
+ },
+ 'PUT',
+ '/v1/secret/metadata/foo',
+ ],
+ [
+ 'deleteRecord',
+ {
+ serializerFor() {
+ return {
+ serializeIntoHash() {},
+ };
+ },
+ },
+ {},
+ {
+ id: 'foo',
+ belongsTo() {
+ return 'secret';
+ },
+ },
+ 'DELETE',
+ '/v1/secret/metadata/foo',
+ ],
+ ].forEach(([adapterMethod, store, type, queryOrSnapshot, expectedHttpVerb, expectedURL]) => {
+ test(`secret-v2: ${adapterMethod}`, function(assert) {
+ let adapter = this.owner.lookup('adapter:secret-v2');
+ adapter[adapterMethod](store, type, queryOrSnapshot);
+ let { url, method } = this.server.handledRequests[0];
+ assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
+ assert.equal(
+ method,
+ expectedHttpVerb,
+ `${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
+ );
});
-
- adapter.query({}, 'secret', { id: '', backend: 'secret' });
- assert.equal(url, '/v1/secret/metadata/', 'query generic url OK');
- assert.equal('GET', method, 'query generic method OK');
- assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
-
- adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' });
- assert.equal(url, '/v1/secret/data/foo', 'queryRecord generic url OK');
- assert.equal('GET', method, 'queryRecord generic method OK');
});
});
diff --git a/ui/tests/unit/adapters/secret-v2-version-test.js b/ui/tests/unit/adapters/secret-v2-version-test.js
new file mode 100644
index 0000000000..bb81074dd3
--- /dev/null
+++ b/ui/tests/unit/adapters/secret-v2-version-test.js
@@ -0,0 +1,79 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import apiStub from 'vault/tests/helpers/noop-all-api-requests';
+
+module('Unit | Adapter | secret-v2-version', function(hooks) {
+ setupTest(hooks);
+
+ hooks.beforeEach(function() {
+ this.server = apiStub();
+ });
+
+ hooks.afterEach(function() {
+ this.server.shutdown();
+ });
+
+ [
+ [
+ 'findRecord with version',
+ 'findRecord',
+ [null, {}, JSON.stringify(['secret', 'foo', '2']), {}],
+ 'GET',
+ '/v1/secret/data/foo?version=2',
+ ],
+ [
+ 'deleteRecord with delete',
+ 'deleteRecord',
+ [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'delete' } }],
+ 'POST',
+ '/v1/secret/delete/foo',
+ { versions: ['2'] },
+ ],
+ [
+ 'deleteRecord with destroy',
+ 'deleteRecord',
+ [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'destroy' } }],
+ 'POST',
+ '/v1/secret/destroy/foo',
+ { versions: ['2'] },
+ ],
+ [
+ 'deleteRecord with destroy',
+ 'deleteRecord',
+ [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'undelete' } }],
+ 'POST',
+ '/v1/secret/undelete/foo',
+ { versions: ['2'] },
+ ],
+ [
+ 'updateRecord makes calls to correct url',
+ 'updateRecord',
+ [
+ {
+ serializerFor() {
+ return { serializeIntoHash() {} };
+ },
+ },
+ {},
+ { id: JSON.stringify(['secret', 'foo', '2']) },
+ ],
+ 'PUT',
+ '/v1/secret/data/foo',
+ ],
+ ].forEach(([testName, adapterMethod, args, expectedHttpVerb, expectedURL, exptectedRequestBody]) => {
+ test(`secret-v2: ${testName}`, function(assert) {
+ let adapter = this.owner.lookup('adapter:secret-v2-version');
+ adapter[adapterMethod](...args);
+ let { url, method, requestBody } = this.server.handledRequests[0];
+ assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
+ assert.equal(
+ method,
+ expectedHttpVerb,
+ `${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
+ );
+ if (exptectedRequestBody) {
+ assert.deepEqual(JSON.parse(requestBody), exptectedRequestBody);
+ }
+ });
+ });
+});
From 33d1f32d510d611e330d135823c1ede3e09d7b9f Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 22:04:48 -0500
Subject: [PATCH 18/50] move secret creation to always be at the root level,
and simplify model creation so we're always directly manipulating a model
---
.../cluster/secrets/backend/create-root.js | 46 +++++++++++++-
.../vault/cluster/secrets/backend/create.js | 61 ++-----------------
.../components/secret-list-header.hbs | 4 +-
3 files changed, 53 insertions(+), 58 deletions(-)
diff --git a/ui/app/routes/vault/cluster/secrets/backend/create-root.js b/ui/app/routes/vault/cluster/secrets/backend/create-root.js
index 28d5c8ccc5..a8aa145f87 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/create-root.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/create-root.js
@@ -1 +1,45 @@
-export { default } from './create';
+import { hash } from 'rsvp';
+import { inject as service } from '@ember/service';
+import EditBase from './secret-edit';
+
+let secretModel = (store, backend, key) => {
+ let backendModel = store.peekRecord('secret-engine', backend);
+ let modelType = backendModel.get('modelTypeForKV');
+ if (modelType !== 'secret-v2') {
+ return store.createRecord(modelType, {
+ id: key,
+ });
+ }
+ let secret = store.createRecord(modelType);
+ secret.set('engine', backendModel);
+ let version = store.createRecord('secret-v2-version', {
+ path: key,
+ });
+ secret.set('selectedVersion', version);
+ return secret;
+};
+
+export default EditBase.extend({
+ wizard: service(),
+ createModel(transition) {
+ const { backend } = this.paramsFor('vault.cluster.secrets.backend');
+ const modelType = this.modelType(backend);
+ if (modelType === 'role-ssh') {
+ return this.store.createRecord(modelType, { keyType: 'ca' });
+ }
+ if (modelType !== 'secret' && modelType !== 'secret-v2') {
+ if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') {
+ this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit');
+ }
+ return this.store.createRecord(modelType);
+ }
+ return secretModel(this.store, backend, transition.queryParams.initialKey);
+ },
+
+ model(params, transition) {
+ return hash({
+ secret: this.createModel(transition),
+ capabilities: {},
+ });
+ },
+});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/create.js b/ui/app/routes/vault/cluster/secrets/backend/create.js
index 9f9a188cf1..63d6f304bb 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/create.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/create.js
@@ -1,59 +1,10 @@
-import { hash } from 'rsvp';
-import { inject as service } from '@ember/service';
-import EmberObject from '@ember/object';
-import EditBase from './secret-edit';
-import KeyMixin from 'vault/models/key-mixin';
+import Route from '@ember/routing/route';
-var SecretProxy = EmberObject.extend(KeyMixin, {
- store: null,
-
- toModel() {
- return this.getProperties('id', 'secretData', 'backend');
- },
-
- createRecord(backend) {
- let backendModel = this.store.peekRecord('secret-engine', backend);
- return this.store.createRecord(backendModel.get('modelTypeForKV'), this.toModel());
- },
-
- willDestroy() {
- this.store = null;
- },
-});
-
-export default EditBase.extend({
- wizard: service(),
- createModel(transition, parentKey) {
- const { backend } = this.paramsFor('vault.cluster.secrets.backend');
- const modelType = this.modelType(backend);
- if (modelType === 'role-ssh') {
- return this.store.createRecord(modelType, { keyType: 'ca' });
- }
- if (modelType !== 'secret' && modelType !== 'secret-v2') {
- if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') {
- this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit');
- }
- return this.store.createRecord(modelType);
- }
- const key = transition.queryParams.initialKey || '';
- const model = SecretProxy.create({
- initialParentKey: parentKey,
- store: this.store,
- });
-
- if (key) {
- // have to set this after so that it will be
- // computed properly in the template (it's dependent on `initialParentKey`)
- model.set('keyWithoutParent', key);
- }
- return model;
- },
-
- model(params, transition) {
- const parentKey = params.secret ? params.secret : '';
- return hash({
- secret: this.createModel(transition, parentKey),
- capabilities: {},
+export default Route.extend({
+ beforeModel(transition) {
+ let { secret } = this.paramsFor(this.routeName);
+ return this.transitionTo('vault.cluster.secrets.backend.create-root', {
+ queryParams: { initialKey: secret },
});
},
});
diff --git a/ui/app/templates/components/secret-list-header.hbs b/ui/app/templates/components/secret-list-header.hbs
index ffb52a961f..4ae8d4d815 100644
--- a/ui/app/templates/components/secret-list-header.hbs
+++ b/ui/app/templates/components/secret-list-header.hbs
@@ -32,8 +32,8 @@
{{#secret-link
mode="create"
- secret=(or baseKey.id '')
- queryParams=(query-params initialKey='')
+ secret=''
+ queryParams=(query-params initialKey=baseKey.id)
class="button has-icon-right is-ghost is-compact"
data-test-secret-create=true
}}
From 4dce16776f177357496b3c26202c5b10291c8095 Mon Sep 17 00:00:00 2001
From: Matthew Irish
Date: Fri, 5 Oct 2018 22:05:53 -0500
Subject: [PATCH 19/50] adjust secret-edit component and associated templates
to work for v1 and v2
---
ui/app/components/secret-edit.js | 139 ++++++++----------
.../cluster/secrets/backend/secret-edit.js | 7 +-
.../partials/secret-edit-display.hbs | 4 +-
.../templates/partials/secret-form-create.hbs | 32 +---
.../templates/partials/secret-form-show.hbs | 4 +-
5 files changed, 79 insertions(+), 107 deletions(-)
diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js
index cfa3b5d653..cdd2ad04b0 100644
--- a/ui/app/components/secret-edit.js
+++ b/ui/app/components/secret-edit.js
@@ -1,9 +1,9 @@
import { or } from '@ember/object/computed';
import { isBlank, isNone } from '@ember/utils';
-import $ from 'jquery';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed, get } from '@ember/object';
+import { task, waitForEvent } from 'ember-concurrency';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import keys from 'vault/lib/keycodes';
import KVObject from 'vault/lib/kv-object';
@@ -18,6 +18,7 @@ export default Component.extend(FocusOnInsertMixin, {
// a key model
key: null,
+ model: null,
// a value to pre-fill the key input - this is populated by the corresponding
// 'initialKey' queryParam
@@ -44,10 +45,15 @@ export default Component.extend(FocusOnInsertMixin, {
codemirrorString: null,
hasLintError: false,
+ isV2: false,
init() {
this._super(...arguments);
- const secrets = this.get('key.secretData');
+ let secrets = this.model.secretData;
+ if (!secrets && this.model.selectedVersion) {
+ this.set('isV2', true);
+ secrets = this.model.belongsTo('selectedVersion').value().secretData;
+ }
const data = KVObject.create({ content: [] }).fromJSON(secrets);
this.set('secretData', data);
this.set('codemirrorString', data.toJSONString());
@@ -56,81 +62,75 @@ export default Component.extend(FocusOnInsertMixin, {
}
this.checkRows();
if (this.get('wizard.featureState') === 'details' && this.get('mode') === 'create') {
- let engine = this.get('key').backend.includes('kv') ? 'kv' : this.get('key').backend;
+ let engine = this.get('model').backend.includes('kv') ? 'kv' : this.get('model').backend;
this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', engine);
}
- if (this.get('mode') === 'edit') {
+ if (this.mode === 'edit') {
this.send('addRow');
}
},
- didInsertElement() {
- this._super(...arguments);
- $(document).on('keyup.keyEdit', this.onEscape.bind(this));
- },
-
willDestroyElement() {
this._super(...arguments);
- const key = this.get('key');
- if (get(key, 'isError') && !key.isDestroyed) {
- key.rollbackAttributes();
+ if (this.model.isError && !this.model.isDestroyed) {
+ model.rollbackAttributes();
}
- $(document).off('keyup.keyEdit');
},
+ waitForKeyUp: task(function*() {
+ while (true) {
+ let event = yield waitForEvent(document.body, 'keyup');
+ this.onEscape(event);
+ }
+ })
+ .on('didInsertElement')
+ .cancelOn('willDestroyElement'),
+
partialName: computed('mode', function() {
return `partials/secret-form-${this.get('mode')}`;
}),
- showPrefix: or('key.initialParentKey', 'key.parentKey'),
-
- requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'),
+ requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
buttonDisabled: or(
'requestInFlight',
- 'key.isFolder',
- 'key.isError',
- 'key.flagsIsInvalid',
+ 'model.isFolder',
+ 'model.isError',
+ 'model.flagsIsInvalid',
'hasLintError',
'error'
),
+ modelForData: computed('isV2', 'model', function() {
+ return this.isV2 ? this.model.belongsTo('selectedVersion').value() : this.model;
+ }),
+
basicModeDisabled: computed('secretDataIsAdvanced', 'showAdvancedMode', function() {
- return this.get('secretDataIsAdvanced') || this.get('showAdvancedMode') === false;
+ return this.secretDataIsAdvanced || this.showAdvancedMode === false;
}),
secretDataAsJSON: computed('secretData', 'secretData.[]', function() {
- return this.get('secretData').toJSON();
+ return this.secretData.toJSON();
}),
secretDataIsAdvanced: computed('secretData', 'secretData.[]', function() {
- return this.get('secretData').isAdvanced();
+ return this.secretData.isAdvanced();
}),
- hasDataChanges() {
- const keyDataString = this.get('key.dataAsJSONString');
- const sameData = this.get('secretData').toJSONString() === keyDataString;
- if (sameData === false) {
- this.set('lastChange', Date.now());
- }
-
- this.get('onDataChange')(!sameData);
- },
-
showAdvancedMode: computed('preferAdvancedEdit', 'secretDataIsAdvanced', 'lastChange', function() {
- return this.get('secretDataIsAdvanced') || this.get('preferAdvancedEdit');
+ return this.secretDataIsAdvanced || this.preferAdvancedEdit;
}),
transitionToRoute() {
- this.get('router').transitionTo(...arguments);
+ this.router.transitionTo(...arguments);
},
onEscape(e) {
- if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
+ if (e.keyCode !== keys.ESC || this.mode !== 'show') {
return;
}
- const parentKey = this.get('key.parentKey');
+ const parentKey = this.model.parentKey;
if (parentKey) {
this.transitionToRoute(LIST_ROUTE, parentKey);
} else {
@@ -139,25 +139,19 @@ export default Component.extend(FocusOnInsertMixin, {
},
// successCallback is called in the context of the component
- persistKey(method, successCallback, isCreate) {
- let model = this.get('key');
- let key = model.get('id');
+ persistKey(successCallback) {
+ let model = this.modelForData;
+ let key = model.get('path') || model.id;
if (key.startsWith('/')) {
key = key.replace(/^\/+/g, '');
- model.set('id', key);
+ model.set(model.pathAttr, key);
}
- if (isCreate && typeof model.createRecord === 'function') {
- // create an ember data model from the proxy
- model = model.createRecord(model.get('backend'));
- this.set('key', model);
- }
-
- return model[method]().then(() => {
- if (!get(model, 'isError')) {
- if (this.get('wizard.featureState') === 'secret') {
- this.get('wizard').transitionFeatureMachine('secret', 'CONTINUE');
+ return model.save().then(() => {
+ if (!model.isError) {
+ if (this.wizard.featureState === 'secret') {
+ this.wizard.transitionFeatureMachine('secret', 'CONTINUE');
}
successCallback(key);
}
@@ -165,85 +159,78 @@ export default Component.extend(FocusOnInsertMixin, {
},
checkRows() {
- if (this.get('secretData').get('length') === 0) {
+ if (this.secretData.length === 0) {
this.send('addRow');
}
},
actions: {
+ //submit on shift + enter
handleKeyDown(e) {
e.stopPropagation();
if (!(e.keyCode === keys.ENTER && e.metaKey)) {
return;
}
- let $form = this.$('form');
+ let $form = this.element.querySelector('form');
+ console.log('form is: ', $form);
if ($form.length) {
$form.submit();
}
- $form = null;
},
handleChange() {
- this.set('codemirrorString', this.get('secretData').toJSONString(true));
- this.hasDataChanges();
+ this.set('codemirrorString', this.secretData.toJSONString(true));
},
createOrUpdateKey(type, event) {
event.preventDefault();
- const newData = this.get('secretData').toJSON();
- this.get('key').set('secretData', newData);
+ const newData = this.secretData.toJSON();
+ let model = this.modelForData;
+ model.set('secretData', newData);
// prevent from submitting if there's no key
// maybe do something fancier later
- if (type === 'create' && isBlank(this.get('key.id'))) {
+ if (type === 'create' && isBlank(model.get('path') || model.id)) {
return;
}
- this.persistKey(
- 'save',
- key => {
- this.hasDataChanges();
- this.transitionToRoute(SHOW_ROUTE, key);
- },
- type === 'create'
- );
+ this.persistKey(key => {
+ this.transitionToRoute(SHOW_ROUTE, key);
+ });
},
deleteKey() {
- this.persistKey('destroyRecord', () => {
+ this.model.destroyRecord().then(() => {
this.transitionToRoute(LIST_ROOT_ROUTE);
});
},
refresh() {
- this.get('onRefresh')();
+ this.onRefresh();
},
addRow() {
- const data = this.get('secretData');
+ const data = this.secretData;
if (isNone(data.findBy('name', ''))) {
data.pushObject({ name: '', value: '' });
this.set('codemirrorString', data.toJSONString(true));
}
this.checkRows();
- this.hasDataChanges();
},
deleteRow(name) {
- const data = this.get('secretData');
+ const data = this.secretData;
const item = data.findBy('name', name);
if (isBlank(item.name)) {
return;
}
data.removeObject(item);
this.checkRows();
- this.hasDataChanges();
this.set('codemirrorString', data.toJSONString(true));
- this.rerender();
},
toggleAdvanced(bool) {
- this.get('onToggleAdvancedEdit')(bool);
+ this.onToggleAdvancedEdit(bool);
},
codemirrorUpdated(val, codemirror) {
@@ -252,7 +239,7 @@ export default Component.extend(FocusOnInsertMixin, {
const noErrors = codemirror.state.lint.marked.length === 0;
if (noErrors) {
try {
- this.get('secretData').fromJSONString(val);
+ this.secretData.fromJSONString(val);
} catch (e) {
this.set('error', e.message);
}
@@ -262,7 +249,7 @@ export default Component.extend(FocusOnInsertMixin, {
},
formatJSON() {
- this.set('codemirrorString', this.get('secretData').toJSONString(true));
+ this.set('codemirrorString', this.secretData.toJSONString(true));
},
},
});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
index 67aec2bf6c..8262211565 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
@@ -76,7 +76,10 @@ export default Route.extend(UnloadModelRoute, {
if (modelType === 'secret-v2') {
// TODO, find by query param to enable viewing versions
let version = resp.versions.findBy('version', resp.currentVersion);
- version.reload();
+ return version.reload().then(() => {
+ resp.set('selectedVersion', version);
+ return resp;
+ });
}
return resp;
}),
@@ -127,6 +130,8 @@ export default Route.extend(UnloadModelRoute, {
},
willTransition(transition) {
+ console.log(this.hasChanges);
+ console.log(this.controller.model.hasDirtyAttributes);
if (this.get('hasChanges')) {
if (
window.confirm(
diff --git a/ui/app/templates/partials/secret-edit-display.hbs b/ui/app/templates/partials/secret-edit-display.hbs
index 0bb384a0f6..aad4f67161 100644
--- a/ui/app/templates/partials/secret-edit-display.hbs
+++ b/ui/app/templates/partials/secret-edit-display.hbs
@@ -1,4 +1,3 @@
-{{#unless key.isFolder}}
{{#if showAdvancedMode}}
{{json-editor
value=codemirrorString
@@ -44,5 +43,4 @@
{{/each}}
- {{/if}}
-{{/unless}}
+ {{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/partials/secret-form-create.hbs b/ui/app/templates/partials/secret-form-create.hbs
index 852e1d6832..1eef75afad 100644
--- a/ui/app/templates/partials/secret-form-create.hbs
+++ b/ui/app/templates/partials/secret-form-create.hbs
@@ -1,30 +1,12 @@
-