From 468331fa61378e0805dec27819e2af92c14140a8 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Mon, 7 Jun 2021 12:44:39 -0500 Subject: [PATCH] UI/license page with autoload (#11778) --- changelog/11778.txt | 3 + ui/app/adapters/license.js | 31 +----- ui/app/components/license-info.js | 55 +++++----- ui/app/controllers/vault/cluster/license.js | 15 --- ui/app/models/license.js | 2 +- ui/app/routes/vault/cluster/license.js | 6 -- ui/app/serializers/license.js | 20 ++++ ui/app/templates/components/license-info.hbs | 81 ++++---------- ui/app/templates/vault/cluster/license.hbs | 5 +- ui/app/templates/vault/cluster/unseal.hbs | 2 +- ui/config/environment.js | 3 + ui/mirage/config.js | 29 +++++ .../components/license-info-test.js | 101 ++++++------------ ui/tests/pages/components/license-info.js | 16 +-- 14 files changed, 147 insertions(+), 222 deletions(-) create mode 100644 changelog/11778.txt delete mode 100644 ui/app/controllers/vault/cluster/license.js create mode 100644 ui/app/serializers/license.js diff --git a/changelog/11778.txt b/changelog/11778.txt new file mode 100644 index 000000000..34acf3d0a --- /dev/null +++ b/changelog/11778.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: update license page with relevant autoload info +``` diff --git a/ui/app/adapters/license.js b/ui/app/adapters/license.js index c116ab16a..6470f969c 100644 --- a/ui/app/adapters/license.js +++ b/ui/app/adapters/license.js @@ -1,36 +1,7 @@ import ClusterAdapter from './cluster'; export default ClusterAdapter.extend({ - queryRecord() { - return this._super(...arguments).then(resp => { - resp.data.id = resp.data.license_id; - return resp.data; - }); - }, - - createRecord(store, type, snapshot) { - let id = snapshot.attr('licenseId'); - return this._super(...arguments).then(() => { - return { - id, - }; - }); - }, - - updateRecord(store, type, snapshot) { - let id = snapshot.attr('licenseId'); - return this._super(...arguments).then(() => { - return { - id, - }; - }); - }, - pathForType() { - return 'license'; - }, - - urlForUpdateRecord() { - return this.buildURL() + '/license'; + return 'license/status'; }, }); diff --git a/ui/app/components/license-info.js b/ui/app/components/license-info.js index c772a9b3d..45cf74340 100644 --- a/ui/app/components/license-info.js +++ b/ui/app/components/license-info.js @@ -1,22 +1,32 @@ -import { equal } from '@ember/object/computed'; -import Component from '@ember/component'; +import Component from '@glimmer/component'; import { allFeatures } from 'vault/helpers/all-features'; -import { computed } from '@ember/object'; - -export default Component.extend({ - expirationTime: '', - startTime: '', - licenseId: '', - features: null, - model: null, - text: '', - showForm: false, - isTemporary: equal('licenseId', 'temporary'), - featuresInfo: computed('features', 'model.performanceStandbyCount', function() { +/** + * @module LicenseInfo + * + * @example + * ```js + * + * + * @param {string} startTime - RFC3339 formatted timestamp of when the license became active + * @param {string} expirationTime - RFC3339 formatted timestamp of when the license will expire + * @param {string} licenseId - unique ID of the license + * @param {Array} features - Array of feature names active on license + * @param {boolean} autoloaded - Whether the license is autoloaded + * @param {number} performanceStandbyCount - Number of performance standbys active + */ +export default class LicenseInfoComponent extends Component { + get featuresInfo() { return allFeatures().map(feature => { - let active = this.features.includes(feature); + let active = this.args.features.includes(feature); if (active && feature === 'Performance Standby') { - let count = this.model.performanceStandbyCount; + let count = this.args.performanceStandbyCount; return { name: feature, active: count ? active : false, @@ -25,14 +35,5 @@ export default Component.extend({ } return { name: feature, active }; }); - }), - saveModel() {}, - actions: { - saveModel(text) { - this.saveModel(text); - }, - toggleForm() { - this.toggleProperty('showForm'); - }, - }, -}); + } +} diff --git a/ui/app/controllers/vault/cluster/license.js b/ui/app/controllers/vault/cluster/license.js deleted file mode 100644 index baa4190cc..000000000 --- a/ui/app/controllers/vault/cluster/license.js +++ /dev/null @@ -1,15 +0,0 @@ -import Controller from '@ember/controller'; - -export default Controller.extend({ - licenseSuccess() { - this.send('doRefresh'); - }, - licenseError() { - //eat the error (handled in MessageError component) - }, - actions: { - saveModel({ text }) { - this.model.save({ text }).then(() => this.licenseSuccess(), () => this.licenseError()); - }, - }, -}); diff --git a/ui/app/models/license.js b/ui/app/models/license.js index 8b8db3307..5cf21a638 100644 --- a/ui/app/models/license.js +++ b/ui/app/models/license.js @@ -24,6 +24,6 @@ export default Model.extend({ features: attr('array'), licenseId: attr('string'), startTime: attr('string'), - text: attr('string'), performanceStandbyCount: attr('number'), + autoloaded: attr('boolean'), }); diff --git a/ui/app/routes/vault/cluster/license.js b/ui/app/routes/vault/cluster/license.js index d1822ea6e..373f0f85d 100644 --- a/ui/app/routes/vault/cluster/license.js +++ b/ui/app/routes/vault/cluster/license.js @@ -13,10 +13,4 @@ export default Route.extend(ClusterRoute, { model() { return this.store.queryRecord('license', {}); }, - - actions: { - doRefresh() { - this.refresh(); - }, - }, }); diff --git a/ui/app/serializers/license.js b/ui/app/serializers/license.js new file mode 100644 index 000000000..f33e28ec2 --- /dev/null +++ b/ui/app/serializers/license.js @@ -0,0 +1,20 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalizeResponse(store, primaryModelClass, payload, id, requestType) { + let transformedPayload = { autoloaded: payload.autoloading_used, license_id: 'no-license' }; + if (payload.autoloaded) { + transformedPayload = { + ...transformedPayload, + ...payload.autoloaded, + }; + } else if (payload.stored) { + transformedPayload = { + ...transformedPayload, + ...payload.stored, + }; + } + transformedPayload.id = transformedPayload.license_id; + return this._super(store, primaryModelClass, transformedPayload, id, requestType); + }, +}); diff --git a/ui/app/templates/components/license-info.hbs b/ui/app/templates/components/license-info.hbs index d52bacb7f..3baca3ddd 100644 --- a/ui/app/templates/components/license-info.hbs +++ b/ui/app/templates/components/license-info.hbs @@ -3,72 +3,29 @@

License

- -{{#if isTemporary}} -
- - Temporary license -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-{{else}} -
- Details - {{#if showForm}} -
-
- -
- -
-
-
-
- -
-
- -
-
-
- {{else}} -
- - - {{date-format startTime 'MMM dd, yyyy hh:mm:ss a'}} to {{date-format expirationTime 'MMM dd, yyyy hh:mm:ss a'}} + +
+ Details +
+ + + {{date-format @startTime 'MMM dd, yyyy hh:mm:ss a'}} to {{date-format @expirationTime 'MMM dd, yyyy hh:mm:ss a'}} + + + {{#if @autoloaded}} + Autoloaded + {{else}} + Stored + Stored licenses will be deprecated in Vault 1.11. We recommend autoloading your license. Read more here. + {{/if}} -
-
-
- -
-
- {{/if}} -
- {{/if}} +
+
+
Features
- {{#each featuresInfo as |info|}} + {{#each this.featuresInfo as |info|}} {{#if info.active}} diff --git a/ui/app/templates/vault/cluster/unseal.hbs b/ui/app/templates/vault/cluster/unseal.hbs index d135f5544..89b32a233 100644 --- a/ui/app/templates/vault/cluster/unseal.hbs +++ b/ui/app/templates/vault/cluster/unseal.hbs @@ -21,7 +21,7 @@ @bottomBorder={{true}} @message="Your Vault license has terminated and Vault is sealed. To unseal, add a current license to your configuration and restart Vault." > -

License documentation

+

License documentation

diff --git a/ui/config/environment.js b/ui/config/environment.js index d8b4be440..5e1fa3667 100644 --- a/ui/config/environment.js +++ b/ui/config/environment.js @@ -45,6 +45,9 @@ module.exports = function(environment) { ENV.APP.LOG_TRANSITIONS = true; // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; // ENV.APP.LOG_VIEW_LOOKUPS = true; + // ENV['ember-cli-mirage'] = { + // enabled: true, + // }; } if (environment === 'test') { diff --git a/ui/mirage/config.js b/ui/mirage/config.js index d70eae4ac..208b3764b 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -1,3 +1,5 @@ +const EXPIRY_DATE = '2021-05-12T23:20:50.52Z'; + export default function() { this.namespace = 'v1'; @@ -29,6 +31,33 @@ export default function() { }; }); + this.get('/sys/license/status', function() { + return { + autoloading_used: false, + stored: { + expiration_time: EXPIRY_DATE, + features: ['DR Replication', 'Namespaces', 'Lease Count Quotas', 'Automated Snapshots'], + license_id: '0eca7ef8-ebc0-f875-315e-3cc94a7870cf', + performance_standby_count: 0, + start_time: '2020-04-28T00:00:00Z', + }, + persisted_autoload: { + expiration_time: EXPIRY_DATE, + features: ['DR Replication', 'Namespaces', 'Lease Count Quotas', 'Automated Snapshots'], + license_id: '0eca7ef8-ebc0-f875-315e-3cc94a7870cf', + performance_standby_count: 0, + start_time: '2020-04-28T00:00:00Z', + }, + autoloaded: { + expiration_time: EXPIRY_DATE, + features: ['DR Replication', 'Namespaces', 'Lease Count Quotas', 'Automated Snapshots'], + license_id: '0eca7ef8-ebc0-f875-315e-3cc94a7870cf', + performance_standby_count: 0, + start_time: '2020-04-28T00:00:00Z', + }, + }; + }); + this.get('/sys/health', function() { return { initialized: true, diff --git a/ui/tests/integration/components/license-info-test.js b/ui/tests/integration/components/license-info-test.js index 3b6125ea4..407c0578f 100644 --- a/ui/tests/integration/components/license-info-test.js +++ b/ui/tests/integration/components/license-info-test.js @@ -3,7 +3,6 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; import { create } from 'ember-cli-page-object'; import license from '../../pages/components/license-info'; import { allFeatures } from 'vault/helpers/all-features'; @@ -15,39 +14,6 @@ const component = create(license); module('Integration | Component | license info', function(hooks) { setupRenderingTest(hooks); - const LICENSE_WARNING_TEXT = `Warning Your temporary license expires in 30 minutes and your vault will seal. Please enter a valid license below.`; - - test('it renders properly for temporary license', async function(assert) { - const now = Date.now(); - this.set('licenseId', 'temporary'); - this.set('expirationTime', addMinutes(now, 30)); - this.set('startTime', now); - this.set('features', ['HSM', 'Namespaces']); - await render( - hbs`` - ); - assert.equal(component.warning, LICENSE_WARNING_TEXT, 'it renders warning text including time left'); - assert.equal(component.hasSaveButton, true, 'it renders the save button'); - assert.equal(component.hasTextInput, true, 'it renders text input for new license'); - assert.equal(component.featureRows.length, FEATURES.length, 'it renders all of the features'); - assert.equal(component.featureRows.objectAt(0).featureName, 'HSM', 'it renders HSM feature'); - assert.equal( - component.featureRows.objectAt(0).featureStatus, - 'Active', - 'it renders Active for HSM feature' - ); - assert.equal( - component.featureRows.objectAt(1).featureName, - 'Performance Replication', - 'it renders Performance Replication feature name' - ); - assert.equal( - component.featureRows.objectAt(1).featureStatus, - 'Not Active', - 'it renders Not Active for Performance Replication' - ); - }); - test('it renders feature status properly for features associated with license', async function(assert) { const now = Date.now(); this.set('licenseId', 'temporary'); @@ -57,58 +23,55 @@ module('Integration | Component | license info', function(hooks) { await render( hbs`` ); + assert.equal(component.detailRows.length, 3, 'Shows License ID, Valid from, and License State rows'); assert.equal(component.featureRows.length, FEATURES.length, 'it renders all of the features'); let activeFeatures = component.featureRows.filter(f => f.featureStatus === 'Active'); - assert.equal(activeFeatures.length, 2); + assert.equal(activeFeatures.length, 2, 'Has two features listed as active'); }); - test('it renders properly for non-temporary license', async function(assert) { + test('it renders properly for autoloaded license', async function(assert) { const now = Date.now(); this.set('licenseId', 'test'); this.set('expirationTime', addMinutes(now, 30)); + this.set('autoloaded', true); this.set('startTime', now); this.set('features', ['HSM', 'Namespaces']); await render( - hbs`` + hbs`` ); - assert.equal(component.hasWarning, false, 'it does not have a warning'); - assert.equal(component.hasSaveButton, false, 'it does not render the save button'); - assert.equal(component.hasTextInput, false, 'it does not render the text input for new license'); - assert.equal(component.hasEnterButton, true, 'it renders the button to toggle license form'); + let row = component.detailRows.filterBy('rowName', 'License state')[0]; + assert.equal(row.rowValue, 'Autoloaded', 'Shows autoloaded status'); }); - test('it shows and hides license form when enter and cancel buttons are clicked', async function(assert) { + test('it renders properly for stored license', async function(assert) { const now = Date.now(); this.set('licenseId', 'test'); this.set('expirationTime', addMinutes(now, 30)); + this.set('autoloaded', false); this.set('startTime', now); this.set('features', ['HSM', 'Namespaces']); await render( - hbs`` + hbs`` ); - await component.enterButton(); - assert.equal(component.hasSaveButton, true, 'it does not render the save button'); - assert.equal(component.hasTextInput, true, 'it does not render the text input for new license'); - assert.equal(component.hasEnterButton, false, 'it renders the button to toggle license form'); - await component.cancelButton(); - assert.equal(component.hasSaveButton, false, 'it does not render the save button'); - assert.equal(component.hasTextInput, false, 'it does not render the text input for new license'); - assert.equal(component.hasEnterButton, true, 'it renders the button to toggle license form'); - }); - - test('it calls saveModel when save button is clicked', async function(assert) { - const now = Date.now(); - this.set('licenseId', 'temporary'); - this.set('expirationTime', addMinutes(now, 30)); - this.set('startTime', now); - this.set('features', ['HSM', 'Namespaces']); - this.set('saveModel', sinon.spy()); - await render( - hbs`` + let row = component.detailRows.filterBy('rowName', 'License state')[0]; + assert.ok( + row.rowValue.includes( + 'Stored licenses will be deprecated in Vault 1.11. We recommend autoloading your license.' + ), + 'Stored license includes recommendation to autoload' ); - await component.text('ABCDE12345'); - await component.saveButton(); - assert.ok(this.saveModel.calledOnce); }); test('it renders Performance Standby as inactive if count is 0', async function(assert) { @@ -136,7 +99,13 @@ module('Integration | Component | license info', function(hooks) { this.set('features', ['Performance Standby', 'Namespaces']); await render( - hbs`` + hbs`` ); let row = component.featureRows.filterBy('featureName', 'Performance Standby')[0]; diff --git a/ui/tests/pages/components/license-info.js b/ui/tests/pages/components/license-info.js index 813ce1fb6..0cb51c578 100644 --- a/ui/tests/pages/components/license-info.js +++ b/ui/tests/pages/components/license-info.js @@ -1,16 +1,10 @@ -import { clickable, fillable, text, isPresent, collection } from 'ember-cli-page-object'; +import { text, collection } from 'ember-cli-page-object'; export default { - text: fillable('[data-test-text-input]'), - isTemp: isPresent('[data-test-temp-license]'), - hasTextInput: isPresent('[data-test-text-input]'), - saveButton: clickable('[data-test-save-button]'), - hasSaveButton: isPresent('[data-test-save-button]'), - enterButton: clickable('[data-test-enter-button]'), - hasEnterButton: isPresent('[data-test-enter-button]'), - cancelButton: clickable('[data-test-cancel-button]'), - hasWarning: isPresent('[data-test-warning-text]'), - warning: text('[data-test-warning-text]'), + detailRows: collection('[data-test-detail-row]', { + rowName: text('[data-test-row-label]'), + rowValue: text('.column.is-flex'), + }), featureRows: collection('[data-test-feature-row]', { featureName: text('[data-test-row-label]'), featureStatus: text('[data-test-feature-status]'),