diff --git a/ui/app/components/database-connection.hbs b/ui/app/components/database-connection.hbs
index 3554a1a00e..2b5d94c9b2 100644
--- a/ui/app/components/database-connection.hbs
+++ b/ui/app/components/database-connection.hbs
@@ -143,7 +143,10 @@
{{/each-in}}
{{/each}}
{{else}}
-
+
+
+
+
{{/if}}
@@ -154,7 +157,10 @@
{{else if (eq @model.isAvailablePlugin false)}}
-
-
-
-
+
+
+
+ Not supported in the UI
+ This database type cannot be viewed in the UI. You will have to use the API or CLI to perform actions here.
+
+
+
+
+
+
{{else}}
{{#each @model.showAttrs as |attr|}}
{{#let attr.options.defaultShown as |defaultDisplay|}}
diff --git a/ui/app/components/database-role-edit.hbs b/ui/app/components/database-role-edit.hbs
index 17cf745a7a..b30ec1daaf 100644
--- a/ui/app/components/database-role-edit.hbs
+++ b/ui/app/components/database-role-edit.hbs
@@ -141,10 +141,10 @@
@modelValidations={{this.modelValidations}}
/>
{{else}}
-
+
+
+
+
{{/if}}
diff --git a/ui/app/components/database-role-setting-form.hbs b/ui/app/components/database-role-setting-form.hbs
index da674ca69e..5c4090fe5b 100644
--- a/ui/app/components/database-role-setting-form.hbs
+++ b/ui/app/components/database-role-setting-form.hbs
@@ -37,7 +37,10 @@
{{/each}}
{{else}}
-
+
+
+
+
{{/if}}
{{! template-lint-configure simple-unless "warn" }}
@@ -51,10 +54,13 @@
{{/each}}
{{else}}
-
+
+
+
+
{{/if}}
{{/unless}}
\ No newline at end of file
diff --git a/ui/app/components/generate-credentials-database.hbs b/ui/app/components/generate-credentials-database.hbs
index 14bb19abe8..716288c210 100644
--- a/ui/app/components/generate-credentials-database.hbs
+++ b/ui/app/components/generate-credentials-database.hbs
@@ -12,20 +12,23 @@
{{! If no role type, that means both static and dynamic requests returned an error }}
{{#unless @roleType}}
-
-
+
-
+
+
+
+
+
{{/unless}}
{{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}}
diff --git a/ui/app/components/mfa/mfa-form.ts b/ui/app/components/mfa/mfa-form.ts
index ded17a6366..c74a662b29 100644
--- a/ui/app/components/mfa/mfa-form.ts
+++ b/ui/app/components/mfa/mfa-form.ts
@@ -6,9 +6,11 @@
import Ember from 'ember';
import Component from '@glimmer/component';
import { service } from '@ember/service';
-import { tracked } from '@glimmer/tracking';
+import { tracked } from 'tracked-built-ins';
+import type { TrackedSet } from 'tracked-built-ins';
import { action } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
import MfaConstraint from 'vault/resources/mfa/constraint';
@@ -54,7 +56,7 @@ export default class MfaForm extends Component {
@tracked error = '';
// Self-enrollment is per MFA method, not per login enforcement (constraint)
// Track method IDs used to fetch a QR code so we don't re-request if a user just enrolled.
- @tracked enrolledMethods = new Set();
+ enrolledMethods: TrackedSet = tracked(Set);
constructor(owner: unknown, args: Args) {
super(owner, args);
@@ -124,7 +126,7 @@ export default class MfaForm extends Component {
await this.args.loginAndTransition.unlinked().perform(response);
} catch (error) {
// Reset enrolled methods if there's an error
- this.enrolledMethods = new Set();
+ this.enrolledMethods.clear();
const errorMsg = errorMessage(error);
const codeUsed = errorMsg.includes('code already used');
const rateLimit = errorMsg.includes('maximum TOTP validation attempts');
@@ -141,30 +143,32 @@ export default class MfaForm extends Component {
}
});
- fetchQrCode = task(async (mfa_method_id: string, constraint: MfaConstraint) => {
- // Self-enrollment is an enterprise only feature
- if (this.version.isCommunity) return;
+ fetchQrCode = task(
+ waitFor(async (mfa_method_id: string, constraint: MfaConstraint) => {
+ // Self-enrollment is an enterprise only feature
+ if (this.version.isCommunity) return;
- const adapter = this.store.adapterFor('application');
- const { mfaRequirement } = this.args.authData;
- try {
- const { data } = await adapter.ajax('/v1/identity/mfa/method/totp/self-enroll', 'POST', {
- unauthenticated: true,
- data: { mfa_method_id, mfa_request_id: mfaRequirement.mfa_request_id },
- });
- if (data?.url) {
- // Set QR code which recomputes currentSelfEnrollConstraint and renders it for the user to scan
- constraint.qrCode = data.url;
- // Add mfa_method_id to list of already enrolled methods for client-side tracking
- this.enrolledMethods.add(mfa_method_id);
- return;
+ const adapter = this.store.adapterFor('application');
+ const { mfaRequirement } = this.args.authData;
+ try {
+ const { data } = await adapter.ajax('/v1/identity/mfa/method/totp/self-enroll', 'POST', {
+ unauthenticated: true,
+ data: { mfa_method_id, mfa_request_id: mfaRequirement.mfa_request_id },
+ });
+ if (data?.url) {
+ // Set QR code which recomputes currentSelfEnrollConstraint and renders it for the user to scan
+ constraint.qrCode = data.url;
+ // Add mfa_method_id to list of already enrolled methods for client-side tracking
+ this.enrolledMethods.add(mfa_method_id);
+ return;
+ }
+ // Not sure it's realistic to get here without the endpoint throwing an error, but just in case!
+ this.error = 'There was a problem generating the QR code. Please try again.';
+ } catch (error) {
+ this.error = errorMessage(error);
}
- // Not sure it's realistic to get here without the endpoint throwing an error, but just in case!
- this.error = 'There was a problem generating the QR code. Please try again.';
- } catch (error) {
- this.error = errorMessage(error);
- }
- });
+ })
+ );
newCodeDelay = task(async (errorMessage) => {
let delay;
diff --git a/ui/app/controllers/vault.js b/ui/app/controllers/vault.js
index 2c83a2cd26..cf2af5ec71 100644
--- a/ui/app/controllers/vault.js
+++ b/ui/app/controllers/vault.js
@@ -4,8 +4,9 @@
*/
import Controller from '@ember/controller';
+import { tracked } from '@glimmer/tracking';
export default class VaultController extends Controller {
queryParams = [{ redirectTo: 'redirect_to' }];
- redirectTo = '';
+ @tracked redirectTo = '';
}
diff --git a/ui/app/initializers/enable-engines.js b/ui/app/initializers/enable-engines.js
deleted file mode 100644
index c47ad9cff3..0000000000
--- a/ui/app/initializers/enable-engines.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import config from '../config/environment';
-
-export function initialize(/* application */) {
- // attach mount hooks to the environment config
- // context will be the router DSL
- config.addRootMounts = function () {
- this.mount('replication');
- };
-}
-
-export default {
- initialize,
-};
diff --git a/ui/app/mixins/cluster-route.js b/ui/app/mixins/cluster-route.js
deleted file mode 100644
index 5d948cc605..0000000000
--- a/ui/app/mixins/cluster-route.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import { service } from '@ember/service';
-import Mixin from '@ember/object/mixin';
-import RSVP from 'rsvp';
-import {
- INIT,
- UNSEAL,
- AUTH,
- CLUSTER,
- CLUSTER_INDEX,
- OIDC_CALLBACK,
- OIDC_PROVIDER,
- NS_OIDC_PROVIDER,
- DR_REPLICATION_SECONDARY,
- DR_REPLICATION_SECONDARY_DETAILS,
- EXCLUDED_REDIRECT_URLS,
- REDIRECT,
-} from 'vault/lib/route-paths';
-
-export default Mixin.create({
- auth: service(),
- store: service(),
- router: service(),
-
- transitionToTargetRoute(transition = {}) {
- const targetRoute = this.targetRouteName(transition);
- if (
- targetRoute &&
- targetRoute !== this.routeName &&
- targetRoute !== transition.targetName &&
- targetRoute !== this.router.currentRouteName
- ) {
- // there may be query params so check for inclusion rather than exact match
- const isExcluded = EXCLUDED_REDIRECT_URLS.find((url) => this.router.currentURL?.includes(url));
- if (
- // only want to redirect if we're going to authenticate
- targetRoute === AUTH &&
- transition.targetName !== CLUSTER_INDEX &&
- !isExcluded
- ) {
- return this.router.transitionTo(targetRoute, {
- queryParams: { redirect_to: this.router.currentURL },
- });
- }
- return this.router.transitionTo(targetRoute);
- }
-
- return RSVP.resolve();
- },
-
- beforeModel(transition) {
- return this.transitionToTargetRoute(transition);
- },
-
- clusterModel() {
- return this.modelFor(CLUSTER) || this.store.peekRecord('cluster', 'vault');
- },
-
- authToken() {
- return this.auth.currentToken;
- },
-
- hasKeyData() {
- /* eslint-disable-next-line ember/no-controller-access-in-routes */
- return !!this.controllerFor(INIT).keyData;
- },
-
- targetRouteName(transition) {
- const cluster = this.clusterModel();
- const isAuthed = this.authToken();
- if (cluster.needsInit) {
- return INIT;
- }
- if (this.hasKeyData() && this.routeName !== UNSEAL && this.routeName !== AUTH) {
- return INIT;
- }
- if (cluster.sealed) {
- return UNSEAL;
- }
- if (cluster?.dr?.isSecondary) {
- if (transition && transition.targetName === DR_REPLICATION_SECONDARY_DETAILS) {
- return DR_REPLICATION_SECONDARY_DETAILS;
- }
- if (this.router.currentRouteName === DR_REPLICATION_SECONDARY_DETAILS) {
- return DR_REPLICATION_SECONDARY_DETAILS;
- }
-
- return DR_REPLICATION_SECONDARY;
- }
- if (!isAuthed) {
- if ((transition && transition.targetName === OIDC_PROVIDER) || this.routeName === OIDC_PROVIDER) {
- return OIDC_PROVIDER;
- }
- if ((transition && transition.targetName === NS_OIDC_PROVIDER) || this.routeName === NS_OIDC_PROVIDER) {
- return NS_OIDC_PROVIDER;
- }
- if ((transition && transition.targetName === OIDC_CALLBACK) || this.routeName === OIDC_CALLBACK) {
- return OIDC_CALLBACK;
- }
- return AUTH;
- }
- if (
- (!cluster.needsInit && this.routeName === INIT) ||
- (!cluster.sealed && this.routeName === UNSEAL) ||
- (!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY)
- ) {
- return CLUSTER;
- }
- if (isAuthed && this.routeName === AUTH) {
- // if you're already authed and you wanna go to auth, you probably want to redirect
- return REDIRECT;
- }
- return null;
- },
-});
diff --git a/ui/app/mixins/model-boundary-route.js b/ui/app/mixins/model-boundary-route.js
deleted file mode 100644
index 3cd425839f..0000000000
--- a/ui/app/mixins/model-boundary-route.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-// meant for use mixed-in to a Route file
-//
-// When a route is deactivated, this mixin clears the Ember Data store of
-// models of type specified by the required param `modelType`.
-//
-// example:
-// Using this as with a modelType of `datacenter` on the infrastructure
-// route will cause all `datacenter` models to get unloaded when the
-// infrastructure route is navigated away from.
-
-import Route from '@ember/routing/route';
-
-import { isPresent } from '@ember/utils';
-import { warn } from '@ember/debug';
-import { on } from '@ember/object/evented';
-import Mixin from '@ember/object/mixin';
-
-export default Mixin.create({
- modelType: null,
- modelTypes: null,
-
- verifyProps: on('init', function () {
- var modelType = this.modelType;
- var modelTypes = this.modelTypes;
- warn(
- 'No `modelType` or `modelTypes` specified for `' +
- this.toString() +
- '`. Check to make sure you still need to use the `model-boundary-route` mixin.',
- isPresent(modelType) || isPresent(modelTypes),
- { id: 'model-boundary-init' }
- );
-
- warn(
- 'Expected `model-boundary-route` to be used on an Ember.Route, not `' + this.toString() + '`.',
- this instanceof Route,
- { id: 'mode-boundary-is-route' }
- );
- }),
-
- clearModelCache: on('deactivate', function () {
- var modelType = this.modelType;
- var modelTypes = this.modelTypes;
-
- if (!modelType && !modelTypes) {
- warn(
- 'Attempted to clear store clear store cache when leaving `' +
- this.routeName +
- '`, but no `modelType` or `modelTypes` was specified.',
- isPresent(modelType),
- { id: 'model-boundary-clear' }
- );
- 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);
- }
- if (modelTypes) {
- modelTypes.forEach((type) => {
- this.store.unloadAll(type);
- });
- }
- }),
-});
diff --git a/ui/app/router.js b/ui/app/router.js
index 02e258f550..5610b2eff5 100644
--- a/ui/app/router.js
+++ b/ui/app/router.js
@@ -223,10 +223,7 @@ Router.map(function () {
this.route('replication-dr-promote', function () {
this.route('details');
});
- if (config.addRootMounts) {
- config.addRootMounts.call(this);
- }
-
+ this.mount('replication');
this.route('not-found', { path: '/*path' });
});
this.route('not-found', { path: '/*path' });
diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js
index b4800af72d..593e2f16ce 100644
--- a/ui/app/routes/vault/cluster.js
+++ b/ui/app/routes/vault/cluster.js
@@ -4,19 +4,37 @@
*/
import { service } from '@ember/service';
-import { computed } from '@ember/object';
-import { reject } from 'rsvp';
import Route from '@ember/routing/route';
+import { reject } from 'rsvp';
import { task, timeout } from 'ember-concurrency';
import Ember from 'ember';
import getStorage from '../../lib/token-storage';
import localStorage from 'vault/lib/local-storage';
-import ClusterRoute from 'vault/mixins/cluster-route';
-import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
+import clearModelCache from 'vault/utils/shared-model-boundary';
import { assert } from '@ember/debug';
import { v4 as uuidv4 } from 'uuid';
+import {
+ INIT,
+ UNSEAL,
+ AUTH,
+ CLUSTER_INDEX,
+ OIDC_CALLBACK,
+ OIDC_PROVIDER,
+ NS_OIDC_PROVIDER,
+ DR_REPLICATION_SECONDARY,
+ DR_REPLICATION_SECONDARY_DETAILS,
+ EXCLUDED_REDIRECT_URLS,
+} from 'vault/lib/route-paths';
+
+const CLUSTER_STATE = {
+ UNINITIALIZED: 'uninitialized',
+ SEALED: 'sealed',
+ DR_SECONDARY: 'dr-secondary',
+ ACTIVE: 'active',
+};
+
const POLL_INTERVAL_MS = 10000;
export const getManagedNamespace = (nsParam, root) => {
@@ -29,34 +47,33 @@ export const getManagedNamespace = (nsParam, root) => {
return `${root}/${nsParam}`;
};
-export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
- auth: service(),
- api: service(),
- analytics: service(),
- currentCluster: service(),
- customMessages: service(),
- flagsService: service('flags'),
- namespaceService: service('namespace'),
- permissions: service(),
- router: service(),
- store: service(),
- version: service(),
- modelTypes: computed(function () {
- return ['node', 'secret', 'secret-engine'];
- }),
+export default class ClusterRoute extends Route {
+ @service auth;
+ @service api;
+ @service analytics;
+ @service currentCluster;
+ @service customMessages;
+ @service('flags') flagsService;
+ @service('namespace') namespaceService;
+ @service permissions;
+ @service router;
+ @service store;
+ @service version;
- queryParams: {
+ modelTypes = ['node', 'secret', 'secret-engine'];
+
+ queryParams = {
namespaceQueryParam: {
refreshModel: true,
},
- },
+ };
getClusterId(params) {
const { cluster_name } = params;
const records = this.store.peekAll('cluster');
const cluster = records.find((record) => record.name === cluster_name);
return cluster?.id ?? null;
- },
+ }
async beforeModel() {
const params = this.paramsFor(this.routeName);
@@ -98,7 +115,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
} else {
return reject({ httpStatus: 404, message: 'not found', path: params.cluster_name });
}
- },
+ }
model(params) {
// if a user's browser settings block localStorage they will be unable to use Vault. The method will throw the error and the rest of the application will not load.
@@ -106,9 +123,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
const id = this.getClusterId(params);
return this.store.findRecord('cluster', id);
- },
+ }
- poll: task(function* () {
+ poll = task({ keepLatest: true }, async () => {
+ // eslint-disable-next-line no-constant-condition
while (true) {
// In test mode, polling causes acceptance tests to hang due to never-settling promises.
// To avoid this, polling is disabled during tests.
@@ -117,22 +135,20 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
if (Ember.testing) {
return;
}
- yield timeout(POLL_INTERVAL_MS);
+ await timeout(POLL_INTERVAL_MS);
try {
/* eslint-disable-next-line ember/no-controller-access-in-routes */
- yield this.controller.model.reload();
- yield this.transitionToTargetRoute();
+ await this.controller.model.reload();
+ await this.transitionToTargetRoute();
} catch (e) {
// we want to keep polling here
}
}
- })
- .cancelOn('deactivate')
- .keepLatest(),
+ });
// Note: do not make this afterModel hook async, it will break the DR secondary flow.
afterModel(model, transition) {
- this._super(...arguments);
+ super.afterModel(...arguments);
this.currentCluster.setCluster(model);
if (model.needsInit && this.auth.currentToken) {
@@ -156,7 +172,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
this.addAnalyticsService(model);
return this.transitionToTargetRoute(transition);
- },
+ }
async addAnalyticsService(model) {
// identify user for analytics service
@@ -187,19 +203,91 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
console.error('unable to start analytics', e);
}
}
- },
+ }
+
+ deactivate() {
+ clearModelCache(this.store, this.modelTypes);
+ this.poll.cancelAll();
+ }
setupController() {
- this._super(...arguments);
+ super.setupController(...arguments);
this.poll.perform();
- },
+ }
- actions: {
- error(e) {
- if (e.httpStatus === 503 && e.errors[0] === 'Vault is sealed') {
- this.refresh();
+ error = (e) => {
+ if (e.httpStatus === 503 && e.errors[0] === 'Vault is sealed') {
+ this.refresh();
+ }
+ return true;
+ };
+
+ get clusterState() {
+ const cluster = this.modelFor(this.routeName);
+ switch (true) {
+ case cluster.needsInit:
+ return CLUSTER_STATE.UNINITIALIZED;
+ case cluster.sealed:
+ return CLUSTER_STATE.SEALED;
+ case cluster.dr?.isSecondary:
+ return CLUSTER_STATE.DR_SECONDARY;
+ default:
+ return CLUSTER_STATE.ACTIVE;
+ }
+ }
+
+ get allowedRedirectRoutes() {
+ const state = this.clusterState;
+ const isAuthenticated = !!this.auth.currentToken;
+
+ switch (true) {
+ case state === CLUSTER_STATE.UNINITIALIZED:
+ return [INIT];
+ case state === CLUSTER_STATE.SEALED:
+ // here we allow init so that we don't redirect users who haven't downloaded keys after init
+ return [UNSEAL, INIT];
+ case state === CLUSTER_STATE.DR_SECONDARY:
+ return [DR_REPLICATION_SECONDARY, DR_REPLICATION_SECONDARY_DETAILS];
+ case isAuthenticated === false:
+ return [AUTH, OIDC_PROVIDER, NS_OIDC_PROVIDER, OIDC_CALLBACK];
+ default:
+ return [null];
+ }
+ }
+
+ targetRouteName(transition) {
+ const currentRouteName = this.router.currentRouteName;
+ const allowedRoutes = this.allowedRedirectRoutes;
+
+ // if the target route or current route is in the allowed routes, go there, otherwise choose the first allowed route
+ return (
+ allowedRoutes?.find((r) => transition?.targetName === r || currentRouteName === r) || allowedRoutes[0]
+ );
+ }
+
+ transitionToTargetRoute(transition = {}) {
+ const targetRoute = this.targetRouteName(transition);
+ if (
+ targetRoute &&
+ targetRoute !== this.routeName &&
+ targetRoute !== transition.targetName &&
+ targetRoute !== this.router.currentRouteName
+ ) {
+ // there may be query params so check for inclusion rather than exact match
+ const isExcluded = EXCLUDED_REDIRECT_URLS.find((url) => this.router.currentURL?.includes(url));
+ if (
+ // only want to redirect if we're going to authenticate
+ targetRoute === AUTH &&
+ transition.targetName !== CLUSTER_INDEX &&
+ !isExcluded
+ ) {
+ return this.router.transitionTo(targetRoute, {
+ queryParams: { redirect_to: this.router.currentURL },
+ });
}
- return true;
- },
- },
-});
+ return this.router.transitionTo(targetRoute);
+ }
+
+ return Promise.resolve();
+ }
+}
diff --git a/ui/app/routes/vault/cluster/access.js b/ui/app/routes/vault/cluster/access.js
index 799307b6ce..5296046120 100644
--- a/ui/app/routes/vault/cluster/access.js
+++ b/ui/app/routes/vault/cluster/access.js
@@ -3,16 +3,20 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { computed } from '@ember/object';
+import { service } from '@ember/service';
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
+import clearModelCache from 'vault/utils/shared-model-boundary';
+
+export default class AccessRoute extends Route {
+ @service store;
+
+ modelTypes = ['capabilities', 'control-group', 'identity/group', 'identity/group-alias', 'identity/alias'];
-export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
- modelTypes: computed(function () {
- return ['capabilities', 'control-group', 'identity/group', 'identity/group-alias', 'identity/alias'];
- }),
model() {
return {};
- },
-});
+ }
+
+ deactivate() {
+ clearModelCache(this.store, this.modelTypes);
+ }
+}
diff --git a/ui/app/routes/vault/cluster/access/leases.js b/ui/app/routes/vault/cluster/access/leases.js
index 4c966d9718..b159842d01 100644
--- a/ui/app/routes/vault/cluster/access/leases.js
+++ b/ui/app/routes/vault/cluster/access/leases.js
@@ -3,14 +3,13 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
import { service } from '@ember/service';
+import Route from '@ember/routing/route';
-export default Route.extend(ClusterRoute, {
- store: service(),
+export default class LeasesRoute extends Route {
+ @service store;
model() {
return this.store.findRecord('capabilities', 'sys/leases/lookup/');
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/auth.js b/ui/app/routes/vault/cluster/auth.js
index 6422ae0003..8e0e0ddf7d 100644
--- a/ui/app/routes/vault/cluster/auth.js
+++ b/ui/app/routes/vault/cluster/auth.js
@@ -4,13 +4,14 @@
*/
import { service } from '@ember/service';
-import ClusterRouteBase from './cluster-route-base';
+import Route from '@ember/routing/route';
import config from 'vault/config/environment';
import { supportedTypes } from 'vault/utils/auth-form-helpers';
import { sanitizePath } from 'core/utils/sanitize-path';
import AuthMethodResource from 'vault/resources/auth/method';
+import { REDIRECT } from 'vault/lib/route-paths';
-export default class AuthRoute extends ClusterRouteBase {
+export default class AuthRoute extends Route {
queryParams = {
authMount: { replace: true, refreshModel: true },
wrapped_token: { refreshModel: true },
@@ -20,13 +21,15 @@ export default class AuthRoute extends ClusterRouteBase {
@service auth;
@service flashMessages;
@service namespace;
+ @service router;
@service store;
@service version;
beforeModel() {
- return super.beforeModel().then(() => {
- return this.version.fetchFeatures();
- });
+ if (this.auth.currentToken) {
+ return this.router.replaceWith(REDIRECT);
+ }
+ return this.version.fetchFeatures();
}
async model(params) {
@@ -62,13 +65,14 @@ export default class AuthRoute extends ClusterRouteBase {
redirect(model, transition) {
if (model?.unwrapResponse) {
// handles the transition
+ /* eslint-disable-next-line ember/no-controller-access-in-routes */
return this.controllerFor('vault.cluster.auth').loginAndTransition.perform(model.unwrapResponse);
}
const hasQueryParam = transition.to?.queryParams?.with;
const isInvalid = !model.directLinkData;
if (hasQueryParam && isInvalid) {
// redirect user and clear out the query param if it's invalid
- this.router.replaceWith(this.routeName, { queryParams: { authMount: null } });
+ return this.router.replaceWith(this.routeName, { queryParams: { authMount: '' } });
}
}
@@ -80,6 +84,7 @@ export default class AuthRoute extends ClusterRouteBase {
return await this.auth.authSuccess(clusterId, authData);
} catch (e) {
const { message } = await this.api.parseError(e);
+ /* eslint-disable-next-line ember/no-controller-access-in-routes */
this.controllerFor('vault.cluster.auth').unwrapTokenError = message;
}
}
@@ -125,7 +130,7 @@ export default class AuthRoute extends ClusterRouteBase {
/*
In older versions of Vault, the "with" query param could refer to either the auth mount path or the type
- (which may be the same, since the default mount path *is* the type).
+ (which may be the same, since the default mount path *is* the type).
For backward compatibility, we handle both scenarios.
→ If `authMount` matches a visible auth mount the method will assume that mount path to login and render as the default in the login form.
→ If `authMount` matches a supported auth type (and the mount does not have `listing_visibility="unauth"`), that type is preselected in the login form.
diff --git a/ui/app/routes/vault/cluster/cluster-route-base.js b/ui/app/routes/vault/cluster/cluster-route-base.js
index 427a688df5..06825a358a 100644
--- a/ui/app/routes/vault/cluster/cluster-route-base.js
+++ b/ui/app/routes/vault/cluster/cluster-route-base.js
@@ -7,17 +7,16 @@
// all of the CLUSTER_ROUTES that are states before you can use vault
//
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
/**
* @type Class
*/
-export default Route.extend(ClusterRoute, {
+export default class ClusterBaseRoute extends Route {
model() {
return this.modelFor('vault.cluster');
- },
+ }
resetController(controller) {
controller.reset && controller.reset();
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/dashboard.js b/ui/app/routes/vault/cluster/dashboard.js
index 27baf00dac..e30fb9cc39 100644
--- a/ui/app/routes/vault/cluster/dashboard.js
+++ b/ui/app/routes/vault/cluster/dashboard.js
@@ -3,14 +3,12 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Route from '@ember/routing/route';
import { service } from '@ember/service';
-// eslint-disable-next-line ember/no-mixins
-import ClusterRoute from 'vault/mixins/cluster-route';
import { action } from '@ember/object';
+import Route from '@ember/routing/route';
import SecretsEngineResource from 'vault/resources/secrets/engine';
-export default class VaultClusterDashboardRoute extends Route.extend(ClusterRoute) {
+export default class VaultClusterDashboardRoute extends Route {
@service store;
@service namespace;
@service version;
diff --git a/ui/app/routes/vault/cluster/init.js b/ui/app/routes/vault/cluster/init.js
index edcd34b8d1..dffd1b2b03 100644
--- a/ui/app/routes/vault/cluster/init.js
+++ b/ui/app/routes/vault/cluster/init.js
@@ -3,6 +3,18 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import ClusterRoute from './cluster-route-base';
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+import { CLUSTER } from 'vault/lib/route-paths';
-export default ClusterRoute;
+export default class InitRoute extends Route {
+ @service router;
+
+ beforeModel() {
+ const cluster = this.modelFor(CLUSTER);
+ // if it doesn't need init, nav to cluster route
+ if (!cluster.needsInit) {
+ return this.router.replaceWith(CLUSTER);
+ }
+ }
+}
diff --git a/ui/app/routes/vault/cluster/license.js b/ui/app/routes/vault/cluster/license.js
index 3868c8c77a..bcf3716bfd 100644
--- a/ui/app/routes/vault/cluster/license.js
+++ b/ui/app/routes/vault/cluster/license.js
@@ -3,22 +3,21 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
import { service } from '@ember/service';
+import Route from '@ember/routing/route';
-export default Route.extend(ClusterRoute, {
- store: service(),
- version: service(),
- router: service(),
+export default class LicenseRoute extends Route {
+ @service store;
+ @service version;
+ @service router;
beforeModel() {
if (this.version.isCommunity) {
this.router.transitionTo('vault.cluster');
}
- },
+ }
model() {
return this.store.queryRecord('license', {});
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js
index 4d0fbee7d8..ba9c4cd6e2 100644
--- a/ui/app/routes/vault/cluster/logout.js
+++ b/ui/app/routes/vault/cluster/logout.js
@@ -4,25 +4,23 @@
*/
import Ember from 'ember';
-import { computed } from '@ember/object';
import { service } from '@ember/service';
import Route from '@ember/routing/route';
-import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
+import clearModelCache from 'vault/utils/shared-model-boundary';
-export default Route.extend(ModelBoundaryRoute, {
- auth: service(),
- controlGroup: service(),
- flashMessages: service(),
- console: service(),
- permissions: service(),
- namespaceService: service('namespace'),
- router: service(),
- version: service(),
- customMessages: service(),
+export default class LogoutRoute extends Route {
+ @service auth;
+ @service store;
+ @service controlGroup;
+ @service flashMessages;
+ @service console;
+ @service permissions;
+ @service('namespace') namespaceService;
+ @service router;
+ @service version;
+ @service customMessages;
- modelTypes: computed(function () {
- return ['secret', 'secret-engine'];
- }),
+ modelTypes = ['secret', 'secret-engine'];
beforeModel({ to: { queryParams } }) {
const ns = this.namespaceService.path;
@@ -49,5 +47,8 @@ export default Route.extend(ModelBoundaryRoute, {
const { cluster_name } = this.paramsFor('vault.cluster');
location.assign(this.router.urlFor('vault.cluster.auth', cluster_name, { queryParams }));
}
- },
-});
+ }
+ deactivate() {
+ clearModelCache(this.store, this.modelTypes);
+ }
+}
diff --git a/ui/app/routes/vault/cluster/policies.js b/ui/app/routes/vault/cluster/policies.js
index a80ee8d840..a4cedb17ec 100644
--- a/ui/app/routes/vault/cluster/policies.js
+++ b/ui/app/routes/vault/cluster/policies.js
@@ -5,18 +5,17 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-
const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
-export default Route.extend(ClusterRoute, {
- version: service(),
+export default class PoliciesRoute extends Route {
+ @service version;
+ @service router;
beforeModel() {
return this.version.fetchFeatures().then(() => {
- return this._super(...arguments);
+ return super.beforeModel(...arguments);
});
- },
+ }
model(params) {
const policyType = params.type;
@@ -24,5 +23,5 @@ export default Route.extend(ClusterRoute, {
return this.router.transitionTo(this.routeName, ALLOWED_TYPES[0]);
}
return {};
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/policies/index.js b/ui/app/routes/vault/cluster/policies/index.js
index 79a7f134f1..4398e3ff1d 100644
--- a/ui/app/routes/vault/cluster/policies/index.js
+++ b/ui/app/routes/vault/cluster/policies/index.js
@@ -5,10 +5,9 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
import ListRoute from 'core/mixins/list-route';
-export default Route.extend(ClusterRoute, ListRoute, {
+export default Route.extend(ListRoute, {
pagination: service(),
version: service(),
diff --git a/ui/app/routes/vault/cluster/policy.js b/ui/app/routes/vault/cluster/policy.js
index 694683178e..93a3731ca5 100644
--- a/ui/app/routes/vault/cluster/policy.js
+++ b/ui/app/routes/vault/cluster/policy.js
@@ -5,17 +5,17 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
const ALLOWED_TYPES = ['acl', 'egp', 'rgp'];
-export default Route.extend(ClusterRoute, {
- version: service(),
+export default class PolicyRoute extends Route {
+ @service version;
+ @service router;
beforeModel() {
return this.version.fetchFeatures().then(() => {
- return this._super(...arguments);
+ return super.beforeModel(...arguments);
});
- },
+ }
model(params) {
const policyType = params.type;
if (!ALLOWED_TYPES.includes(policyType)) {
@@ -25,5 +25,5 @@ export default Route.extend(ClusterRoute, {
return this.router.transitionTo('vault.cluster.policies', policyType);
}
return {};
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/replication-dr-promote.js b/ui/app/routes/vault/cluster/replication-dr-promote.js
new file mode 100644
index 0000000000..d904ad47b9
--- /dev/null
+++ b/ui/app/routes/vault/cluster/replication-dr-promote.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+import { CLUSTER } from 'vault/lib/route-paths';
+
+export default class ReplicationDrPromote extends Route {
+ @service router;
+
+ beforeModel() {
+ const cluster = this.modelFor(CLUSTER);
+ // if it's not a dr secondary, go back to cluster
+ if (!cluster?.dr?.isSecondary) {
+ return this.router.replaceWith(CLUSTER);
+ }
+ }
+}
diff --git a/ui/app/routes/vault/cluster/replication-dr-promote/details.js b/ui/app/routes/vault/cluster/replication-dr-promote/details.js
index 36db679ae1..d4a28e870c 100644
--- a/ui/app/routes/vault/cluster/replication-dr-promote/details.js
+++ b/ui/app/routes/vault/cluster/replication-dr-promote/details.js
@@ -4,12 +4,11 @@
*/
import { service } from '@ember/service';
-import Base from '../cluster-route-base';
+import Route from '@ember/routing/route';
-export default Base.extend({
- replicationMode: service(),
+export default class ReplicationDetailsRoute extends Route {
+ @service replicationMode;
beforeModel() {
- this._super(...arguments);
this.replicationMode.setMode('dr');
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/replication-dr-promote/index.js b/ui/app/routes/vault/cluster/replication-dr-promote/index.js
index 36db679ae1..15fd6845c5 100644
--- a/ui/app/routes/vault/cluster/replication-dr-promote/index.js
+++ b/ui/app/routes/vault/cluster/replication-dr-promote/index.js
@@ -4,12 +4,11 @@
*/
import { service } from '@ember/service';
-import Base from '../cluster-route-base';
+import Route from '@ember/routing/route';
-export default Base.extend({
- replicationMode: service(),
+export default class ReplicationIndexRoute extends Route {
+ @service replicationMode;
beforeModel() {
- this._super(...arguments);
this.replicationMode.setMode('dr');
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/secrets.js b/ui/app/routes/vault/cluster/secrets.js
deleted file mode 100644
index 6ba3f5eabf..0000000000
--- a/ui/app/routes/vault/cluster/secrets.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-
-export default Route.extend(ClusterRoute);
diff --git a/ui/app/routes/vault/cluster/settings.js b/ui/app/routes/vault/cluster/settings.js
index 925101c1da..e9fa73fb89 100644
--- a/ui/app/routes/vault/cluster/settings.js
+++ b/ui/app/routes/vault/cluster/settings.js
@@ -4,10 +4,9 @@
*/
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-export default Route.extend(ClusterRoute, {
+export default class SettingsRoute extends Route {
model() {
return {};
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/storage.js b/ui/app/routes/vault/cluster/storage.js
index a312830ccc..37279f2013 100644
--- a/ui/app/routes/vault/cluster/storage.js
+++ b/ui/app/routes/vault/cluster/storage.js
@@ -4,22 +4,15 @@
*/
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
import { service } from '@ember/service';
-export default Route.extend(ClusterRoute, {
- store: service(),
+export default class StorageRoute extends Route {
+ @service store;
model() {
// findAll method will return all records in store as well as response from server
// when removing a peer via the cli, stale records would continue to appear until refresh
// query method will only return records from response
return this.store.query('server', {});
- },
-
- actions: {
- doRefresh() {
- this.refresh();
- },
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/tools.js b/ui/app/routes/vault/cluster/tools.js
index 23ae6296b0..98f3447790 100644
--- a/ui/app/routes/vault/cluster/tools.js
+++ b/ui/app/routes/vault/cluster/tools.js
@@ -4,10 +4,9 @@
*/
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-export default Route.extend(ClusterRoute, {
+export default class ToolsRoute extends Route {
model() {
return this.modelFor('vault.cluster');
- },
-});
+ }
+}
diff --git a/ui/app/routes/vault/cluster/unseal.js b/ui/app/routes/vault/cluster/unseal.js
index 0aac55a8af..9821f80ef0 100644
--- a/ui/app/routes/vault/cluster/unseal.js
+++ b/ui/app/routes/vault/cluster/unseal.js
@@ -3,6 +3,18 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import ClusterRoute from './cluster-route-base';
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+import { CLUSTER } from 'vault/lib/route-paths';
-export default ClusterRoute.extend({});
+export default class UnsealRoute extends Route {
+ @service router;
+
+ beforeModel() {
+ const cluster = this.modelFor(CLUSTER);
+ // if it's not sealed, we don't need to unseal
+ if (!cluster.sealed) {
+ return this.router.replaceWith(CLUSTER);
+ }
+ }
+}
diff --git a/ui/app/utils/shared-model-boundary.js b/ui/app/utils/shared-model-boundary.js
new file mode 100644
index 0000000000..17d7bf7753
--- /dev/null
+++ b/ui/app/utils/shared-model-boundary.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+export default function clearModelCache(store, modelType) {
+ if (!modelType) {
+ return;
+ }
+ const modelTypes = Array.isArray(modelType) ? modelType : [modelType];
+ if (store.isDestroyed || store.isDestroying) {
+ // Prevent unload attempt after test teardown, resulting in test failure
+ return;
+ }
+ modelTypes.forEach((type) => {
+ store.unloadAll(type);
+ });
+}
diff --git a/ui/lib/replication/addon/routes/application.js b/ui/lib/replication/addon/routes/application.js
index 9c81eb11f9..289102e9c6 100644
--- a/ui/lib/replication/addon/routes/application.js
+++ b/ui/lib/replication/addon/routes/application.js
@@ -4,16 +4,14 @@
*/
import { service } from '@ember/service';
-import { setProperties } from '@ember/object';
import Route from '@ember/routing/route';
-import ClusterRoute from 'vault/mixins/cluster-route';
-export default Route.extend(ClusterRoute, {
- version: service(),
- store: service(),
- auth: service(),
- router: service('app-router'),
- capabilities: service(),
+export default class ApplicationRoute extends Route {
+ @service version;
+ @service store;
+ @service auth;
+ @service('app-router') router;
+ @service capabilities;
async fetchCapabilities() {
const enablePath = (type, cluster) => `sys/replication/${type}/${cluster}/enable`;
@@ -29,21 +27,19 @@ export default Route.extend(ClusterRoute, {
canEnablePrimaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
canEnableSecondaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
};
- },
+ }
beforeModel() {
if (this.auth.activeCluster.replicationRedacted) {
// disallow replication access if endpoints are redacted
return this.router.transitionTo('vault.cluster');
}
- return this.version.fetchFeatures().then(() => {
- return this._super(...arguments);
- });
- },
+ return this.version.fetchFeatures();
+ }
model() {
return this.auth.activeCluster;
- },
+ }
async afterModel(model) {
const {
@@ -53,17 +49,10 @@ export default Route.extend(ClusterRoute, {
canEnableSecondaryPerformance,
} = await this.fetchCapabilities();
- setProperties(model, {
- canEnablePrimaryDr,
- canEnableSecondaryDr,
- canEnablePrimaryPerformance,
- canEnableSecondaryPerformance,
- });
+ model.canEnablePrimaryDr = canEnablePrimaryDr;
+ model.canEnableSecondaryDr = canEnableSecondaryDr;
+ model.canEnablePrimaryPerformance = canEnablePrimaryPerformance;
+ model.canEnableSecondaryPerformance = canEnableSecondaryPerformance;
return model;
- },
- actions: {
- refresh() {
- this.refresh();
- },
- },
-});
+ }
+}
diff --git a/ui/lib/replication/addon/routes/mode/index.js b/ui/lib/replication/addon/routes/mode/index.js
index 2bd8cffec4..05876ff9b5 100644
--- a/ui/lib/replication/addon/routes/mode/index.js
+++ b/ui/lib/replication/addon/routes/mode/index.js
@@ -30,12 +30,4 @@ export default Route.extend({
return cluster;
});
},
-
- afterModel(model) {
- const replicationMode = this.paramsFor('mode').replication_mode;
- const cluster = model[replicationMode];
- if (!cluster.isPrimary || cluster.replicationDisabled || cluster.replicationUnsupported) {
- return this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
- }
- },
});
diff --git a/ui/tests/acceptance/auth/auth-login-test.js b/ui/tests/acceptance/auth/auth-login-test.js
index 528e243955..94a20c0776 100644
--- a/ui/tests/acceptance/auth/auth-login-test.js
+++ b/ui/tests/acceptance/auth/auth-login-test.js
@@ -291,7 +291,7 @@ module('Acceptance | auth login', function (hooks) {
assert.expect(6);
this.authType = 'oidc';
this.expectedPayload = {
- redirect_uri: 'http://localhost:7357/ui/vault/auth/custom-oidc/oidc/callback',
+ redirect_uri: `${window.location.origin}/ui/vault/auth/custom-oidc/oidc/callback`,
role: 'some-dev',
};
this.server.post('/auth/custom-oidc/oidc/auth_url', (schema, req) => {
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
index ee95cab4f9..f9bbec1032 100644
--- a/ui/tests/acceptance/enterprise-replication-test.js
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -3,7 +3,16 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { click, fillIn, findAll, currentURL, visit, settled, waitFor } from '@ember/test-helpers';
+import {
+ click,
+ fillIn,
+ findAll,
+ currentURL,
+ currentRouteName,
+ visit,
+ settled,
+ waitFor,
+} from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
@@ -84,7 +93,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
assert.ok(findAll('[data-test-demote-warning]').length, 'displays the demotion warning');
});
- test('DR primary: shows empty state when secondary mode is not enabled and we navigated to the secondary details page', async function (assert) {
+ test('DR primary: redirects to cluster route when navigating to the secondary details page', async function (assert) {
// enable dr replication
await visit('/vault/replication/dr');
@@ -94,16 +103,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await settled(); // eslint-disable-line
await pollCluster(this.owner);
await visit('/vault/replication-dr-promote/details');
-
- assert
- .dom('[data-test-component="empty-state"]')
- .exists('Empty state is shown when no secondary is configured');
- assert
- .dom('[data-test-empty-state-message]')
- .hasText(
- 'This Disaster Recovery secondary has not been enabled. You can do so from the Disaster Recovery Primary.',
- 'Renders the correct message for when a primary is enabled but no secondary is configured and we have navigated to the secondary details page.'
- );
+ assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'redirects to the cluster route');
});
test('DR primary: runs analytics service when enabled', async function (assert) {
@@ -126,22 +126,6 @@ module('Acceptance | Enterprise | replication', function (hooks) {
addAnalyticsSpy.restore();
});
- test('DR secondary: shows empty state when replication is not enabled', async function (assert) {
- await visit('/vault/replication-dr-promote/details');
-
- assert.dom('[data-test-component="empty-state"]').exists();
- assert
- .dom(GENERAL.emptyStateTitle)
- .includesText('Disaster Recovery secondary not set up', 'shows the correct title of the empty state');
-
- assert
- .dom(GENERAL.emptyStateMessage)
- .hasText(
- 'This cluster has not been enabled as a Disaster Recovery Secondary. You can do so by enabling replication and adding a secondary from the Disaster Recovery Primary.',
- 'renders default message specific to when no replication is enabled'
- );
- });
-
test('Performance primary: add secondary and delete config', async function (assert) {
const secondaryName = `performanceSecondary`;
const mode = 'deny';
diff --git a/ui/tests/acceptance/enterprise-sidebar-nav-test.js b/ui/tests/acceptance/enterprise-sidebar-nav-test.js
index 5d77c24272..e0d0437f0b 100644
--- a/ui/tests/acceptance/enterprise-sidebar-nav-test.js
+++ b/ui/tests/acceptance/enterprise-sidebar-nav-test.js
@@ -5,7 +5,7 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
-import { click, currentURL, visit } from '@ember/test-helpers';
+import { click, currentURL } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
@@ -16,12 +16,12 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
- hooks.beforeEach(function () {
- return login();
+ hooks.beforeEach(async function () {
+ await login();
});
// common links are tested in the sidebar-nav test and will not be covered here
- test('it should render enterprise only navigation links', async function (assert) {
+ test(`it should render enterprise only navigation links`, async function (assert) {
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
await click(link('Secrets Sync'));
@@ -41,12 +41,9 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
'Replication performance route renders'
);
- // for some reason clicking this link would cause the testing browser locally
- // to navigate to 'vault/replication/dr' and halt the test runner
- assert
- .dom(link('Disaster Recovery'))
- .hasAttribute('href', '/ui/vault/replication/dr', 'Replication dr route renders');
- await visit('/vault');
+ await click(link('Disaster Recovery'));
+ assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication DR route renders');
+ await click(link('Back to main navigation'));
await click(link('Client Count'));
assert.dom(panel('Client Count')).exists('Client Count nav panel renders');
diff --git a/ui/tests/integration/components/database-role-setting-form-test.js b/ui/tests/integration/components/database-role-setting-form-test.js
index 9ded81a9d2..fac274e1cd 100644
--- a/ui/tests/integration/components/database-role-setting-form-test.js
+++ b/ui/tests/integration/components/database-role-setting-form-test.js
@@ -9,6 +9,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { setRunOptions } from 'ember-a11y-testing/test-support';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
const testCases = [
{
@@ -96,7 +97,7 @@ module('Integration | Component | database-role-setting-form', function (hooks)
},
});
await render(hbs``);
- assert.dom('[data-test-component="empty-state"]').exists({ count: 2 }, 'Two empty states exist');
+ assert.dom(GENERAL.emptyStateTitle).exists({ count: 2 }, 'Two empty states exist');
});
test('it shows appropriate fields based on roleType and db plugin', async function (assert) {
diff --git a/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js b/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js
index 29c268c756..5c1fd2c3ec 100644
--- a/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js
+++ b/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js
@@ -51,7 +51,10 @@ module('Integration | Component | open-api-explorer | swagger-ui', function (hoo
this.totalApiPaths = Object.keys(this.openApiResponse.paths).length;
- this.renderComponent = () => render(hbs``, { owner: this.engine });
+ this.renderComponent = async () => {
+ await render(hbs``, { owner: this.engine });
+ await waitFor(SELECTORS.searchInput);
+ };
});
test('it renders', async function (assert) {
diff --git a/ui/tests/integration/routes/cluster-route-test.js b/ui/tests/integration/routes/cluster-route-test.js
new file mode 100644
index 0000000000..b5516791e2
--- /dev/null
+++ b/ui/tests/integration/routes/cluster-route-test.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import ClusterRoute from 'vault/routes/vault/cluster';
+import { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY } from 'vault/lib/route-paths';
+import { setupTest } from 'ember-qunit';
+import { module, test } from 'qunit';
+import sinon from 'sinon';
+
+module('Integration | Route | ClusterRoute', function (hooks) {
+ setupTest(hooks);
+
+ function createClusterRoute(
+ context,
+ clusterModel = {},
+ methods = {
+ auth: { currentToken: null },
+ }
+ ) {
+ context.owner.register('route:cluster-route-test', ClusterRoute);
+ const instance = context.owner.lookup('route:cluster-route-test');
+ for (const key of Object.keys(methods)) {
+ if (typeof methods[key] === 'function') {
+ sinon.stub(instance, key).callsFake(methods[key]);
+ }
+ }
+ if (methods.auth) {
+ instance.auth = methods.auth;
+ }
+ instance.modelFor = () => clusterModel;
+ instance.router = { transitionTo: () => {} };
+ return instance;
+ }
+
+ const INIT_TESTS = [
+ {
+ clusterState: { needsInit: true },
+ expected: INIT,
+ description: 'forwards to INIT when cluster needs init',
+ },
+ {
+ clusterState: { needsInit: false, sealed: true },
+ expected: UNSEAL,
+ description: 'forwards to UNSEAL if sealed and initialized',
+ },
+ {
+ clusterState: { needsInit: false, sealed: false },
+ expected: AUTH,
+ description: 'forwards to AUTH if unsealed and initialized',
+ },
+ {
+ clusterState: { dr: { isSecondary: true } },
+ expected: DR_REPLICATION_SECONDARY,
+ description: 'forwards to DR_REPLICATION_SECONDARY if is a dr secondary',
+ },
+ ];
+
+ for (const { clusterState, expected, description } of INIT_TESTS) {
+ test(`#targetRouteName init case: ${expected}`, function (assert) {
+ const subject = createClusterRoute(this, clusterState);
+ subject.routeName = CLUSTER;
+ assert.strictEqual(subject.targetRouteName(), expected, description);
+ });
+ }
+
+ test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) {
+ const subject = createClusterRoute(
+ this,
+ { needsInit: false, sealed: false, dr: { isSecondary: false } },
+ { auth: { currentToken: null } }
+ );
+ subject.router.currentRouteName = INIT;
+ assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT');
+
+ subject.router.currentRouteName = UNSEAL;
+ assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL');
+
+ subject.router.currentRouteName = AUTH;
+ assert.strictEqual(
+ subject.targetRouteName(),
+ AUTH,
+ 'forwards when non-authenticated and navigating to AUTH'
+ );
+
+ subject.router.currentRouteName = DR_REPLICATION_SECONDARY;
+ assert.strictEqual(
+ subject.targetRouteName(),
+ AUTH,
+ 'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
+ );
+ });
+
+ test('#transitionToTargetRoute', function (assert) {
+ const redirectRouteURL = '/vault/secrets-engines/secret/create';
+ const subject = createClusterRoute(this, { needsInit: false, sealed: false });
+ subject.router.currentURL = redirectRouteURL;
+ const spy = sinon.stub(subject.router, 'transitionTo');
+ subject.transitionToTargetRoute();
+ assert.ok(
+ spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }),
+ 'calls transitionTo with the expected args'
+ );
+
+ spy.restore();
+ });
+
+ test('#transitionToTargetRoute with auth as a target', function (assert) {
+ const subject = createClusterRoute(this, { needsInit: false, sealed: false });
+ const spy = sinon.stub(subject.router, 'transitionTo');
+ // in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again
+ subject.transitionToTargetRoute({ targetName: AUTH });
+ assert.ok(spy.notCalled, 'transitionTo is not called');
+ spy.restore();
+ });
+
+ test('#transitionToTargetRoute with auth target, coming from cluster route', function (assert) {
+ const subject = createClusterRoute(this, { needsInit: false, sealed: false });
+ const spy = sinon.stub(subject.router, 'transitionTo');
+ subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX });
+ assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to');
+ spy.restore();
+ });
+});
diff --git a/ui/tests/unit/mixins/cluster-route-test.js b/ui/tests/unit/mixins/cluster-route-test.js
deleted file mode 100644
index 5f609b01dc..0000000000
--- a/ui/tests/unit/mixins/cluster-route-test.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import EmberObject from '@ember/object';
-import ClusterRouteMixin from 'vault/mixins/cluster-route';
-import {
- INIT,
- UNSEAL,
- AUTH,
- CLUSTER,
- CLUSTER_INDEX,
- DR_REPLICATION_SECONDARY,
- REDIRECT,
-} from 'vault/lib/route-paths';
-import { module, test } from 'qunit';
-import sinon from 'sinon';
-
-module('Unit | Mixin | cluster route', function () {
- function createClusterRoute(
- clusterModel = {},
- methods = {
- router: { transitionTo: () => {} },
- hasKeyData: () => false,
- authToken: () => null,
- transitionTo: () => {},
- }
- ) {
- const ClusterRouteObject = EmberObject.extend(
- ClusterRouteMixin,
- Object.assign(methods, { clusterModel: () => clusterModel })
- );
- return ClusterRouteObject.create();
- }
-
- test('#targetRouteName init', function (assert) {
- let subject = createClusterRoute({ needsInit: true });
- subject.routeName = CLUSTER;
- assert.strictEqual(subject.targetRouteName(), INIT, 'forwards to INIT when cluster needs init');
-
- subject = createClusterRoute({ needsInit: false, sealed: true });
- subject.routeName = CLUSTER;
- assert.strictEqual(subject.targetRouteName(), UNSEAL, 'forwards to UNSEAL if sealed and initialized');
-
- subject = createClusterRoute({ needsInit: false });
- subject.routeName = CLUSTER;
- assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards to AUTH if unsealed and initialized');
-
- subject = createClusterRoute({ dr: { isSecondary: true } });
- subject.routeName = CLUSTER;
- assert.strictEqual(
- subject.targetRouteName(),
- DR_REPLICATION_SECONDARY,
- 'forwards to DR_REPLICATION_SECONDARY if is a dr secondary'
- );
- });
-
- test('#targetRouteName when #hasDataKey is true', function (assert) {
- let subject = createClusterRoute(
- { needsInit: false, sealed: true },
- { hasKeyData: () => true, authToken: () => null }
- );
-
- subject.routeName = CLUSTER;
- assert.strictEqual(
- subject.targetRouteName(),
- INIT,
- 'still land on INIT if there are keys on the controller'
- );
-
- subject.routeName = UNSEAL;
- assert.strictEqual(subject.targetRouteName(), UNSEAL, 'allowed to proceed to unseal');
-
- subject = createClusterRoute(
- { needsInit: false, sealed: false },
- { hasKeyData: () => true, authToken: () => null }
- );
-
- subject.routeName = AUTH;
- assert.strictEqual(subject.targetRouteName(), AUTH, 'allowed to proceed to auth');
- });
-
- test('#targetRouteName happy path forwards to CLUSTER route', function (assert) {
- const subject = createClusterRoute(
- { needsInit: false, sealed: false, dr: { isSecondary: false } },
- { hasKeyData: () => false, authToken: () => 'a token' }
- );
- subject.routeName = INIT;
- assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when inited and navigating to INIT');
-
- subject.routeName = UNSEAL;
- assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL');
-
- subject.routeName = AUTH;
- assert.strictEqual(
- subject.targetRouteName(),
- REDIRECT,
- 'forwards when authenticated and navigating to AUTH'
- );
-
- subject.routeName = DR_REPLICATION_SECONDARY;
- assert.strictEqual(
- subject.targetRouteName(),
- CLUSTER,
- 'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
- );
- });
-
- test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) {
- const subject = createClusterRoute(
- { needsInit: false, sealed: false, dr: { isSecondary: false } },
- { hasKeyData: () => false, authToken: () => null }
- );
- subject.routeName = INIT;
- assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT');
-
- subject.routeName = UNSEAL;
- assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL');
-
- subject.routeName = AUTH;
- assert.strictEqual(
- subject.targetRouteName(),
- AUTH,
- 'forwards when non-authenticated and navigating to AUTH'
- );
-
- subject.routeName = DR_REPLICATION_SECONDARY;
- assert.strictEqual(
- subject.targetRouteName(),
- AUTH,
- 'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
- );
- });
-
- test('#transitionToTargetRoute', function (assert) {
- const redirectRouteURL = '/vault/secrets-engines/secret/create';
- const subject = createClusterRoute({ needsInit: false, sealed: false });
- subject.router.currentURL = redirectRouteURL;
- const spy = sinon.spy(subject.router, 'transitionTo');
- subject.transitionToTargetRoute();
- assert.ok(
- spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }),
- 'calls transitionTo with the expected args'
- );
-
- spy.restore();
- });
-
- test('#transitionToTargetRoute with auth as a target', function (assert) {
- const subject = createClusterRoute({ needsInit: false, sealed: false });
- const spy = sinon.spy(subject, 'transitionTo');
- // in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again
- subject.transitionToTargetRoute({ targetName: AUTH });
- assert.ok(spy.notCalled, 'transitionTo is not called');
- spy.restore();
- });
-
- test('#transitionToTargetRoute with auth target, coming from cluster route', function (assert) {
- const subject = createClusterRoute({ needsInit: false, sealed: false });
- const spy = sinon.spy(subject.router, 'transitionTo');
- subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX });
- assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to');
- spy.restore();
- });
-});