Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ui/secondary token flow dr #9150

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/lib/core/addon/components/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default Component.extend({
glyph: computed('type', function() {
const modalType = this.get('type');
if (!modalType) {
return {};
return;
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
}
return messageTypes([this.get('type')]);
}),
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions ui/lib/core/app/helpers/date-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'core/helpers/date-format';
1 change: 1 addition & 0 deletions ui/lib/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
],
"dependencies": {
"autosize": "*",
"date-fns": "*",
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
"Duration.js": "*",
"base64-js": "*",
"ember-auto-import": "*",
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/replication/addon/components/replication-summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
try {
yield this.submitHandler.perform(...arguments);
} catch (e) {
// TODO handle error
// do not handle error
}
}),
actions: {
Expand Down
30 changes: 30 additions & 0 deletions ui/lib/replication/addon/controllers/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { copy } from 'ember-copy';
import { resolve } from 'rsvp';
import decodeConfigFromJWT from 'replication/utils/decode-config-from-jwt';

const DEFAULTS = {
token: null,
Expand All @@ -19,9 +20,12 @@ const DEFAULTS = {
};

export default Controller.extend(copy(DEFAULTS, true), {
isModalActive: false,
expirationDate: null,
store: service(),
rm: service('replication-mode'),
replicationMode: alias('rm.mode'),
flashMessages: service(),

submitError(e) {
if (e.errors) {
Expand Down Expand Up @@ -62,6 +66,13 @@ export default Controller.extend(copy(DEFAULTS, true), {
primary_api_addr: null,
primary_cluster_addr: null,
});

// decode token and return epoch expiration, convert to timestamp
const expirationDate = new Date(decodeConfigFromJWT(this.token).exp * 1000);
this.set('expirationDate', expirationDate);

// open modal
this.toggleProperty('isModalActive');
return cluster.reload();
}
this.reset();
Expand Down Expand Up @@ -105,7 +116,26 @@ export default Controller.extend(copy(DEFAULTS, true), {
onSubmit(/*action, mode, data, event*/) {
return this.submitHandler(...arguments);
},
copyClose(successMessage) {
// separate action for copy & close button so it does not try and use execCommand to copy token to clipboard
if (!!successMessage && typeof successMessage === 'string') {
this.get('flashMessages').success(successMessage);
}
this.toggleProperty('isModalActive');
this.transitionToRoute('mode.secondaries');
},
toggleModal(successMessage) {
if (!!successMessage && typeof successMessage === 'string') {
this.get('flashMessages').success(successMessage);
}
// use copy browser extension to copy token if you close the modal by clicking outside of it.
const htmlSelectedToken = document.querySelector('.textarea');
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
htmlSelectedToken.select();
document.execCommand('copy');

this.toggleProperty('isModalActive');
this.transitionToRoute('mode.secondaries');
},
clear() {
this.reset();
this.setProperties({
Expand Down
132 changes: 67 additions & 65 deletions ui/lib/replication/addon/templates/mode/secondaries/add.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,74 @@
<p>Generate a token to enable {{replicationMode}} Replication or change primaries on secondary cluster.</p>
</div>
{{message-error errors=errors}}
{{#if token}}
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
<div class="field">
<label for="activation-token" class="is-label">
Activation token
</label>
<div class="control">
<textarea readonly value={{token}} class="textarea" />
</div>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
{{#copy-button
clipboardText=token
class="button is-primary"
buttonType="button"
success=(action (set-flash-message 'Activation token copied!'))
}}
Copy
{{/copy-button}}
</div>
<div class="control">
<button {{action 'clear'}} type="button" class="button">
Back
</button>
</div>
<div class="field">
<label for="activation-token-id" class="is-label">
Secondary ID
</label>
<div class="control">
{{input class="input" name="activation-token-id" id="activation-token-id" value=id data-test-replication-secondary-id=true}}
</div>
{{else}}
<div class="field">
<label for="activation-token-id" class="is-label">
Secondary ID
</label>
<div class="control">
{{input class="input" name="activation-token-id" id="activation-token-id" value=id data-test-replication-secondary-id=true}}
</div>
<p class="help has-text-grey">
This will be used to identify secondary cluster once a connection has been established with the primary.
</p>
</div>
<div class="field">
{{ttl-picker onChange=(action (mut ttl)) class="is-marginless"}}
<p class="help has-text-grey">
This is the Time To Live for the generated secondary token. After this period, the generated token will no longer be valid.
</p>
<p class="help has-text-grey">
This will be used to identify secondary cluster once a connection has been established with the primary.
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
</p>
</div>
<div class="field">
{{ttl-picker onChange=(action (mut ttl)) class="is-marginless"}}
<p class="help has-text-grey">
This is the Time To Live for the generated secondary token. After this period, the generated token will no longer be valid.
</p>
</div>
{{#if (eq replicationMode "performance")}}
<PathFilterConfigList
@paths={{paths}}
@config={{filterConfig}}
@id={{id}}
/>
{{/if}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
class="button is-primary"
data-test-secondary-add=true
>
Generate token
</button>
</div>
{{#if (eq replicationMode "performance")}}
<PathFilterConfigList
@paths={{paths}}
@config={{filterConfig}}
@id={{id}}
/>
{{/if}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
class="button is-primary"
data-test-secondary-add=true
>
Generate token
</button>
</div>
<div class="control">
{{#link-to "mode.secondaries" replicationMode class="button"}}
Cancel
{{/link-to}}
</div>
<div class="control">
{{#link-to "mode.secondaries" replicationMode class="button"}}
Cancel
{{/link-to}}
</div>
{{/if}}
</div>
</form>

{{#if isModalActive}}
<Modal @title="Copy your token" @onClose={{action "toggleModal" "Token copied!"}} @isActive={{isModalActive}}>
<section class="modal-card-body">
<p>This token can be used to enable DR replication or change primaries on the secondary cluster.</p>
<div class="box is-shadowless is-fullwidth is-sideless">
<h2 class="title is-6">Activation token</h2>
<div class="copy-text level">
<div class="is-fullwidth">
<textarea readonly value={{token}} class="textarea level-left"/>
</div>
</div>
<div class="has-top-margin-xl has-bottom-margin-s">
{{info-table-row
label="TTL"
value=ttl}}
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
{{info-table-row
label="Expires"
value=(date-format expirationDate 'MMM DD, YYYY hh:mm:ss A')}}
</div>
</div>
</section>
<footer class="modal-card-foot">
<CopyButton class="button is-primary copy-close" data-test-button="modal-copy-close" @clipboardText={{token}}
@buttonType="button" @type="copy" @success={{action "copyClose" "Token copied!"}}>
Copy &amp; Close
</CopyButton>
</footer>
</Modal>
{{/if}}
19 changes: 18 additions & 1 deletion ui/tests/acceptance/enterprise-replication-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import { pollCluster } from 'vault/tests/helpers/poll-cluster';
import { dateFormat } from 'vault/tests/helpers/date-format';
import { create } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
import ss from 'vault/tests/pages/components/search-select';
Expand Down Expand Up @@ -80,14 +81,29 @@ module('Acceptance | Enterprise | replication', function(hooks) {
await click('[data-test-replication-link="secondaries"]');
await click('[data-test-secondary-add]');
await fillIn('[data-test-replication-secondary-id]', secondaryName);

await click('#deny');
await clickTrigger();
mountPath = searchSelect.options.objectAt(0).text;
await searchSelect.options.objectAt(0).click();
await click('[data-test-secondary-add]');

await pollCluster(this.owner);
// checks on secondary token modal
assert.dom('#modal-wormhole').exists();

const date = new Date();
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
date.setMinutes(date.getMinutes() + 30); // add default 30 min TTL to current date to return expires
const dateFromHelper = dateFormat([date, 'MMM DD, YYYY hh:mm:ss A']);
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
// because timestamp might be off by a 1 sec due to different in exp from token vs. adding 30 min, only checking month and day.
const fistSixDateFromHelper = dateFromHelper.slice(0, 6);
const dateDisplayed = document.querySelector('[data-test-row-value="Expires"]').innerText;
const fistSixDateDisplay = dateDisplayed.slice(0, 6);

assert.equal(
firstSixDateDisplay,
firstSixDateFromHelper,
'shows the correct expiration date and in the correct format'
);

// click into the added secondary's mount filter config
await click('[data-test-replication-link="secondaries"]');
Expand Down Expand Up @@ -227,6 +243,7 @@ module('Acceptance | Enterprise | replication', function(hooks) {

// enable DR primary replication
const enableButton = document.querySelector('.is-primary');

await click(enableButton);
await click('[data-test-replication-enable="true"]');
await pollCluster(this.owner);
Expand Down
8 changes: 8 additions & 0 deletions ui/tests/helpers/date-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { helper } from '@ember/component/helper';
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
import formatDate from 'date-fns/format';

export function dateFormat([date, format]) {
return formatDate(date, format);
}

export default helper(dateFormat);
38 changes: 24 additions & 14 deletions ui/tests/integration/helpers/date-format-test.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { dateFormat } from '../../../helpers/date-format';
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Helper | date-format', function(hooks) {
setupRenderingTest(hooks);

test('it is able to format a date object', function(assert) {
test('it is able to format a date object', async function(assert) {
let today = new Date();
let result = dateFormat([today, 'YYYY']);
assert.ok(typeof result === 'string');
assert.ok(result !== 'Invalid Date', 'it is not an invalid date');
assert.ok(Number(result) >= 2017);
this.set('today', today);

await render(hbs`<p data-test-date-format>Date: {{date-format today "YYYY"}}</p>`);
assert
.dom('[data-test-date-format]')
.includesText(today.getFullYear(), 'it renders the date in the year format');
});

test('it supports date timestamps', function(assert) {
let today = new Date().getTime();
let result = dateFormat([today, 'YYYY']);
assert.ok(Number(result) >= 2017);
test('it formats the date as specified', async function(assert) {
let today = new Date();
Monkeychip marked this conversation as resolved.
Show resolved Hide resolved
this.set('today', today);

await render(hbs`<p class="date-format">{{date-format today 'hh:mm:ss'}}</p>`);
let formattedDate = document.querySelector('.date-format').innerText;
assert.ok(formattedDate.match(/^\d{2}:\d{2}:\d{2}$/));
});

test('it supports date strings', function(assert) {
let today = new Date().toString();
let result = dateFormat([today, 'YYYY']);
assert.ok(Number(result) >= 2017);
test('it supports date strings', async function(assert) {
let todayString = new Date().getFullYear().toString();
this.set('todayString', todayString);

await render(hbs`<p data-test-date-format>Date: {{date-format todayString}}</p>`);
assert
.dom('[data-test-date-format]')
.includesText(todayString, 'it renders the a date if passed in as a string');
});
});