UI/license page with autoload (#11778)

This commit is contained in:
Chelsea Shaw 2021-06-07 12:44:39 -05:00 committed by GitHub
parent 69d0242db9
commit 468331fa61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 147 additions and 222 deletions

3
changelog/11778.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: update license page with relevant autoload info
```

View File

@ -1,36 +1,7 @@
import ClusterAdapter from './cluster'; import ClusterAdapter from './cluster';
export default ClusterAdapter.extend({ 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() { pathForType() {
return 'license'; return 'license/status';
},
urlForUpdateRecord() {
return this.buildURL() + '/license';
}, },
}); });

View File

@ -1,22 +1,32 @@
import { equal } from '@ember/object/computed'; import Component from '@glimmer/component';
import Component from '@ember/component';
import { allFeatures } from 'vault/helpers/all-features'; import { allFeatures } from 'vault/helpers/all-features';
import { computed } from '@ember/object'; /**
* @module LicenseInfo
export default Component.extend({ *
expirationTime: '', * @example
startTime: '', * ```js
licenseId: '', * <LicenseInfo
features: null, * @startTime="2020-03-12T23:20:50.52Z"
model: null, * @expirationTime="2021-05-12T23:20:50.52Z"
text: '', * @licenseId="some-license-id"
showForm: false, * @features={{array 'Namespaces' 'DR Replication'}}
isTemporary: equal('licenseId', 'temporary'), * @autoloaded={{true}}
featuresInfo: computed('features', 'model.performanceStandbyCount', function() { * @performanceStandbyCount=1
* />
*
* @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<string>} 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 => { return allFeatures().map(feature => {
let active = this.features.includes(feature); let active = this.args.features.includes(feature);
if (active && feature === 'Performance Standby') { if (active && feature === 'Performance Standby') {
let count = this.model.performanceStandbyCount; let count = this.args.performanceStandbyCount;
return { return {
name: feature, name: feature,
active: count ? active : false, active: count ? active : false,
@ -25,14 +35,5 @@ export default Component.extend({
} }
return { name: feature, active }; return { name: feature, active };
}); });
}), }
saveModel() {}, }
actions: {
saveModel(text) {
this.saveModel(text);
},
toggleForm() {
this.toggleProperty('showForm');
},
},
});

View File

@ -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());
},
},
});

View File

@ -24,6 +24,6 @@ export default Model.extend({
features: attr('array'), features: attr('array'),
licenseId: attr('string'), licenseId: attr('string'),
startTime: attr('string'), startTime: attr('string'),
text: attr('string'),
performanceStandbyCount: attr('number'), performanceStandbyCount: attr('number'),
autoloaded: attr('boolean'),
}); });

View File

@ -13,10 +13,4 @@ export default Route.extend(ClusterRoute, {
model() { model() {
return this.store.queryRecord('license', {}); return this.store.queryRecord('license', {});
}, },
actions: {
doRefresh() {
this.refresh();
},
},
}); });

View File

@ -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);
},
});

View File

@ -3,72 +3,29 @@
<h1 class="title is-3">License</h1> <h1 class="title is-3">License</h1>
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
<MessageError @model={{model}} />
{{#if isTemporary}} <section class="box is-sideless is-marginless is-shadowless is-fullwidth">
<section class="box is-sideless is-fullwidth"> <span class="title is-5">Details</span>
<AlertBanner <div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
@type="warning" <InfoTableRow @label="License ID" @value={{@licenseId}} @data-test-detail-row={{true}} />
@message="Your temporary license expires in {{date-from-now expirationTime}} and your vault will seal. Please enter a valid license below." <InfoTableRow @label="Valid from" @value={{@startTime}} @data-test-detail-row={{true}}>
@class="license-warning" {{date-format @startTime 'MMM dd, yyyy hh:mm:ss a'}} to {{date-format @expirationTime 'MMM dd, yyyy hh:mm:ss a'}}
data-test-cluster-status </InfoTableRow>
data-test-warning-text <InfoTableRow @label="License state" @value={{if @autoloaded "Autoloaded" "Stored"}} @data-test-detail-row={{true}}>
/> {{#if @autoloaded}}
<span class="title is-5" data-test-temp-license>Temporary license</span> Autoloaded
<form {{action "saveModel" text on="submit"}}> {{else}}
<div class="box is-shadowless is-fullwidth is-marginless"> Stored
<div class="field"> <Icon @glyph="alert-triangle" class="has-text-highlight" /> <span class="is-size-8">Stored licenses will be deprecated in Vault 1.11. We recommend autoloading your license. Read more <a href="https://learn.hashicorp.com/tutorials/nomad/hashicorp-enterprise-license" rel="noreferrer noopener" target="_blank" >here</a>.</span>
<label for="license-id" class="is-label">License</label> {{/if}}
<div class="control">
<Input @id="license-id" @value={{text}} @autocomplete="off" class="input" data-test-text-input="data-test-text-input" />
</div>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary" data-test-save-button>Save</button>
</div>
</div>
</form>
</section>
{{else}}
<section class="box is-sideless is-fullwidth">
<span class="title is-5">Details</span>
{{#if showForm}}
<form {{action "saveModel" text on="submit"}}>
<div class="field">
<label for="license-id" class="is-label">License</label>
<div class="control">
<Input @id="license-id" @value={{text}} @autocomplete="off" class="input" data-test-text-input="data-test-text-input" />
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-primary" data-test-save-button>Save</button>
</div>
<div class="control">
<button type="button" {{action "toggleForm"}} class="button" data-test-cancel-button>Cancel</button>
</div>
</div>
</form>
{{else}}
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
<InfoTableRow @label="License ID" @value={{licenseId}} />
<InfoTableRow @label="Valid from" @value={{startTime}}>
{{date-format startTime 'MMM dd, yyyy hh:mm:ss a'}} to {{date-format expirationTime 'MMM dd, yyyy hh:mm:ss a'}}
</InfoTableRow> </InfoTableRow>
</div> </div>
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless"> </section>
<div class="control">
<button type="button" {{action "toggleForm"}} class="button" data-test-enter-button>Enter new license</button>
</div>
</div>
{{/if}}
</section>
{{/if}}
<section class="box is-sideless is-marginless is-shadowless is-fullwidth"> <section class="box is-sideless is-marginless is-shadowless is-fullwidth">
<span class="title is-5">Features</span> <span class="title is-5">Features</span>
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless"> <div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
{{#each featuresInfo as |info|}} {{#each this.featuresInfo as |info|}}
<InfoTableRow @label={{info.name}} @value={{if info.active "Active" "Not Active"}} @data-test-feature-row="data-test-feature-row"> <InfoTableRow @label={{info.name}} @value={{if info.active "Active" "Not Active"}} @data-test-feature-row="data-test-feature-row">
{{#if info.active}} {{#if info.active}}
<Icon <Icon

View File

@ -3,7 +3,6 @@
@expirationTime={{model.expirationTime}} @expirationTime={{model.expirationTime}}
@licenseId={{model.licenseId}} @licenseId={{model.licenseId}}
@features={{model.features}} @features={{model.features}}
@text={{model.text}} @autoloaded={{model.autoloaded}}
@saveModel={{action "saveModel"}} @performanceStandbyCount={{model.performanceStandbyCount}}
@model={{model}}
/> />

View File

@ -21,7 +21,7 @@
@bottomBorder={{true}} @bottomBorder={{true}}
@message="Your Vault license has terminated and Vault is sealed. To unseal, add a current license to your configuration and restart Vault." @message="Your Vault license has terminated and Vault is sealed. To unseal, add a current license to your configuration and restart Vault."
> >
<p class="align-right"><a href="https://learn.hashicorp.com/tutorials/nomad/hashicorp-enterprise-license" rel="noreferrer noopener">License documentation</a></p> <p class="align-right"><a href="https://learn.hashicorp.com/tutorials/nomad/hashicorp-enterprise-license" target="_blank" rel="noreferrer noopener">License documentation</a></p>
</EmptyState> </EmptyState>
</div> </div>
</div> </div>

View File

@ -45,6 +45,9 @@ module.exports = function(environment) {
ENV.APP.LOG_TRANSITIONS = true; ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true; // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true; // ENV.APP.LOG_VIEW_LOOKUPS = true;
// ENV['ember-cli-mirage'] = {
// enabled: true,
// };
} }
if (environment === 'test') { if (environment === 'test') {

View File

@ -1,3 +1,5 @@
const EXPIRY_DATE = '2021-05-12T23:20:50.52Z';
export default function() { export default function() {
this.namespace = 'v1'; 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() { this.get('/sys/health', function() {
return { return {
initialized: true, initialized: true,

View File

@ -3,7 +3,6 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { create } from 'ember-cli-page-object'; import { create } from 'ember-cli-page-object';
import license from '../../pages/components/license-info'; import license from '../../pages/components/license-info';
import { allFeatures } from 'vault/helpers/all-features'; import { allFeatures } from 'vault/helpers/all-features';
@ -15,39 +14,6 @@ const component = create(license);
module('Integration | Component | license info', function(hooks) { module('Integration | Component | license info', function(hooks) {
setupRenderingTest(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`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}}/>`
);
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) { test('it renders feature status properly for features associated with license', async function(assert) {
const now = Date.now(); const now = Date.now();
this.set('licenseId', 'temporary'); this.set('licenseId', 'temporary');
@ -57,58 +23,55 @@ module('Integration | Component | license info', function(hooks) {
await render( await render(
hbs`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}}/>` hbs`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}}/>`
); );
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'); assert.equal(component.featureRows.length, FEATURES.length, 'it renders all of the features');
let activeFeatures = component.featureRows.filter(f => f.featureStatus === 'Active'); 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(); const now = Date.now();
this.set('licenseId', 'test'); this.set('licenseId', 'test');
this.set('expirationTime', addMinutes(now, 30)); this.set('expirationTime', addMinutes(now, 30));
this.set('autoloaded', true);
this.set('startTime', now); this.set('startTime', now);
this.set('features', ['HSM', 'Namespaces']); this.set('features', ['HSM', 'Namespaces']);
await render( await render(
hbs`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}}/>` hbs`<LicenseInfo
@licenseId={{this.licenseId}}
@expirationTime={{this.expirationTime}}
@startTime={{this.startTime}}
@features={{this.features}}
@autoloaded={{true}}
/>`
); );
assert.equal(component.hasWarning, false, 'it does not have a warning'); let row = component.detailRows.filterBy('rowName', 'License state')[0];
assert.equal(component.hasSaveButton, false, 'it does not render the save button'); assert.equal(row.rowValue, 'Autoloaded', 'Shows autoloaded status');
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 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(); const now = Date.now();
this.set('licenseId', 'test'); this.set('licenseId', 'test');
this.set('expirationTime', addMinutes(now, 30)); this.set('expirationTime', addMinutes(now, 30));
this.set('autoloaded', false);
this.set('startTime', now); this.set('startTime', now);
this.set('features', ['HSM', 'Namespaces']); this.set('features', ['HSM', 'Namespaces']);
await render( await render(
hbs`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}}/>` hbs`<LicenseInfo
@licenseId={{this.licenseId}}
@expirationTime={{this.expirationTime}}
@startTime={{this.startTime}}
@features={{this.features}}
@autoloaded={{false}}
/>`
); );
await component.enterButton(); let row = component.detailRows.filterBy('rowName', 'License state')[0];
assert.equal(component.hasSaveButton, true, 'it does not render the save button'); assert.ok(
assert.equal(component.hasTextInput, true, 'it does not render the text input for new license'); row.rowValue.includes(
assert.equal(component.hasEnterButton, false, 'it renders the button to toggle license form'); 'Stored licenses will be deprecated in Vault 1.11. We recommend autoloading your license.'
await component.cancelButton(); ),
assert.equal(component.hasSaveButton, false, 'it does not render the save button'); 'Stored license includes recommendation to autoload'
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`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}} @saveModel={{this.saveModel}}/>`
); );
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) { 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']); this.set('features', ['Performance Standby', 'Namespaces']);
await render( await render(
hbs`<LicenseInfo @licenseId={{this.licenseId}} @expirationTime={{this.expirationTime}} @startTime={{this.startTime}} @features={{this.features}} @model={{this.model}}/>` hbs`<LicenseInfo
@licenseId={{this.licenseId}}
@expirationTime={{this.expirationTime}}
@startTime={{this.startTime}}
@features={{this.features}}
@performanceStandbyCount={{this.model.performanceStandbyCount}}
/>`
); );
let row = component.featureRows.filterBy('featureName', 'Performance Standby')[0]; let row = component.featureRows.filterBy('featureName', 'Performance Standby')[0];

View File

@ -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 { export default {
text: fillable('[data-test-text-input]'), detailRows: collection('[data-test-detail-row]', {
isTemp: isPresent('[data-test-temp-license]'), rowName: text('[data-test-row-label]'),
hasTextInput: isPresent('[data-test-text-input]'), rowValue: text('.column.is-flex'),
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]'),
featureRows: collection('[data-test-feature-row]', { featureRows: collection('[data-test-feature-row]', {
featureName: text('[data-test-row-label]'), featureName: text('[data-test-row-label]'),
featureStatus: text('[data-test-feature-status]'), featureStatus: text('[data-test-feature-status]'),