diff --git a/ui/app/styles/components/action-block.scss b/ui/app/styles/components/action-block.scss
index fa29cf6834f3..137b4fa540c9 100644
--- a/ui/app/styles/components/action-block.scss
+++ b/ui/app/styles/components/action-block.scss
@@ -12,6 +12,7 @@
display: grid;
padding: $spacing-m $spacing-l;
line-height: inherit;
+ grid-gap: $spacing-m;
@include until($mobile) {
@include stacked-grid();
@@ -20,7 +21,7 @@
.action-block-info {
@include until($mobile) {
- @include stacked-grid();
+ @include stacked-content();
}
}
@@ -42,17 +43,25 @@
}
}
+/* Action Block Grid */
.replication-actions-grid-layout {
display: flex;
flex-wrap: wrap;
+ margin: $spacing-m 0;
+ @include until($tablet) {
+ display: block;
+ }
}
.replication-actions-grid-item {
flex-basis: 50%;
- padding: 5px;
+ padding: $spacing-s;
}
.replication-actions-grid-item .action-block {
height: 100%;
width: 100%;
+ @include until($tablet) {
+ height: inherit;
+ }
}
diff --git a/ui/app/styles/components/modal.scss b/ui/app/styles/components/modal.scss
index 01a8c899b868..34911989d518 100644
--- a/ui/app/styles/components/modal.scss
+++ b/ui/app/styles/components/modal.scss
@@ -39,6 +39,11 @@
}
}
+.modal-card-title.title {
+ display: flex;
+ align-items: center;
+}
+
pre {
background-color: inherit;
}
@@ -52,3 +57,20 @@ pre {
color: $yellow-dark;
}
}
+
+.modal-confirm-section .is-help {
+ color: $grey;
+ margin: $spacing-xxs 0;
+ strong {
+ color: inherit;
+ }
+}
+
+.modal-confirm-section {
+ margin: $spacing-xl 0 $spacing-m;
+}
+
+.modal-card-foot-outlined {
+ background: #f7f8fa;
+ border-top: 1px solid #bac1cc;
+}
diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss
index 1e0e528d6d16..1d01a39b964d 100644
--- a/ui/app/styles/core/helpers.scss
+++ b/ui/app/styles/core/helpers.scss
@@ -158,3 +158,8 @@
.has-border-danger {
border: 1px solid $danger;
}
+
+ul.bullet {
+ list-style: disc;
+ padding-left: $spacing-m;
+}
diff --git a/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs b/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs
index dff7e35ea5c5..5efe8b721ff2 100644
--- a/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs
+++ b/ui/app/templates/vault/cluster/replication-dr-promote/details.hbs
@@ -1,44 +1,44 @@
-
+ {{#if Page.isDisabled}}
+
+
+
+
+ {{#link-to "vault.cluster.secrets.backends" }}
+
+ Go back
+ {{/link-to}}
+
+
+
+ Need help?
+
+
+
+
+
+ {{else}}
+
+
- {{#if Page.isDisabled}}
-
-
-
-
- {{#link-to "vault.cluster.secrets.backends" }}
-
- Go back
- {{/link-to}}
-
-
-
- Need help?
-
-
-
-
-
- {{else}}
-
-
-
-
-
+
+
+
{{/if}}
diff --git a/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs b/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs
index 2c4ef45f4a3f..71b515f5ef17 100644
--- a/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs
+++ b/ui/app/templates/vault/cluster/replication-dr-promote/index.hbs
@@ -1,62 +1,40 @@
-
-
-
- Disaster Recovery secondary is enabled
-
-
-
-
-
-
- {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='')}}
- Operation token
- {{/link-to}}
-
-
- {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='update')}}
- Update primary
- {{/link-to}}
-
-
- {{#link-to 'vault.cluster.replication-dr-promote' (query-params action='promote')}}
- Promote
- {{/link-to}}
-
-
-
- {{#if (eq action 'promote')}}
-
+
+
+
-
- {{/if}}
- {{#if (eq action 'update')}}
-
- {{/if}}
- {{#unless action}}
-
-
- Generate an Operation Token by entering a portion of the master key.
- Once all portions are entered, the generated operation token may be used to manage your secondary Disaster Recovery cluster.
-
-
- {{/unless}}
-
-
+ {{#if Page.isDisabled}}
+
+
+
+
+ {{#link-to "vault.cluster.secrets.backends" }}
+
+ Go back
+ {{/link-to}}
+
+
+
+ Need help?
+
+
+
+
+
+ {{else}}
+
+ {{/if}}
+
+
+
diff --git a/ui/lib/core/addon/components/confirmation-modal.js b/ui/lib/core/addon/components/confirmation-modal.js
new file mode 100644
index 000000000000..2dda7d33f844
--- /dev/null
+++ b/ui/lib/core/addon/components/confirmation-modal.js
@@ -0,0 +1,35 @@
+/**
+ * @module ConfirmationModal
+ * ConfirmationModal components are used to provide an alternative to ConfirmationButton that automatically prompts the user to fill in confirmation text before they can continue with a potentially destructive action. It is built off the Modal component
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {function} onConfirm - onConfirm is the action that happens when user clicks onConfirm after filling in the confirmation block
+ * @param {boolean} isActive - Controls whether the modal is "active" eg. visible or not.
+ * @param {string} title - Title of the modal
+ * @param {function} onClose - specify what to do when user attempts to close modal
+ * @param {string} [buttonText=Confirm] - Button text on the confirm button
+ * @param {string} [confirmText=Yes] - The confirmation text that the user must type before continuing
+ * @param {string} [buttonClass=is-danger] - extra class to add to confirm button (eg. "is-danger")
+ * @param {sting} [type=warning] - Applies message-type styling to header. Override to default with empty string
+ * @param {string} [toConfirmMsg] - Finishes the sentence "Type YES to confirm ..."
+ */
+
+import Component from '@ember/component';
+import layout from '../templates/components/confirmation-modal';
+
+export default Component.extend({
+ layout,
+ buttonClass: 'is-danger',
+ buttonText: 'Confirm',
+ confirmText: 'Yes',
+ type: 'warning',
+ toConfirmMsg: '',
+});
diff --git a/ui/lib/core/addon/templates/components/confirmation-modal.hbs b/ui/lib/core/addon/templates/components/confirmation-modal.hbs
new file mode 100644
index 000000000000..471c14b48d28
--- /dev/null
+++ b/ui/lib/core/addon/templates/components/confirmation-modal.hbs
@@ -0,0 +1,46 @@
+
+
+
+
+ {{yield}}
+
+
+
+ Confirm
+
+
Type {{confirmText}} to confirm {{toConfirmMsg}}
+ {{input
+ type="text"
+ value=confirmationInput
+ name="confirmationInput"
+ class="input has-margin-top"
+ autocomplete="off"
+ spellcheck="false"
+ data-test-confirmation-modal-input="confirmationInput"
+ }}
+
+
+
+
+
diff --git a/ui/lib/core/addon/templates/components/modal.hbs b/ui/lib/core/addon/templates/components/modal.hbs
index 5c1f623514e7..f2eb136eaa72 100644
--- a/ui/lib/core/addon/templates/components/modal.hbs
+++ b/ui/lib/core/addon/templates/components/modal.hbs
@@ -13,7 +13,7 @@
data-test-modal-glyph={{glyph.glyph}}
/>
{{/if}}
- {{title}}
+ {{title}}
{{#if showCloseButton}}
diff --git a/ui/lib/core/addon/templates/components/replication-action-disable.hbs b/ui/lib/core/addon/templates/components/replication-action-disable.hbs
index dede146a3141..ad13eae18c93 100644
--- a/ui/lib/core/addon/templates/components/replication-action-disable.hbs
+++ b/ui/lib/core/addon/templates/components/replication-action-disable.hbs
@@ -1,67 +1,52 @@
-
- Disable Replication
-
-
-
- Disable {{replicationDisplayMode}} Replication entirely on the cluster.
- {{#if model.replicationAttrs.isPrimary}}
- Any secondaries will no longer be able to connect.
- {{else if (eq model.replicationAttrs.modeForUrl 'bootstrapping')}}
-
- Since the cluster is currently bootstrapping, we need to know which mode to disable.
- Be sure to choose it below.
-
- Replication cluster mode
-
-
-
-
- {{#each (array 'primary' 'secondary') as |modeOption|}}
-
- {{modeOption}}
-
- {{/each}}
-
-
-
- {{else}}
- The cluster will no longer be able to connect to the primary.
- {{/if}}
-
-
-
- In the secondary case this means a wipe of the
- underlying storage when connected to a primary, and in the primary case,
- secondaries connecting back to the cluster (even if they have connected
- before) will require a wipe of the underlying storage.
-
-
-
-
-
+
+
+ Disable Replication
+
+
+ Disable {{replicationDisplayMode}} Replication entirely on the cluster.
+
+
+
+
+
Disable Replication
-
+
+
+
+
+ {{#if (and model.replicationAttrs.isPrimary (eq replicationDisplayMode "DR"))}}This cannot be undone. {{/if}}
+ Disabling {{replicationDisplayMode}} Replication entirely on this {{if (eq model.replicationAttrs.isPrimary true) "primary" "secondary"}} cluster means that:
+
+
+ {{#if model.replicationAttrs.isPrimary}}
+ Secondaries will no longer be able to connect
+ We will wipe the underlying storage of connected secondaries
+ Secondaries connecting back to the cluster will require a wipe of the underlying storage
+ {{else}}
+ We will wipe the underlying storage of this secondary when connected to a primary
+ {{/if}}
+ Re-enabling this node will change its cluster ID
+
+
diff --git a/ui/lib/core/addon/templates/components/replication-actions.hbs b/ui/lib/core/addon/templates/components/replication-actions.hbs
index 9579ee45904f..3d0bdeeec561 100644
--- a/ui/lib/core/addon/templates/components/replication-actions.hbs
+++ b/ui/lib/core/addon/templates/components/replication-actions.hbs
@@ -2,14 +2,19 @@
{{else}}
- {{#each (if selectedAction (array selectedAction) (replication-action-for-mode replicationMode model.replicationAttrs.modeForUrl)) as |replicationAction index|}}
-
- {{component (concat 'replication-action-' replicationAction)
- onSubmit=(action "onSubmit")
- replicationMode=replicationMode
- model=model
- replicationDisplayMode=replicationDisplayMode
- }}
-
- {{/each}}
+
+ {{#each
+ (replication-action-for-mode replicationMode model.replicationAttrs.modeForUrl)
+ as |replicationAction|
+ }}
+
+ {{component (concat 'replication-action-' replicationAction)
+ onSubmit=(action "onSubmit")
+ replicationMode=replicationMode
+ model=model
+ replicationDisplayMode=replicationDisplayMode
+ }}
+
+ {{/each}}
+
{{/if}}
diff --git a/ui/lib/core/addon/templates/components/replication-header.hbs b/ui/lib/core/addon/templates/components/replication-header.hbs
index 6eeb8351b5e3..d33d9bc37803 100644
--- a/ui/lib/core/addon/templates/components/replication-header.hbs
+++ b/ui/lib/core/addon/templates/components/replication-header.hbs
@@ -47,14 +47,23 @@
+ {{#link-to
+ "vault.cluster.replication-dr-promote.details"
+ tagName="li"
+ activeClass="is-active"
+ }}
+ {{#link-to "vault.cluster.replication-dr-promote.details"}}
+ Details
+ {{/link-to}}
+ {{/link-to}}
{{#link-to
"vault.cluster.replication-dr-promote"
tagName="li"
activeClass="is-active"
- current-when=""
+ current-when="vault.cluster.replication-dr-promote.index"
}}
{{#link-to "vault.cluster.replication-dr-promote"}}
- Details
+ Manage
{{/link-to}}
{{/link-to}}
diff --git a/ui/lib/core/app/components/confirmation-modal.js b/ui/lib/core/app/components/confirmation-modal.js
new file mode 100644
index 000000000000..9cfcc6021b68
--- /dev/null
+++ b/ui/lib/core/app/components/confirmation-modal.js
@@ -0,0 +1 @@
+export { default } from 'core/components/confirmation-modal';
diff --git a/ui/tests/integration/components/confirmation-modal-test.js b/ui/tests/integration/components/confirmation-modal-test.js
new file mode 100644
index 000000000000..7b9b62e60295
--- /dev/null
+++ b/ui/tests/integration/components/confirmation-modal-test.js
@@ -0,0 +1,34 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import sinon from 'sinon';
+import { fillIn, find, render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | confirmation-modal', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders with disabled confirmation button until input matches', async function(assert) {
+ let spy = sinon.spy();
+ this.set('onConfirm', spy);
+
+ await render(hbs`
+
+
+ `);
+
+ assert.dom('[data-test-confirm-button]').isDisabled();
+ assert.equal(
+ find('[data-test-confirm-button]').textContent.trim(),
+ 'Plz Continue',
+ 'Confirm button has specified value'
+ );
+
+ await fillIn('[data-test-confirmation-modal-input="confirmationInput"]', 'Destructive Thing');
+ assert.dom('[data-test-confirm-button]').isNotDisabled();
+ });
+});
diff --git a/ui/tests/integration/components/replication-actions-test.js b/ui/tests/integration/components/replication-actions-test.js
index 309cd51d0517..fbe5eb176476 100644
--- a/ui/tests/integration/components/replication-actions-test.js
+++ b/ui/tests/integration/components/replication-actions-test.js
@@ -39,21 +39,21 @@ module('Integration | Component | replication actions', function(hooks) {
});
let testCases = [
- ['dr', 'primary', 'disable', 'Disable Replication', null, ['disable', 'primary']],
- ['performance', 'primary', 'disable', 'Disable Replication', null, ['disable', 'primary']],
- ['dr', 'secondary', 'disable', 'Disable Replication', null, ['disable', 'secondary']],
- ['performance', 'secondary', 'disable', 'Disable Replication', null, ['disable', 'secondary']],
- ['dr', 'primary', 'recover', 'Recover', null, ['recover']],
- ['performance', 'primary', 'recover', 'Recover', null, ['recover']],
- ['performance', 'secondary', 'recover', 'Recover', null, ['recover']],
+ ['dr', 'primary', 'disable', 'Disable Replication', null, ['disable', 'primary'], false],
+ ['performance', 'primary', 'disable', 'Disable Replication', null, ['disable', 'primary'], false],
+ ['dr', 'secondary', 'disable', 'Disable Replication', null, ['disable', 'secondary'], false],
+ ['performance', 'secondary', 'disable', 'Disable Replication', null, ['disable', 'secondary'], false],
+ ['dr', 'primary', 'recover', 'Recover', null, ['recover'], true],
+ ['performance', 'primary', 'recover', 'Recover', null, ['recover'], true],
+ ['performance', 'secondary', 'recover', 'Recover', null, ['recover'], true],
- ['dr', 'primary', 'reindex', 'Reindex', null, ['reindex']],
- ['performance', 'primary', 'reindex', 'Reindex', null, ['reindex']],
- ['dr', 'secondary', 'reindex', 'Reindex', null, ['reindex']],
- ['performance', 'secondary', 'reindex', 'Reindex', null, ['reindex']],
+ ['dr', 'primary', 'reindex', 'Reindex', null, ['reindex'], true],
+ ['performance', 'primary', 'reindex', 'Reindex', null, ['reindex'], true],
+ ['dr', 'secondary', 'reindex', 'Reindex', null, ['reindex'], true],
+ ['performance', 'secondary', 'reindex', 'Reindex', null, ['reindex'], true],
- ['dr', 'primary', 'demote', 'Demote cluster', null, ['demote', 'primary']],
- ['performance', 'primary', 'demote', 'Demote cluster', null, ['demote', 'primary']],
+ ['dr', 'primary', 'demote', 'Demote cluster', null, ['demote', 'primary'], true],
+ ['performance', 'primary', 'demote', 'Demote cluster', null, ['demote', 'primary'], true],
// we don't do dr secondary promote in this component so just test perf
[
'performance',
@@ -65,6 +65,7 @@ module('Integration | Component | replication actions', function(hooks) {
await blur('[name="primary_cluster_addr"]');
},
['promote', 'secondary', { primary_cluster_addr: 'cluster addr' }],
+ true,
],
// don't yet update-primary for dr
@@ -80,10 +81,19 @@ module('Integration | Component | replication actions', function(hooks) {
await blur('#primary_api_addr');
},
['update-primary', 'secondary', { token: 'token', primary_api_addr: 'addr' }],
+ true,
],
];
- for (let [replicationMode, clusterMode, action, headerText, fillInFn, expectedOnSubmit] of testCases) {
+ for (let [
+ replicationMode,
+ clusterMode,
+ action,
+ headerText,
+ fillInFn,
+ expectedOnSubmit,
+ oldVersion,
+ ] of testCases) {
test(`replication mode ${replicationMode}, cluster mode: ${clusterMode}, action: ${action}`, async function(assert) {
const testKey = `${replicationMode}-${clusterMode}-${action}`;
this.set('model', {
@@ -111,16 +121,32 @@ module('Integration | Component | replication actions', function(hooks) {
});
this.set('storeService.capabilitiesReturnVal', ['root']);
await render(
- hbs`{{replication-actions model=model replicationMode=replicationMode selectedAction=selectedAction onSubmit=(action onSubmit)}}`
+ hbs`
+
+ {{replication-actions model=model replicationMode=replicationMode selectedAction=selectedAction onSubmit=(action onSubmit)}}
+ `
+ );
+ assert.equal(
+ find('h4').textContent.trim(),
+ headerText,
+ `${testKey}: renders the correct component header (${oldVersion})`
);
-
- assert.equal(find('h4').textContent.trim(), headerText, `${testKey}: renders the correct component`);
if (typeof fillInFn === 'function') {
await fillInFn.call(this);
}
- await click('[data-test-confirm-action-trigger]');
- await click('[data-test-confirm-button]');
+ if (oldVersion) {
+ await click('[data-test-confirm-action-trigger]');
+ await click('[data-test-confirm-button]');
+ } else {
+ await click('[data-test-replication-action-trigger]');
+ await fillIn(
+ '[data-test-confirmation-modal-input]',
+ replicationMode === 'dr' ? 'Disaster Recovery' : 'Performance'
+ );
+ await blur('[data-test-confirmation-modal-input]');
+ await click('[data-test-confirm-button]');
+ }
});
}
});