diff --git a/changelog/22122.txt b/changelog/22122.txt new file mode 100644 index 0000000000..a7e723090c --- /dev/null +++ b/changelog/22122.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: upgrade Ember to 4.12 +``` diff --git a/ui/.ember-cli b/ui/.ember-cli index f6e59871ff..fcd9114b12 100644 --- a/ui/.ember-cli +++ b/ui/.ember-cli @@ -9,8 +9,8 @@ "output-path": "../http/web_ui", /** - Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript - rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. */ "isTypeScriptProject": false } diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index bb45483198..e4c12623ea 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -8,13 +8,14 @@ 'use strict'; module.exports = { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', root: true, parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 'latest', sourceType: 'module', - ecmaFeatures: { - legacyDecorators: true, + requireConfigFile: false, + babelOptions: { + plugins: [['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]], }, }, plugins: ['ember'], @@ -45,6 +46,7 @@ module.exports = { files: [ './.eslintrc.js', './.prettierrc.js', + './.stylelintrc.js', './.template-lintrc.js', './ember-cli-build.js', './testem.js', @@ -60,13 +62,7 @@ module.exports = { browser: false, node: true, }, - plugins: ['node'], - extends: ['plugin:node/recommended'], - rules: { - // this can be removed once the following is fixed - // https://github.com/mysticatea/eslint-plugin-node/issues/77 - 'node/no-unpublished-require': 'off', - }, + extends: ['plugin:n/recommended'], }, { // test files diff --git a/ui/.prettierrc.js b/ui/.prettierrc.js index 8c776351a4..a9d4dd9354 100644 --- a/ui/.prettierrc.js +++ b/ui/.prettierrc.js @@ -17,5 +17,11 @@ module.exports = { printWidth: 125, }, }, + { + files: '*.{js,ts}', + options: { + singleQuote: true, + }, + }, ], }; diff --git a/ui/.stylelintignore b/ui/.stylelintignore new file mode 100644 index 0000000000..a0cf71cbd1 --- /dev/null +++ b/ui/.stylelintignore @@ -0,0 +1,8 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ + +# addons +/.node_modules.ember-try/ diff --git a/ui/.stylelintrc.js b/ui/.stylelintrc.js new file mode 100644 index 0000000000..021c539ad0 --- /dev/null +++ b/ui/.stylelintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], +}; diff --git a/ui/.template-lintrc.js b/ui/.template-lintrc.js index 3540521890..5510459e29 100644 --- a/ui/.template-lintrc.js +++ b/ui/.template-lintrc.js @@ -46,6 +46,7 @@ module.exports = { allow: ['supported-auth-backends'], }, 'require-input-label': 'off', + 'no-array-prototype-extensions': 'off', }, ignore: ['lib/story-md', 'tests/**'], // ember language server vscode extension does not currently respect the ignore field diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js index 2fb882a4ac..b282a9b4d5 100644 --- a/ui/app/components/auth-form.js +++ b/ui/app/components/auth-form.js @@ -219,6 +219,8 @@ export default Component.extend(DEFAULTS, { }; }) ); + // without unloading the records there will be an issue where all methods set to list when unauthenticated will appear for all namespaces + // if possible, it would be more reliable to add a namespace attr to the model so we could filter against the current namespace rather than unloading all next(() => { store.unloadAll('auth-method'); }); @@ -265,7 +267,7 @@ export default Component.extend(DEFAULTS, { return; } let response = null; - this.setOktaNumberChallenge(true); + this.args.setOktaNumberChallenge(true); this.setCancellingAuth(false); // keep polling /auth/okta/verify/:nonce API every 1s until a response is given with the correct number for the Okta Number Challenge while (response === null) { @@ -328,7 +330,7 @@ export default Component.extend(DEFAULTS, { }); }, returnToLoginFromOktaNumberChallenge() { - this.setOktaNumberChallenge(false); + this.args.setOktaNumberChallenge(false); this.set('oktaNumberChallengeAnswer', null); }, }, diff --git a/ui/app/components/database-role-edit.js b/ui/app/components/database-role-edit.js index 8f0c000310..7ff9874470 100644 --- a/ui/app/components/database-role-edit.js +++ b/ui/app/components/database-role-edit.js @@ -62,7 +62,7 @@ export default class DatabaseRoleEdit extends Component { delete() { const secret = this.args.model; const backend = secret.backend; - secret + return secret .destroyRecord() .then(() => { try { @@ -89,7 +89,7 @@ export default class DatabaseRoleEdit extends Component { const path = roleSecret.type === 'static' ? 'static-roles' : 'roles'; roleSecret.set('path', path); } - roleSecret + return roleSecret .save() .then(() => { try { @@ -110,7 +110,7 @@ export default class DatabaseRoleEdit extends Component { rotateRoleCred(id) { const backend = this.args.model?.backend; const adapter = this.store.adapterFor('database/credential'); - adapter + return adapter .rotateRoleCredentials(backend, id) .then(() => { this.flashMessages.success(`Success: Credentials for ${id} role were rotated`); diff --git a/ui/app/components/generate-credentials.js b/ui/app/components/generate-credentials.js index ea639c9a51..dc7507b945 100644 --- a/ui/app/components/generate-credentials.js +++ b/ui/app/components/generate-credentials.js @@ -55,7 +55,9 @@ export default Component.extend({ }, willDestroy() { - if (!this.model.isDestroyed && !this.model.isDestroying) { + // components are torn down after store is unloaded and will cause an error if attempt to unload record + const noTeardown = this.store && !this.store.isDestroying; + if (noTeardown && !this.model.isDestroyed && !this.model.isDestroying) { this.model.unloadRecord(); } this._super(...arguments); diff --git a/ui/app/components/identity/edit-form.js b/ui/app/components/identity/edit-form.js index 1668fc8afe..a80d385303 100644 --- a/ui/app/components/identity/edit-form.js +++ b/ui/app/components/identity/edit-form.js @@ -12,6 +12,7 @@ import { waitFor } from '@ember/test-waiters'; export default Component.extend({ flashMessages: service(), + store: service(), 'data-test-component': 'identity-edit-form', attributeBindings: ['data-test-component'], model: null, @@ -73,12 +74,13 @@ export default Component.extend({ ).drop(), willDestroy() { - this._super(...arguments); + // components are torn down after store is disconnected and will cause an error if attempt to unload record + const noTeardown = this.store && !this.store.isDestroying; const model = this.model; - if (!model) return; - if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) { + if (noTeardown && model && model.get('isDirty') && !model.isDestroyed && !model.isDestroying) { model.rollbackAttributes(); } + this._super(...arguments); }, actions: { diff --git a/ui/app/components/mfa/mfa-login-enforcement-form.js b/ui/app/components/mfa/mfa-login-enforcement-form.js index 95e9757cef..c7991b7a15 100644 --- a/ui/app/components/mfa/mfa-login-enforcement-form.js +++ b/ui/app/components/mfa/mfa-login-enforcement-form.js @@ -6,6 +6,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { A } from '@ember/array'; import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import handleHasManySelection from 'core/utils/search-select-has-many'; @@ -40,14 +41,14 @@ export default class MfaLoginEnforcementForm extends Component { searchSelectOptions = null; @tracked name; - @tracked targets = []; + @tracked targets = A([]); @tracked selectedTargetType = 'accessor'; @tracked selectedTargetValue = null; @tracked searchSelect = { options: [], selected: [], }; - @tracked authMethods = []; + @tracked authMethods = A([]); @tracked modelErrors; constructor() { @@ -100,6 +101,12 @@ export default class MfaLoginEnforcementForm extends Component { return this.args.modelErrors || this.modelErrors; } + updateModelForKey(key) { + const newValue = this.targets.filter((t) => t.key === key).map((t) => t.value); + // Set the model value directly instead of using Array methods (like .addObject) + this.args.model[key] = newValue; + } + @task *save() { this.modelErrors = {}; @@ -139,21 +146,22 @@ export default class MfaLoginEnforcementForm extends Component { this.selectedTargetValue = selected; } } + @action addTarget() { const { label, key } = this.selectedTarget; const value = this.selectedTargetValue; this.targets.addObject({ label, value, key }); - // add target to appropriate model property - this.args.model[key].addObject(value); + // recalculate value for appropriate model property + this.updateModelForKey(key); this.selectedTargetValue = null; this.resetTargetState(); } @action removeTarget(target) { this.targets.removeObject(target); - // remove target from appropriate model property - this.args.model[target.key].removeObject(target.value); + // recalculate value for appropriate model property + this.updateModelForKey(target.key); } @action cancel() { diff --git a/ui/app/components/mount-backend-form.js b/ui/app/components/mount-backend-form.js index 6b09f5e1f0..96acd34ea2 100644 --- a/ui/app/components/mount-backend-form.js +++ b/ui/app/components/mount-backend-form.js @@ -35,12 +35,12 @@ export default class MountBackendForm extends Component { @tracked errorMessage = ''; willDestroy() { - // if unsaved, we want to unload so it doesn't show up in the auth mount list - super.willDestroy(...arguments); - if (this.args.mountModel) { - const method = this.args.mountModel.isNew ? 'unloadRecord' : 'rollbackAttributes'; - this.args.mountModel[method](); + // components are torn down after store is unloaded and will cause an error if attempt to unload record + const noTeardown = this.store && !this.store.isDestroying; + if (noTeardown && this.args?.mountModel) { + this.args.mountModel.rollbackAttributes(); } + super.willDestroy(...arguments); } checkPathChange(type) { diff --git a/ui/app/components/role-edit.js b/ui/app/components/role-edit.js index e5345f6a54..0032c56ee9 100644 --- a/ui/app/components/role-edit.js +++ b/ui/app/components/role-edit.js @@ -26,10 +26,10 @@ export default Component.extend(FocusOnInsertMixin, { requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), willDestroyElement() { - this._super(...arguments); - if (this.model && this.model.isError) { + if (this.model && this.model.isError && !this.model.isDestroyed && !this.model.isDestroying) { this.model.rollbackAttributes(); } + this._super(...arguments); }, waitForKeyUp: task(function* () { diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index a2739389bd..e816564983 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -35,14 +35,13 @@ export default class SecretEdit extends Component { @service store; @tracked secretData = null; - @tracked isV2 = false; @tracked codemirrorString = null; // fired on did-insert from render modifier @action createKvData(elem, [model]) { - if (!model.secretData && model.selectedVersion) { - this.isV2 = true; + if (this.isV2) { + // pre-fill secret data from selected version model.secretData = model.belongsTo('selectedVersion').value().secretData; } this.secretData = KVObject.create({ content: [] }).fromJSON(model.secretData); @@ -97,6 +96,9 @@ export default class SecretEdit extends Component { @or('model.isLoading', 'model.isReloading', 'model.isSaving') requestInFlight; @or('requestInFlight', 'model.isFolder', 'model.flagsIsInvalid') buttonDisabled; + get isV2() { + return !!this.args.model?.selectedVersion; + } get modelForData() { const { model } = this.args; if (!model) return null; diff --git a/ui/app/components/transform-edit-base.js b/ui/app/components/transform-edit-base.js index 4fb47b4853..0ab6775ce7 100644 --- a/ui/app/components/transform-edit-base.js +++ b/ui/app/components/transform-edit-base.js @@ -44,10 +44,10 @@ export default Component.extend(FocusOnInsertMixin, { }, willDestroyElement() { - this._super(...arguments); - if (this.model && this.model.isError) { + if (this.model && this.model.isError && !this.model.isDestroyed && !this.model.isDestroying) { this.model.rollbackAttributes(); } + this._super(...arguments); }, transitionToRoute() { diff --git a/ui/app/components/transit-edit.js b/ui/app/components/transit-edit.js index ee0c1a8840..2a8853238c 100644 --- a/ui/app/components/transit-edit.js +++ b/ui/app/components/transit-edit.js @@ -26,10 +26,10 @@ export default Component.extend(FocusOnInsertMixin, { requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'), willDestroyElement() { - this._super(...arguments); - if (this.key && this.key.isError) { + if (this.key && this.key.isError && !this.key.isDestroyed && !this.key.isDestroying) { this.key.rollbackAttributes(); } + this._super(...arguments); }, waitForKeyUp: task(function* () { diff --git a/ui/app/initializers/deprecation-filter.js b/ui/app/initializers/deprecation-filter.js index 905c610ae5..2e0f2a4a5d 100644 --- a/ui/app/initializers/deprecation-filter.js +++ b/ui/app/initializers/deprecation-filter.js @@ -10,10 +10,10 @@ export function initialize() { registerDeprecationHandler((message, options, next) => { // filter deprecations that are scheduled to be removed in a specific version // when upgrading or addressing deprecation warnings be sure to update this or remove if not needed - if (options?.until !== '5.0.0') { - next(message, options); + if (options?.until.includes('5.0')) { + return; } - return; + next(message, options); }); } diff --git a/ui/app/lib/route-paths.js b/ui/app/lib/route-paths.js index e0125ae0e5..63ad2d24ca 100644 --- a/ui/app/lib/route-paths.js +++ b/ui/app/lib/route-paths.js @@ -6,6 +6,7 @@ export const INIT = 'vault.cluster.init'; export const UNSEAL = 'vault.cluster.unseal'; export const AUTH = 'vault.cluster.auth'; +export const LOGOUT = 'vault.cluster.logout'; export const REDIRECT = 'vault.cluster.redirect'; export const CLUSTER = 'vault.cluster'; export const CLUSTER_INDEX = 'vault.cluster.index'; diff --git a/ui/app/macros/maybe-query-record.js b/ui/app/macros/maybe-query-record.js index 94cc369923..655b0a2583 100644 --- a/ui/app/macros/maybe-query-record.js +++ b/ui/app/macros/maybe-query-record.js @@ -7,6 +7,23 @@ import { computed } from '@ember/object'; import ObjectProxy from '@ember/object/proxy'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import { resolve } from 'rsvp'; +/** + * after upgrading to Ember 4.12 a secrets test was erroring with "Cannot create a new tag for `` after it has been destroyed" + * see this GH issue for information on the fix https://github.com/emberjs/ember.js/issues/16541#issuecomment-382403523 + */ +ObjectProxy.reopen({ + unknownProperty(key) { + if (this.isDestroying || this.isDestroyed) { + return; + } + + if (this.content && (this.content.isDestroying || this.content.isDestroyed)) { + return; + } + + return this._super(key); + }, +}); export function maybeQueryRecord(modelName, options = {}, ...keys) { return computed(...keys, 'store', { diff --git a/ui/app/mixins/model-boundary-route.js b/ui/app/mixins/model-boundary-route.js index 3d639d7c84..0fb9e02e84 100644 --- a/ui/app/mixins/model-boundary-route.js +++ b/ui/app/mixins/model-boundary-route.js @@ -56,6 +56,11 @@ export default Mixin.create({ ); return; } + if (this.store.isDestroyed || this.store.isDestroying) { + // Prevent unload attempt after test teardown, resulting in test failure + return; + } + if (modelType) { this.store.unloadAll(modelType); } diff --git a/ui/app/mixins/unload-model-route.js b/ui/app/mixins/unload-model-route.js index 62375a4265..6f4c2befcb 100644 --- a/ui/app/mixins/unload-model-route.js +++ b/ui/app/mixins/unload-model-route.js @@ -19,7 +19,6 @@ export default Mixin.create({ return; } removeRecord(this.store, model); - model.destroy(); // it's important to unset the model on the controller since controllers are singletons this.controller.set(modelPath, null); }, diff --git a/ui/app/mixins/unsaved-model-route.js b/ui/app/mixins/unsaved-model-route.js index 717504dca6..1033c24f84 100644 --- a/ui/app/mixins/unsaved-model-route.js +++ b/ui/app/mixins/unsaved-model-route.js @@ -4,6 +4,7 @@ */ import Mixin from '@ember/object/mixin'; +import Ember from 'ember'; // this mixin relies on `unload-model-route` also being used export default Mixin.create({ @@ -15,6 +16,7 @@ export default Mixin.create({ } if (model.hasDirtyAttributes) { if ( + Ember.testing || window.confirm( 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?' ) diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index 0a8600a0db..9c6fa42750 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -5,75 +5,104 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { inject as service } from '@ember/service'; -import { alias, and, equal, gte, not, or } from '@ember/object/computed'; -import { get, computed } from '@ember/object'; +import { get } from '@ember/object'; -export default Model.extend({ - version: service(), +export default class ClusterModel extends Model { + @service version; - nodes: hasMany('nodes', { async: false }), - name: attr('string'), - status: attr('string'), - standby: attr('boolean'), - type: attr('string'), - license: attr('object'), + @hasMany('nodes', { async: false, inverse: null }) nodes; + @attr('string') name; + @attr('string') status; + @attr('boolean') standby; + @attr('string') type; + @attr('object') license; /* Licensing concerns */ - licenseExpiry: alias('license.expiry_time'), - licenseState: alias('license.state'), + get licenseExpiry() { + return this.license?.expiry_time; + } + get licenseState() { + return this.license?.state; + } - needsInit: computed('nodes', 'nodes.@each.initialized', function () { - // needs init if no nodes are initialized - return this.nodes.isEvery('initialized', false); - }), + get needsInit() { + return this.nodes.every((node) => { + return node.initialized === false; + }); + } - unsealed: computed('nodes', 'nodes.{[],@each.sealed}', function () { - // unsealed if there's at least one unsealed node - return !!this.nodes.findBy('sealed', false); - }), + get unsealed() { + return !!this.nodes.find((node) => { + return node.sealed === false; + }); + } - sealed: not('unsealed'), + get sealed() { + return !this.unsealed; + } - leaderNode: computed('nodes', 'nodes.[]', function () { + get leaderNode() { const nodes = this.nodes; - if (nodes.get('length') === 1) { - return nodes.get('firstObject'); + if (nodes.length === 1) { + return nodes[0]; } else { - return nodes.findBy('isLeader'); + return nodes.find((node) => node.isLeader === true); } - }), + } - sealThreshold: alias('leaderNode.sealThreshold'), - sealProgress: alias('leaderNode.progress'), - sealType: alias('leaderNode.type'), - storageType: alias('leaderNode.storageType'), - hcpLinkStatus: alias('leaderNode.hcpLinkStatus'), - hasProgress: gte('sealProgress', 1), - usingRaft: equal('storageType', 'raft'), + get sealThreshold() { + return this.leaderNode?.sealThreshold; + } + get sealProgress() { + return this.leaderNode?.progress; + } + get sealType() { + return this.leaderNode?.type; + } + get storageType() { + return this.leaderNode?.storageType; + } + get hcpLinkStatus() { + return this.leaderNode?.hcpLinkStatus; + } + get hasProgress() { + return this.sealProgress >= 1; + } + get usingRaft() { + return this.storageType === 'raft'; + } //replication mode - will only ever be 'unsupported' //otherwise the particular mode will have the relevant mode attr through replication-attributes - mode: attr('string'), - allReplicationDisabled: and('{dr,performance}.replicationDisabled'), - anyReplicationEnabled: or('{dr,performance}.replicationEnabled'), + @attr('string') mode; + get allReplicationDisabled() { + return this.dr?.replicationDisabled && this.performance?.replicationDisabled; + } + get anyReplicationEnabled() { + return this.dr?.replicationEnabled || this.performance?.replicationEnabled; + } - dr: belongsTo('replication-attributes', { async: false, inverse: null }), - performance: belongsTo('replication-attributes', { async: false, inverse: null }), + @belongsTo('replication-attributes', { async: false, inverse: null }) dr; + @belongsTo('replication-attributes', { async: false, inverse: null }) performance; // this service exposes what mode the UI is currently viewing // replicationAttrs will then return the relevant `replication-attributes` model - rm: service('replication-mode'), - drMode: alias('dr.mode'), - replicationMode: alias('rm.mode'), - replicationModeForDisplay: computed('replicationMode', function () { + @service('replication-mode') rm; + get drMode() { + return this.dr.mode; + } + get replicationMode() { + return this.rm.mode; + } + get replicationModeForDisplay() { return this.replicationMode === 'dr' ? 'Disaster Recovery' : 'Performance'; - }), - replicationIsInitializing: computed('dr.mode', 'performance.mode', function () { + } + get replicationIsInitializing() { // a mode of null only happens when a cluster is being initialized // otherwise the mode will be 'disabled', 'primary', 'secondary' - return !this.dr.mode || !this.performance.mode; - }), - replicationAttrs: computed('dr.mode', 'performance.mode', 'replicationMode', function () { + return !this.dr?.mode || !this.performance?.mode; + } + get replicationAttrs() { const replicationMode = this.replicationMode; return replicationMode ? get(this, replicationMode) : null; - }), -}); + } +} diff --git a/ui/app/models/kubernetes/role.js b/ui/app/models/kubernetes/role.js index 03473e74f2..167d7e92c2 100644 --- a/ui/app/models/kubernetes/role.js +++ b/ui/app/models/kubernetes/role.js @@ -52,12 +52,6 @@ export default class KubernetesRoleModel extends Model { }) kubernetesRoleName; - @attr('string', { - label: 'Service account name', - subText: 'Vault will use the default template when generating service accounts, roles and role bindings.', - }) - serviceAccountName; - @attr('string', { label: 'Allowed Kubernetes namespaces', subText: diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index afda9977d8..39da7b3157 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -160,13 +160,6 @@ export default class SecretEngineModel extends Model { return 'vault.cluster.secrets.backend.list-root'; } - get accessor() { - if (this.version === 2) { - return `v2 ${this.accessor}`; - } - return this.accessor; - } - get localDisplay() { return this.local ? 'local' : 'replicated'; } 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 7e1be219f5..8a277bbb6e 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -5,6 +5,7 @@ import AdapterError from '@ember-data/adapter/error'; import { set } from '@ember/object'; +import Ember from 'ember'; import { resolve } from 'rsvp'; import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; @@ -314,6 +315,12 @@ export default Route.extend(UnloadModelRoute, { willTransition(transition) { /* eslint-disable-next-line ember/no-controller-access-in-routes */ const { mode, model } = this.controller; + + // If model is clean or deleted, continue + if (!model.hasDirtyAttributes || model.isDeleted) { + return true; + } + // TODO: below is KV v2 logic, remove with engine work const version = model.get('selectedVersion'); const changed = model.changedAttributes(); const changedKeys = Object.keys(changed); @@ -330,9 +337,10 @@ export default Route.extend(UnloadModelRoute, { // and explicity ignore it here if ( (mode !== 'show' && changedKeys.length && changedKeys[0] !== 'backend') || - (mode !== 'show' && version && Object.keys(version.changedAttributes()).length) + (mode !== 'show' && version && version.hasDirtyAttributes) ) { if ( + Ember.testing || window.confirm( 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?' ) diff --git a/ui/app/routes/vault/cluster/settings/auth/enable.js b/ui/app/routes/vault/cluster/settings/auth/enable.js index 356ac5ebe8..19cded9cfe 100644 --- a/ui/app/routes/vault/cluster/settings/auth/enable.js +++ b/ui/app/routes/vault/cluster/settings/auth/enable.js @@ -9,11 +9,6 @@ import { inject as service } from '@ember/service'; export default class VaultClusterSettingsAuthEnableRoute extends Route { @service store; - beforeModel() { - // Unload to prevent naming collisions when we mount a new engine - this.store.unloadAll('auth-method'); - } - model() { const authMethod = this.store.createRecord('auth-method'); authMethod.set('config', this.store.createRecord('mount-config')); diff --git a/ui/app/routes/vault/cluster/settings/mount-secret-backend.js b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js index de9b16d1ad..8aa810384b 100644 --- a/ui/app/routes/vault/cluster/settings/mount-secret-backend.js +++ b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js @@ -9,11 +9,6 @@ import { inject as service } from '@ember/service'; export default class VaultClusterSettingsMountSecretBackendRoute extends Route { @service store; - beforeModel() { - // Unload to prevent naming collisions when we mount a new engine - this.store.unloadAll('secret-engine'); - } - model() { const secretEngine = this.store.createRecord('secret-engine'); secretEngine.set('config', this.store.createRecord('mount-config')); diff --git a/ui/app/serializers/mfa-login-enforcement.js b/ui/app/serializers/mfa-login-enforcement.js index bf71cb4f1e..ca5b90b16d 100644 --- a/ui/app/serializers/mfa-login-enforcement.js +++ b/ui/app/serializers/mfa-login-enforcement.js @@ -40,6 +40,7 @@ export default class MfaLoginEnforcementSerializer extends ApplicationSerializer // ensure that they are sent to the server, otherwise removing items will not be persisted json.auth_method_accessors = json.auth_method_accessors || []; json.auth_method_types = json.auth_method_types || []; + // TODO: create array transform which serializes an empty array if empty return this.transformHasManyKeys(json, 'server'); } } diff --git a/ui/app/serializers/mount-config.js b/ui/app/serializers/mount-config.js deleted file mode 100644 index 32fbf4c102..0000000000 --- a/ui/app/serializers/mount-config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend(); diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js index 170ca6e930..4d7205f073 100644 --- a/ui/app/services/auth.js +++ b/ui/app/services/auth.js @@ -4,19 +4,21 @@ */ import Ember from 'ember'; -import { resolve, reject } from 'rsvp'; -import { assign } from '@ember/polyfills'; +import { task, timeout } from 'ember-concurrency'; +import { getOwner } from '@ember/application'; import { isArray } from '@ember/array'; import { computed, get } from '@ember/object'; -import { capitalize } from '@ember/string'; - -import fetch from 'fetch'; -import { getOwner } from '@ember/application'; +import { alias } from '@ember/object/computed'; +import { assign } from '@ember/polyfills'; import Service, { inject as service } from '@ember/service'; -import getStorage from '../lib/token-storage'; +import { capitalize } from '@ember/string'; +import fetch from 'fetch'; +import { resolve, reject } from 'rsvp'; + +import getStorage from 'vault/lib/token-storage'; import ENV from 'vault/config/environment'; import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; -import { task, timeout } from 'ember-concurrency'; + const TOKEN_SEPARATOR = '☃'; const TOKEN_PREFIX = 'vault-'; const ROOT_PREFIX = '_root_'; @@ -26,7 +28,7 @@ export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX }; export default Service.extend({ permissions: service(), - store: service(), + currentCluster: service(), router: service(), namespaceService: service('namespace'), @@ -40,9 +42,7 @@ export default Service.extend({ return expiration ? this.now() >= expiration : null; }, - get activeCluster() { - return this.activeClusterId ? this.store.peekRecord('cluster', this.activeClusterId) : null; - }, + activeCluster: alias('currentCluster.cluster'), // eslint-disable-next-line tokens: computed({ diff --git a/ui/app/services/current-cluster.js b/ui/app/services/current-cluster.js index 2aa61b52c6..0265e33c9f 100644 --- a/ui/app/services/current-cluster.js +++ b/ui/app/services/current-cluster.js @@ -4,11 +4,12 @@ */ import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; -export default Service.extend({ - cluster: null, +export default class CurrentClusterService extends Service { + @tracked cluster = null; setCluster(cluster) { - this.set('cluster', cluster); - }, -}); + this.cluster = cluster; + } +} diff --git a/ui/app/services/replication-mode.js b/ui/app/services/replication-mode.js index 25316f827d..9f471aa548 100644 --- a/ui/app/services/replication-mode.js +++ b/ui/app/services/replication-mode.js @@ -4,15 +4,16 @@ */ import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; -export default Service.extend({ - mode: null, +export default class ReplicationModeService extends Service { + @tracked mode = null; getMode() { return this.mode; - }, + } setMode(mode) { - this.set('mode', mode); - }, -}); + this.mode = mode; + } +} diff --git a/ui/app/services/store.js b/ui/app/services/store.js index 6e0767be0b..40b1a402f7 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -140,7 +140,7 @@ export default class StoreService extends Store { // pushes records into the store and returns the result fetchPage(modelName, query) { const response = this.constructResponse(modelName, query); - this.peekAll(modelName).map((model) => model.unloadRecord()); + this.unloadAll(modelName); return new Promise((resolve) => { // after the above unloadRecords are finished, push into store schedule('destroy', () => { @@ -187,4 +187,29 @@ export default class StoreService extends Store { clearAllDatasets() { this.clearDataset(); } + /** + * this is designed to be a temporary workaround to an issue in the test environment after upgrading to Ember 4.12 + * when performing an unloadAll or unloadRecord for auth-method or secret-engine models within the app code an error breaks the tests + * after the test run is finished during teardown an unloadAll happens and the error "Expected a stable identifier" is thrown + * it seems that when the unload happens in the app, for some reason the mount-config relationship models are not unloaded + * then when the unloadAll happens a second time during test teardown there seems to be an issue since those records should already have been unloaded + * when logging in the teardownRecord hook, it appears that other embedded inverse: null relationships such as replication-attributes are torn down when the parent model is unloaded + * the following fixes the issue by explicitly unloading the mount-config models associated to the parent + * this should be looked into further to find the root cause, at which time these overrides may be removed + */ + unloadAll(modelName) { + const hasMountConfig = ['auth-method', 'secret-engine']; + if (hasMountConfig.includes(modelName)) { + this.peekAll(modelName).forEach((record) => this.unloadRecord(record)); + } else { + super.unloadAll(modelName); + } + } + unloadRecord(record) { + const hasMountConfig = ['auth-method', 'secret-engine']; + if (record && hasMountConfig.includes(record.constructor.modelName) && record.config) { + super.unloadRecord(record.config); + } + super.unloadRecord(record); + } } diff --git a/ui/app/templates/components/clients/attribution.hbs b/ui/app/templates/components/clients/attribution.hbs index cf57aacd9d..216039d797 100644 --- a/ui/app/templates/components/clients/attribution.hbs +++ b/ui/app/templates/components/clients/attribution.hbs @@ -69,8 +69,8 @@ {{/if}}
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} + {{capitalize (get @chartLegend "0.label")}} + {{capitalize (get @chartLegend "1.label")}}
{{else}}
diff --git a/ui/app/templates/components/clients/horizontal-bar-chart.hbs b/ui/app/templates/components/clients/horizontal-bar-chart.hbs index 1aac955cbb..e22b25cf4c 100644 --- a/ui/app/templates/components/clients/horizontal-bar-chart.hbs +++ b/ui/app/templates/components/clients/horizontal-bar-chart.hbs @@ -15,11 +15,7 @@ {{! Component must be in curly bracket notation }} {{! template-lint-disable no-curly-component-invocation }} {{#modal-dialog - tagName="div" - tetherTarget=this.tooltipTarget - targetAttachment="bottom middle" - attachment="bottom middle" - offset="35px 0" + tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="35px 0" }}

{{this.tooltipText}}

diff --git a/ui/app/templates/components/clients/line-chart.hbs b/ui/app/templates/components/clients/line-chart.hbs index 65433a9525..b48b820f1b 100644 --- a/ui/app/templates/components/clients/line-chart.hbs +++ b/ui/app/templates/components/clients/line-chart.hbs @@ -18,11 +18,7 @@ {{! Component must be in curly bracket notation }} {{! template-lint-disable no-curly-component-invocation }} {{#modal-dialog - tagName="div" - tetherTarget=this.tooltipTarget - targetAttachment="bottom middle" - attachment="bottom middle" - offset="35px 0" + tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="35px 0" }}

{{this.tooltipMonth}}

diff --git a/ui/app/templates/components/clients/monthly-usage.hbs b/ui/app/templates/components/clients/monthly-usage.hbs index 307c6b098f..9540d0f405 100644 --- a/ui/app/templates/components/clients/monthly-usage.hbs +++ b/ui/app/templates/components/clients/monthly-usage.hbs @@ -40,8 +40,8 @@ {{#if @verticalBarChartData}}
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} + {{capitalize (get @chartLegend "0.label")}} + {{capitalize (get @chartLegend "1.label")}}
{{/if}}
\ No newline at end of file diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs index 2a857cbbe4..485d9f31c3 100644 --- a/ui/app/templates/components/clients/running-total.hbs +++ b/ui/app/templates/components/clients/running-total.hbs @@ -78,8 +78,8 @@ {{#if this.hasAverageNewClients}}
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} + {{capitalize (get @chartLegend "0.label")}} + {{capitalize (get @chartLegend "1.label")}}
{{/if}}
diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs index 01e0b7da94..f9e35161b6 100644 --- a/ui/app/templates/components/clients/vertical-bar-chart.hbs +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -18,11 +18,7 @@ {{! Component must be in curly bracket notation }} {{! template-lint-disable no-curly-component-invocation }} {{#modal-dialog - tagName="div" - tetherTarget=this.tooltipTarget - targetAttachment="bottom middle" - attachment="bottom middle" - offset="10px 0" + tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="10px 0" }}

{{this.tooltipTotal}}

diff --git a/ui/app/templates/components/database-role-edit.hbs b/ui/app/templates/components/database-role-edit.hbs index 95b8bae09f..4625ebaa81 100644 --- a/ui/app/templates/components/database-role-edit.hbs +++ b/ui/app/templates/components/database-role-edit.hbs @@ -153,7 +153,7 @@ {{/if}}
- + Cancel
diff --git a/ui/app/templates/components/identity/popup-alias.hbs b/ui/app/templates/components/identity/popup-alias.hbs index 8588990415..2a3f3344fe 100644 --- a/ui/app/templates/components/identity/popup-alias.hbs +++ b/ui/app/templates/components/identity/popup-alias.hbs @@ -1,6 +1,6 @@ - {{#let this.params.firstObject as |item|}} + {{#let (get this.params "0") as |item|}}
{{/if}} diff --git a/ui/lib/core/addon/components/masked-input.hbs b/ui/lib/core/addon/components/masked-input.hbs index 550f4d7481..1559845136 100644 --- a/ui/lib/core/addon/components/masked-input.hbs +++ b/ui/lib/core/addon/components/masked-input.hbs @@ -37,7 +37,7 @@ class="copy-button button {{if @displayOnly 'is-compact'}}" data-test-copy-button > - + {{/if}} {{#if @allowDownload}} diff --git a/ui/lib/core/addon/components/modal.hbs b/ui/lib/core/addon/components/modal.hbs index 5349161761..e53100505b 100644 --- a/ui/lib/core/addon/components/modal.hbs +++ b/ui/lib/core/addon/components/modal.hbs @@ -1,5 +1,5 @@ -
+