diff --git a/changelog/31478.txt b/changelog/31478.txt
new file mode 100644
index 0000000000..23f8bbd28a
--- /dev/null
+++ b/changelog/31478.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: Fix DR secondary view from not loading/transitioning.
+```
diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js
index 70046b6403..8a39098b1e 100644
--- a/ui/app/routes/vault/cluster.js
+++ b/ui/app/routes/vault/cluster.js
@@ -110,8 +110,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
poll: task(function* () {
while (true) {
- // when testing, the polling loop causes promises to never settle so acceptance tests hang
- // to get around that, we just disable the poll in tests
+ // In test mode, polling causes acceptance tests to hang due to never-settling promises.
+ // To avoid this, polling is disabled during tests.
+ // If your test depends on cluster status changes (e.g., replication mode),
+ // manually trigger polling using pollCluster from 'vault/tests/helpers/poll-cluster'.
if (Ember.testing) {
return;
}
@@ -128,7 +130,8 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
.cancelOn('deactivate')
.keepLatest(),
- async afterModel(model, transition) {
+ // Note: do not make this afterModel hook async, it will break the DR secondary flow.
+ afterModel(model, transition) {
this._super(...arguments);
this.currentCluster.setCluster(model);
@@ -142,7 +145,20 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
if (this.namespaceService.path && !this.version.hasNamespaces) {
return this.router.transitionTo(this.routeName, { queryParams: { namespace: '' } });
}
+ // Skip analytics initialization if the cluster is a DR secondary:
+ // 1. There is little value in collecting analytics in this state.
+ // 2. The analytics service requires resolving async setup (e.g. await),
+ // which delays the afterModel hook resolution and breaks the DR secondary flow.
+ if (model.dr?.isSecondary) {
+ return this.transitionToTargetRoute(transition);
+ }
+ this.addAnalyticsService(model);
+
+ return this.transitionToTargetRoute(transition);
+ },
+
+ async addAnalyticsService(model) {
// identify user for analytics service
if (this.analytics.activated) {
let licenseId = '';
@@ -172,8 +188,6 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
console.log('unable to start analytics', e);
}
}
-
- return this.transitionToTargetRoute(transition);
},
setupController() {
diff --git a/ui/lib/core/addon/components/replication-action-promote.hbs b/ui/lib/core/addon/components/replication-action-promote.hbs
index 4548f25dee..74b94062c0 100644
--- a/ui/lib/core/addon/components/replication-action-promote.hbs
+++ b/ui/lib/core/addon/components/replication-action-promote.hbs
@@ -8,7 +8,7 @@
Promote cluster
-
+
Promote this cluster to a
{{this.model.replicationModeForDisplay}}
primary
diff --git a/ui/lib/core/addon/components/replication-header.hbs b/ui/lib/core/addon/components/replication-header.hbs
index b8790d96fd..9c29084553 100644
--- a/ui/lib/core/addon/components/replication-header.hbs
+++ b/ui/lib/core/addon/components/replication-header.hbs
@@ -37,12 +37,16 @@
{{else}}
-
-
+
Details
-
-
+
Manage
diff --git a/ui/lib/replication/addon/components/known-secondaries-card.hbs b/ui/lib/replication/addon/components/known-secondaries-card.hbs
index 90d2d61697..4f3d546727 100644
--- a/ui/lib/replication/addon/components/known-secondaries-card.hbs
+++ b/ui/lib/replication/addon/components/known-secondaries-card.hbs
@@ -29,7 +29,12 @@
{{/if}}
{{#if this.cluster.canAddSecondary}}
-
+
Add secondary
{{/if}}
diff --git a/ui/lib/replication/addon/templates/mode/secondaries/add.hbs b/ui/lib/replication/addon/templates/mode/secondaries/add.hbs
index a8f516c68b..1d3f482d2e 100644
--- a/ui/lib/replication/addon/templates/mode/secondaries/add.hbs
+++ b/ui/lib/replication/addon/templates/mode/secondaries/add.hbs
@@ -47,7 +47,7 @@
{{/if}}
-
+
diff --git a/ui/tests/acceptance/enterprise-replication-dr-secondaries-test.js b/ui/tests/acceptance/enterprise-replication-dr-secondaries-test.js
new file mode 100644
index 0000000000..be741343a2
--- /dev/null
+++ b/ui/tests/acceptance/enterprise-replication-dr-secondaries-test.js
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { click, visit, settled } from '@ember/test-helpers';
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { login, logout } from 'vault/tests/helpers/auth/auth-helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import sinon from 'sinon';
+import { disableReplication, enableReplication } from 'vault/tests/helpers/replication';
+import { pollCluster } from 'vault/tests/helpers/poll-cluster';
+
+// To allow a user to login and create a secondary dr cluster we demote a primary dr cluster.
+// We stub this demotion so we do not break the dev process for all future tests.
+// All DR secondary assertions are done in one test to avoid the lengthy setup and teardown process.
+module('Acceptance | Enterprise | replication-secondaries', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(async function () {
+ await login();
+ await settled();
+ await disableReplication('dr');
+ await settled();
+ await disableReplication('performance');
+ await settled();
+ });
+
+ hooks.afterEach(async function () {
+ // For the tests following this, return to a good state.
+ // We've reset mirage with this.server.shutdown() but re-poll the cluster to get the latest state.
+ this.server.shutdown();
+ await pollCluster(this.owner);
+ await disableReplication('dr');
+ await settled();
+ await pollCluster(this.owner);
+ await logout();
+ });
+
+ test('DR secondary: manage tab, details tab, and analytics are not run', async function (assert) {
+ // Log in and set up a DR primary
+ await login();
+ await settled();
+ await enableReplication('dr', 'primary');
+ await pollCluster(this.owner);
+ await click('[data-test-replication-link="manage"]');
+
+ // Stub the demote action so it does not actually demote the cluster
+ this.server.post('/sys/replication/dr/demote', () => {
+ return { request_id: 'fake-demote', data: { success: true } };
+ });
+ // Stub endpoints for DR secondary state
+ this.server.post('/sys/capabilities-self', () => ({ capabilities: [] }));
+ this.server.get('/sys/replication/status', () => ({
+ request_id: '2f50313f-be70-493d-5883-c84c2d6f05ce',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ dr: {
+ cluster_id: '7222cbbf-3fb3-949b-8e03-cd5a15babde6',
+ corrupted_merkle_tree: false,
+ known_primary_cluster_addrs: null,
+ last_corruption_check_epoch: '-62135596800',
+ last_reindex_epoch: '0',
+ merkle_root: 'd3ae75bde029e05d435f92b9ecc5641c1b027cc4',
+ mode: 'secondary',
+ primaries: [],
+ primary_cluster_addr: '',
+ secondary_id: '',
+ ssct_generation_counter: 0,
+ state: 'idle',
+ },
+ performance: {
+ mode: 'disabled',
+ },
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ mount_type: '',
+ }));
+ this.server.get('/sys/replication/dr/status', () => ({
+ data: { mode: 'secondary', cluster_id: 'dr-cluster-id' },
+ }));
+ this.server.get('/sys/health', () => ({
+ initialized: true,
+ sealed: false,
+ standby: false,
+ performance_standby: false,
+ replication_performance_mode: 'disabled',
+ replication_dr_mode: 'secondary',
+ server_time_utc: 1754948244,
+ version: '1.21.0-beta1+ent',
+ enterprise: true,
+ cluster_name: 'vault-cluster-64853bcd',
+ cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
+ last_wal: 82,
+ license: {
+ state: 'autoloaded',
+ expiry_time: '2029-01-27T00:00:00Z',
+ terminated: false,
+ },
+ echo_duration_ms: 0,
+ clock_skew_ms: 0,
+ replication_primary_canary_age_ms: 0,
+ removed_from_cluster: false,
+ }));
+ this.server.get('/sys/seal-status', () => ({
+ type: 'shamir',
+ initialized: true,
+ sealed: false,
+ t: 1,
+ n: 1,
+ progress: 0,
+ nonce: '',
+ version: '1.21.0-beta1+ent',
+ build_date: '2025-08-11T14:11:00Z',
+ migration: false,
+ cluster_name: 'vault-cluster-64853bcd',
+ cluster_id: '113a6c47-077f-bea7-0e8e-70a91821e85a',
+ recovery_seal: false,
+ storage_type: 'raft',
+ removed_from_cluster: false,
+ }));
+
+ await click(GENERAL.button('demote'));
+ await pollCluster(this.owner); // We must poll the cluster to stimulate a cluster reload. This is skipped in ember testing so must be forced.
+
+ // Spy on the route's addAnalyticsService method
+ const clusterRoute = this.owner.lookup('route:vault.cluster');
+ const addAnalyticsSpy = sinon.spy(clusterRoute, 'addAnalyticsService');
+
+ // Visit the DR secondary view. This is the route used only by DR secondaries
+ await visit('/vault/replication-dr-promote');
+
+ assert
+ .dom('[data-test-promote-description]')
+ .hasText(
+ 'Promote this cluster to a Disaster Recovery primary',
+ 'shows the correct description for a DR secondary'
+ );
+ assert.dom('[data-test-mode]').includesText('secondary', 'shows the DR secondary mode badge');
+
+ await click(GENERAL.linkTo('Details'));
+ assert
+ .dom('[data-test-replication-secondary-card]')
+ .hasClass(
+ 'has-error-border',
+ 'shows error border on status because the DR secondary is not connected to a primary.'
+ );
+
+ // Assert addAnalyticsService was NOT called
+ assert.false(addAnalyticsSpy.called, 'addAnalyticsService should not be called on DR secondary');
+
+ // Restore spy
+ addAnalyticsSpy.restore();
+ });
+});
diff --git a/ui/tests/acceptance/enterprise-replication-modes-test.js b/ui/tests/acceptance/enterprise-replication-modes-test.js
index d7ba375938..d3ea3a5212 100644
--- a/ui/tests/acceptance/enterprise-replication-modes-test.js
+++ b/ui/tests/acceptance/enterprise-replication-modes-test.js
@@ -9,9 +9,9 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, currentURL, settled, visit, waitFor } from '@ember/test-helpers';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { STATUS_DISABLED_RESPONSE, mockReplicationBlock } from 'vault/tests/helpers/replication';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
const s = {
- navLink: (title) => `[data-test-sidebar-nav-link="${title}"]`,
title: (t) => `[data-test-replication-title="${t}"]`,
detailLink: (mode) => `[data-test-replication-details-link="${mode}"]`,
summaryCard: '[data-test-replication-summary-card]',
@@ -53,8 +53,8 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
await assertTitle(assert, 'Replication unsupported');
// Nav links
- assert.dom(s.navLink('Performance')).doesNotExist('hides performance link');
- assert.dom(s.navLink('Disaster Recovery')).doesNotExist('hides dr link');
+ assert.dom(GENERAL.navLink('Performance')).doesNotExist('hides performance link');
+ assert.dom(GENERAL.navLink('Disaster Recovery')).doesNotExist('hides dr link');
});
test('replication page when disabled', async function (assert) {
@@ -64,15 +64,15 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
await assertTitle(assert, 'Enable Replication');
// Nav links
- assert.dom(s.navLink('Performance')).exists('shows performance link');
- assert.dom(s.navLink('Disaster Recovery')).exists('shows dr link');
+ assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
+ assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
- await click(s.navLink('Performance'));
+ await click(GENERAL.navLink('Performance'));
assert.strictEqual(currentURL(), '/vault/replication/performance', 'it navigates to the correct page');
await settled();
assert.dom(s.enableForm).exists();
- await click(s.navLink('Disaster Recovery'));
+ await click(GENERAL.navLink('Disaster Recovery'));
await assertTitle(assert, 'Enable Disaster Recovery Replication', 'dr');
});
@@ -90,21 +90,21 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
assert.dom(s.detailLink('dr')).hasText('Enable', 'CTA to enable dr');
// Nav links
- assert.dom(s.navLink('Performance')).exists('shows performance link');
- assert.dom(s.navLink('Disaster Recovery')).exists('shows dr link');
+ assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
+ assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
- await click(s.navLink('Performance'));
+ await click(GENERAL.navLink('Performance'));
assert.strictEqual(currentURL(), `/vault/replication/performance`, `goes to correct URL`);
await waitFor(s.dashboard);
assert.dom(s.dashboard).exists(`it shows the replication dashboard`);
- await click(s.navLink('Disaster Recovery'));
+ await click(GENERAL.navLink('Disaster Recovery'));
await assertTitle(assert, 'Enable Disaster Recovery Replication', 'dr');
assert.dom(s.enableForm).exists('it shows the enable view for dr');
});
});
- // DR secondary mode is a whole other thing, test primary only here
- test(`replication page when dr primary only`, async function (assert) {
+
+ test('replication page when dr primary only', async function (assert) {
await this.setupMocks({
dr: mockReplicationBlock('primary'),
performance: mockReplicationBlock(),
@@ -115,20 +115,20 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
assert.dom(s.detailLink('dr')).hasText('Details', 'CTA to see dr details');
// Nav links
- assert.dom(s.navLink('Performance')).exists('shows performance link');
- assert.dom(s.navLink('Disaster Recovery')).exists('shows dr link');
+ assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
+ assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
- await click(s.navLink('Performance'));
+ await click(GENERAL.navLink('Performance'));
assert.strictEqual(currentURL(), `/vault/replication/performance`, `goes to correct URL`);
await waitFor(s.enableForm);
assert.dom(s.enableForm).exists('it shows the enable view for performance');
- await click(s.navLink('Disaster Recovery'));
+ await click(GENERAL.navLink('Disaster Recovery'));
await assertTitle(assert, 'Disaster Recovery primary', 'Disaster Recovery');
assert.dom(s.dashboard).exists(`it shows the replication dashboard`);
});
- test(`replication page both primary`, async function (assert) {
+ test('replication page both primary', async function (assert) {
await this.setupMocks({
dr: mockReplicationBlock('primary'),
performance: mockReplicationBlock('primary'),
@@ -137,11 +137,11 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
await assertTitle(assert, 'Disaster Recovery & Performance primary', 'Disaster Recovery & Performance');
assert.dom(s.summaryCard).exists({ count: 2 }, 'shows 2 summary cards');
- await click(s.navLink('Performance'));
+ await click(GENERAL.navLink('Performance'));
await assertTitle(assert, 'Performance primary', 'Performance');
assert.dom(s.enableForm).doesNotExist();
- await click(s.navLink('Disaster Recovery'));
+ await click(GENERAL.navLink('Disaster Recovery'));
await assertTitle(assert, 'Disaster Recovery primary', 'Disaster Recovery');
assert.dom(s.enableForm).doesNotExist();
});
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
index 178249d8fc..7895cb498a 100644
--- a/ui/tests/acceptance/enterprise-replication-test.js
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -10,7 +10,9 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { pollCluster } from 'vault/tests/helpers/poll-cluster';
import { addSecondary, disableReplication, enableReplication } from 'vault/tests/helpers/replication';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import sinon from 'sinon';
+// note: dr secondary tests are done in ./replication-dr-secondaries-test.js
module('Acceptance | Enterprise | replication', function (hooks) {
setupApplicationTest(hooks);
@@ -30,7 +32,103 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await settled();
});
- test('it shows DR empty state when DR is not configured', async function (assert) {
+ test('DR primary: enables primary and adds secondary', async function (assert) {
+ const secondaryName = 'drSecondary';
+
+ await enableReplication('dr', 'primary');
+ await pollCluster(this.owner);
+ await settled();
+ assert
+ .dom('[data-test-replication-title="Disaster Recovery"]')
+ .includesText('Disaster Recovery', 'it displays the replication type correctly');
+ assert
+ .dom('[data-test-replication-mode-display]')
+ .includesText('primary', 'it displays the cluster mode correctly');
+
+ await addSecondary(secondaryName);
+ // modal for copying the token appears
+ await click(GENERAL.button('Copy token')); // must copy token before escaping the modal.
+ await click(GENERAL.cancelButton);
+ await pollCluster(this.owner);
+ await settled();
+
+ await click('[data-test-replication-link="secondaries"]');
+ assert
+ .dom('[data-test-secondary-name]')
+ .includesText(secondaryName, 'it displays the secondary in the list of known secondaries');
+
+ // verify overflow-wrap class is applied to secondary name
+ assert
+ .dom('[data-test-secondary-name]')
+ .hasClass('overflow-wrap', 'it applies the overflow-wrap class to the secondary name');
+ });
+
+ test('DR primary: shows demotion warning when Performance replication is active', async function (assert) {
+ await visit('vault/replication/performance');
+
+ // enable perf replication
+ await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+ await click(GENERAL.submitButton);
+
+ await pollCluster(this.owner);
+
+ // enable dr replication
+ await visit('/vault/replication/dr');
+
+ await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+
+ await click(GENERAL.submitButton);
+
+ await pollCluster(this.owner);
+ await visit('/vault/replication/dr/manage');
+
+ await click(GENERAL.button('demote'));
+ 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) {
+ // enable dr replication
+
+ await visit('/vault/replication/dr');
+
+ await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+ await click(GENERAL.submitButton);
+ 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.'
+ );
+ });
+
+ test('DR primary: runs analytics service when enabled', async function (assert) {
+ // Spy on the route's addAnalyticsService method if needed
+ const clusterRoute = this.owner.lookup('route:vault.cluster');
+ const addAnalyticsSpy = sinon.spy(clusterRoute, 'addAnalyticsService');
+
+ // Set up DR replication as primary
+ await enableReplication('dr', 'primary');
+ await pollCluster(this.owner);
+ await settled();
+
+ // Visit the route that triggers analytics
+ await visit('/vault/replication/replication/dr'); // or the correct route for your app
+
+ // Verify that analytics service was called
+ assert.true(addAnalyticsSpy.called, 'addAnalyticsService should be called on DR primary');
+
+ // Clean up spy
+ 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();
@@ -46,7 +144,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
);
});
- test('Performance replication: add secondary and delete config', async function (assert) {
+ test('Performance primary: add secondary and delete config', async function (assert) {
const secondaryName = `performanceSecondary`;
const mode = 'deny';
@@ -56,11 +154,12 @@ module('Acceptance | Enterprise | replication', function (hooks) {
// confirm that the details dashboard shows
await waitFor('[data-test-replication-dashboard]', 2000);
await addSecondary(secondaryName, mode);
+ await click(GENERAL.button('Copy token')); // must copy token before escaping the modal.
+ await click(GENERAL.cancelButton);
await pollCluster(this.owner);
await settled();
+
await click('[data-test-replication-link="secondaries"]');
- await click(GENERAL.button('Copy token'));
- await click(GENERAL.cancelButton);
await click(GENERAL.menuTrigger);
await click('[data-test-replication-path-filter-link]');
assert.strictEqual(
@@ -87,80 +186,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
.exists('shows a table row the recently added secondary');
});
- test('DR replication: enable and add secondary', async function (assert) {
- const secondaryName = 'drSecondary';
-
- await enableReplication('dr', 'primary');
- await pollCluster(this.owner);
- await settled();
- assert
- .dom('[data-test-replication-title="Disaster Recovery"]')
- .includesText('Disaster Recovery', 'it displays the replication type correctly');
- assert
- .dom('[data-test-replication-mode-display]')
- .includesText('primary', 'it displays the cluster mode correctly');
-
- await addSecondary(secondaryName);
- await pollCluster(this.owner);
- await settled();
-
- await click('[data-test-replication-link="secondaries"]');
- await click(GENERAL.button('Copy token'));
- await click(GENERAL.cancelButton);
- assert
- .dom('[data-test-secondary-name]')
- .includesText(secondaryName, 'it displays the secondary in the list of known secondaries');
-
- // verify overflow-wrap class is applied to secondary name
- assert
- .dom('[data-test-secondary-name]')
- .hasClass('overflow-wrap', 'it applies the overflow-wrap class to the secondary name');
- });
-
- test('disabling dr primary when perf replication is enabled', async function (assert) {
- await visit('vault/replication/performance');
-
- // enable perf replication
- await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
- await click(GENERAL.submitButton);
-
- await pollCluster(this.owner);
-
- // enable dr replication
- await visit('/vault/replication/dr');
-
- await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
-
- await click(GENERAL.submitButton);
-
- await pollCluster(this.owner);
- await visit('/vault/replication/dr/manage');
-
- await click(GENERAL.button('demote'));
- assert.ok(findAll('[data-test-demote-warning]').length, 'displays the demotion warning');
- });
-
- test('navigating to dr secondary details page when dr secondary is not enabled', async function (assert) {
- // enable dr replication
-
- await visit('/vault/replication/dr');
-
- await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
- await click(GENERAL.submitButton);
- await settled(); // eslint-disable-line
- await pollCluster(this.owner);
- await visit('/vault/replication-dr-promote/details');
-
- assert.dom('[data-test-component="empty-state"]').exists();
- 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 message when replication is enabled'
- );
- });
-
- test('add secondary and navigate through token generation modal', async function (assert) {
+ test('Performance primary: manages secondary token generation and TTL configuration', async function (assert) {
const secondaryNameFirst = 'firstSecondary';
const secondaryNameSecond = 'secondSecondary';
@@ -188,7 +214,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await fillIn('[data-test-input="Secondary ID"]', secondaryNameSecond);
await click(GENERAL.toggleInput('Time to Live (TTL) for generated secondary token'));
await fillIn('[data-test-ttl-value]', 3);
- await click('[data-test-secondary-add]');
+ await click(GENERAL.submitButton);
await pollCluster(this.owner);
await settled();
@@ -207,8 +233,36 @@ module('Acceptance | Enterprise | replication', function (hooks) {
.includesText(secondaryNameFirst, 'it displays the secondary in the list of secondaries');
});
- test('render performance and dr primary and navigate to details page', async function (assert) {
- // enable perf primary replication
+ test('Performance primary: demotes primary to secondary and displays correct status', async function (assert) {
+ // enable perf replication
+ await enableReplication('performance', 'primary');
+ await pollCluster(this.owner);
+ await settled();
+
+ // demote perf primary to a secondary
+ await click('[data-test-replication-link="manage"]');
+
+ // open demote modal
+ await click(GENERAL.button('demote'));
+
+ // enter confirmation text
+ await fillIn('[data-test-confirmation-modal-input="Demote to secondary?"]', 'Performance');
+ // Click confirm button
+ await click(GENERAL.confirmButton);
+
+ await pollCluster(this.owner);
+ await settled();
+
+ await click('[data-test-replication-link="details"]');
+ await waitFor('[data-test-replication-dashboard]');
+ assert.dom('[data-test-replication-dashboard]').exists();
+ assert.dom('[data-test-selectable-card-container="secondary"]').exists();
+ assert
+ .dom('[data-test-replication-mode-display]')
+ .hasText('secondary', 'it displays the cluster mode correctly in header');
+ });
+
+ test('Replication Dashboard: displays summary cards for both Performance and DR primaries', async function (assert) {
await enableReplication('performance', 'primary');
await pollCluster(this.owner);
await settled();
@@ -246,33 +300,4 @@ module('Acceptance | Enterprise | replication', function (hooks) {
.exists('shows the correct card on the details dashboard');
assert.strictEqual(currentURL(), '/vault/replication/dr');
});
-
- test('render performance secondary and navigate to the details page', async function (assert) {
- // enable perf replication
- await enableReplication('performance', 'primary');
- await pollCluster(this.owner);
- await settled();
-
- // demote perf primary to a secondary
- await click('[data-test-replication-link="manage"]');
-
- // open demote modal
- await click(GENERAL.button('demote'));
-
- // enter confirmation text
- await fillIn('[data-test-confirmation-modal-input="Demote to secondary?"]', 'Performance');
- // Click confirm button
- await click(GENERAL.confirmButton);
-
- await pollCluster(this.owner);
- await settled();
-
- await click('[data-test-replication-link="details"]');
- await waitFor('[data-test-replication-dashboard]');
- assert.dom('[data-test-replication-dashboard]').exists();
- assert.dom('[data-test-selectable-card-container="secondary"]').exists();
- assert
- .dom('[data-test-replication-mode-display]')
- .hasText('secondary', 'it displays the cluster mode correctly in header');
- });
});
diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts
index b62e36a9bf..ca5ce5395a 100644
--- a/ui/tests/helpers/general-selectors.ts
+++ b/ui/tests/helpers/general-selectors.ts
@@ -27,7 +27,7 @@ export const GENERAL = {
confirmButton: '[data-test-confirm-button]', // used most often on modal or confirm popups
confirmTrigger: '[data-test-confirm-action-trigger]',
copyButton: '[data-test-copy-button]',
- // there should only be one save button per view (e.g. one per form) so this does not need to be dynamic
+ // there should only be one submit button per view (e.g. one per form) so this does not need to be dynamic
// this button should be used for any kind of "submit" on a form or "save" action.
submitButton: '[data-test-submit]',
button: (label: string) => (label ? `[data-test-button="${label}"]` : '[data-test-button]'),
diff --git a/ui/tests/helpers/replication.js b/ui/tests/helpers/replication.js
index 4409585425..5a25c07775 100644
--- a/ui/tests/helpers/replication.js
+++ b/ui/tests/helpers/replication.js
@@ -38,7 +38,7 @@ export async function addSecondary(secondaryName, mountFilterMode = null) {
await searchSelect.options.objectAt(0).click();
}
- await click('[data-test-secondary-add]');
+ await click(GENERAL.submitButton);
}
export const disableReplication = async (type, assert) => {