From dfbfe374dd1f1961e3d1335fc7311e3bbd91a4e1 Mon Sep 17 00:00:00 2001 From: madalynrose Date: Fri, 12 Oct 2018 15:03:01 -0400 Subject: [PATCH] Licensing in the UI (#5437) Add licensing to the UI --- ui/app/adapters/cluster.js | 15 ++- ui/app/adapters/license.js | 36 ++++++ ui/app/adapters/secret-engine.js | 6 +- ui/app/components/auth-info.js | 5 + ui/app/components/license-info.js | 32 +++++ ui/app/components/pgp-list.js | 4 +- .../vault/cluster/access/leases/list.js | 3 +- ui/app/controllers/vault/cluster/license.js | 15 +++ .../mode/secondaries/config-edit.js | 16 +-- .../vault/cluster/settings/seal.js | 13 ++- ui/app/controllers/vault/cluster/unseal.js | 10 +- ui/app/helpers/all-features.js | 22 ++++ ui/app/models/license.js | 29 +++++ ui/app/router.js | 1 + .../cluster/access/control-group-accessor.js | 8 +- .../vault/cluster/access/control-groups.js | 8 +- .../vault/cluster/access/namespaces/create.js | 8 +- .../vault/cluster/access/namespaces/index.js | 8 +- ui/app/routes/vault/cluster/license.js | 22 ++++ ui/app/routes/vault/cluster/policies.js | 8 +- ui/app/routes/vault/cluster/policy.js | 8 +- ui/app/routes/vault/cluster/replication.js | 8 +- .../vault/cluster/settings/control-groups.js | 8 +- ui/app/services/version.js | 8 +- ui/app/services/wizard.js | 6 +- ui/app/templates/components/auth-info.hbs | 7 ++ ui/app/templates/components/license-info.hbs | 77 +++++++++++++ ui/app/templates/vault/cluster/license.hbs | 9 ++ .../integration/components/license-test.js | 109 ++++++++++++++++++ ui/tests/pages/components/license-info.js | 18 +++ 30 files changed, 478 insertions(+), 49 deletions(-) create mode 100644 ui/app/adapters/license.js create mode 100644 ui/app/components/license-info.js create mode 100644 ui/app/controllers/vault/cluster/license.js create mode 100644 ui/app/helpers/all-features.js create mode 100644 ui/app/models/license.js create mode 100644 ui/app/routes/vault/cluster/license.js create mode 100644 ui/app/templates/components/license-info.hbs create mode 100644 ui/app/templates/vault/cluster/license.hbs create mode 100644 ui/tests/integration/components/license-test.js create mode 100644 ui/tests/pages/components/license-info.js diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js index ea6a8c23cf..c806bdd1c2 100644 --- a/ui/app/adapters/cluster.js +++ b/ui/app/adapters/cluster.js @@ -9,7 +9,17 @@ import DS from 'ember-data'; const { AdapterError } = DS; -const ENDPOINTS = ['health', 'seal-status', 'tokens', 'token', 'seal', 'unseal', 'init', 'capabilities-self']; +const ENDPOINTS = [ + 'health', + 'seal-status', + 'tokens', + 'token', + 'seal', + 'unseal', + 'init', + 'capabilities-self', + 'license', +]; const REPLICATION_ENDPOINTS = { reindex: 'reindex', @@ -28,6 +38,7 @@ export default ApplicationAdapter.extend({ shouldBackgroundReloadRecord() { return true; }, + findRecord(store, type, id, snapshot) { let fetches = { health: this.health(), @@ -70,7 +81,7 @@ export default ApplicationAdapter.extend({ }, features() { - return this.ajax(`${this.buildURL()}/license/features`, 'GET', { + return this.ajax(`${this.urlFor('license')}/features`, 'GET', { unauthenticated: true, }); }, diff --git a/ui/app/adapters/license.js b/ui/app/adapters/license.js new file mode 100644 index 0000000000..c116ab16ad --- /dev/null +++ b/ui/app/adapters/license.js @@ -0,0 +1,36 @@ +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'; + }, +}); diff --git a/ui/app/adapters/secret-engine.js b/ui/app/adapters/secret-engine.js index 2290154741..5648165536 100644 --- a/ui/app/adapters/secret-engine.js +++ b/ui/app/adapters/secret-engine.js @@ -76,7 +76,11 @@ export default ApplicationAdapter.extend({ saveZeroAddressConfig(store, type, snapshot) { const path = snapshot.id; - const roles = store.peekAll('role-ssh').filterBy('zeroAddress').mapBy('id').join(','); + const roles = store + .peekAll('role-ssh') + .filterBy('zeroAddress') + .mapBy('id') + .join(','); const url = `/v1/${path}/config/zeroaddress`; const data = { roles }; if (roles === '') { diff --git a/ui/app/components/auth-info.js b/ui/app/components/auth-info.js index d9127feacf..0f4991d883 100644 --- a/ui/app/components/auth-info.js +++ b/ui/app/components/auth-info.js @@ -2,10 +2,13 @@ import { inject as service } from '@ember/service'; import { or } from '@ember/object/computed'; import Component from '@ember/component'; import { run } from '@ember/runloop'; +import { computed } from '@ember/object'; + export default Component.extend({ auth: service(), wizard: service(), router: service(), + version: service(), transitionToRoute: function() { this.get('router').transitionTo(...arguments); @@ -15,6 +18,8 @@ export default Component.extend({ isRenewing: or('fakeRenew', 'auth.isRenewing'), + isOSS: computed.alias('version.isOSS'), + actions: { restartGuide() { this.get('wizard').restartGuide(); diff --git a/ui/app/components/license-info.js b/ui/app/components/license-info.js new file mode 100644 index 0000000000..c131a631d2 --- /dev/null +++ b/ui/app/components/license-info.js @@ -0,0 +1,32 @@ +import Component from '@ember/component'; +import { allFeatures } from 'vault/helpers/all-features'; +import { computed } from '@ember/object'; + +export default Component.extend({ + expirationTime: '', + startTime: '', + licenseId: '', + features: null, + text: '', + showForm: false, + isTemporary: computed('licenseId', function() { + return this.licenseId === 'temporary'; + }), + featuresInfo: computed('features', function() { + let info = []; + allFeatures().forEach(feature => { + let active = this.features.includes(feature) ? true : false; + info.push({ name: feature, active: active }); + }); + return info; + }), + saveModel() {}, + actions: { + saveModel(text) { + this.saveModel(text); + }, + toggleForm() { + this.toggleProperty('showForm'); + }, + }, +}); diff --git a/ui/app/components/pgp-list.js b/ui/app/components/pgp-list.js index cd14a91c7d..8e668d5d29 100644 --- a/ui/app/components/pgp-list.js +++ b/ui/app/components/pgp-list.js @@ -8,7 +8,9 @@ export default Component.extend({ if (num) { num = parseInt(num, 10); } - return Array(num || 0).fill(null).map(() => ({ value: '' })); + return Array(num || 0) + .fill(null) + .map(() => ({ value: '' })); }), listLength: 0, actions: { diff --git a/ui/app/controllers/vault/cluster/access/leases/list.js b/ui/app/controllers/vault/cluster/access/leases/list.js index 2f01cbbefc..36b6828901 100644 --- a/ui/app/controllers/vault/cluster/access/leases/list.js +++ b/ui/app/controllers/vault/cluster/access/leases/list.js @@ -62,8 +62,7 @@ export default Controller.extend({ const adapter = this.get('store').adapterFor('lease'); const method = isForce ? 'forceRevokePrefix' : 'revokePrefix'; const fn = adapter[method]; - fn - .call(adapter, prefix) + fn.call(adapter, prefix) .then(() => { return this.transitionToRoute('vault.cluster.access.leases.list-root').then(() => { this.get('flashMessages').success(`All of the leases under ${prefix} will be revoked.`); diff --git a/ui/app/controllers/vault/cluster/license.js b/ui/app/controllers/vault/cluster/license.js new file mode 100644 index 0000000000..baa4190ccf --- /dev/null +++ b/ui/app/controllers/vault/cluster/license.js @@ -0,0 +1,15 @@ +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/controllers/vault/cluster/replication/mode/secondaries/config-edit.js b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js index f4a6ee2fba..90c2703f17 100644 --- a/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js +++ b/ui/app/controllers/vault/cluster/replication/mode/secondaries/config-edit.js @@ -35,13 +35,15 @@ export default Controller.extend({ modelMethod .call(config) .then(() => { - this.transitionToRoute(...redirectArgs).followRedirects().then(() => { - flash.success( - `The performance mount filter config for the secondary ${id} was successfully ${isDelete - ? 'deleted' - : 'saved'}.` - ); - }); + this.transitionToRoute(...redirectArgs) + .followRedirects() + .then(() => { + flash.success( + `The performance mount filter config for the secondary ${id} was successfully ${ + isDelete ? 'deleted' : 'saved' + }.` + ); + }); }) .catch(e => { const errString = e.errors.join('.'); diff --git a/ui/app/controllers/vault/cluster/settings/seal.js b/ui/app/controllers/vault/cluster/settings/seal.js index db4139101e..d0afb3673b 100644 --- a/ui/app/controllers/vault/cluster/settings/seal.js +++ b/ui/app/controllers/vault/cluster/settings/seal.js @@ -6,11 +6,14 @@ export default Controller.extend({ actions: { seal() { - return this.model.cluster.store.adapterFor('cluster').seal().then(() => { - this.model.cluster.get('leaderNode').set('sealed', true); - this.get('auth').deleteCurrentToken(); - return this.transitionToRoute('vault.cluster'); - }); + return this.model.cluster.store + .adapterFor('cluster') + .seal() + .then(() => { + this.model.cluster.get('leaderNode').set('sealed', true); + this.get('auth').deleteCurrentToken(); + return this.transitionToRoute('vault.cluster'); + }); }, }, }); diff --git a/ui/app/controllers/vault/cluster/unseal.js b/ui/app/controllers/vault/cluster/unseal.js index 52744af098..72539a6694 100644 --- a/ui/app/controllers/vault/cluster/unseal.js +++ b/ui/app/controllers/vault/cluster/unseal.js @@ -6,10 +6,12 @@ export default Controller.extend({ actions: { transitionToCluster(resp) { - return this.get('model').reload().then(() => { - this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'CONTINUE', resp); - return this.transitionToRoute('vault.cluster', this.get('model.name')); - }); + return this.get('model') + .reload() + .then(() => { + this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'CONTINUE', resp); + return this.transitionToRoute('vault.cluster', this.get('model.name')); + }); }, setUnsealState(resp) { diff --git a/ui/app/helpers/all-features.js b/ui/app/helpers/all-features.js new file mode 100644 index 0000000000..dd01b41de2 --- /dev/null +++ b/ui/app/helpers/all-features.js @@ -0,0 +1,22 @@ +import { helper as buildHelper } from '@ember/component/helper'; + +const ALL_FEATURES = [ + 'HSM', + 'Performance Replication', + 'DR Replication', + 'MFA', + 'Sentinel', + 'AWS KMS Autounseal', + 'GCP CKMS Autounseal', + 'Seal Wrapping', + 'Control Groups', + 'Azure Key Vault Seal', + 'Performance Standby', + 'Namespaces', +]; + +export function allFeatures() { + return ALL_FEATURES; +} + +export default buildHelper(allFeatures); diff --git a/ui/app/models/license.js b/ui/app/models/license.js new file mode 100644 index 0000000000..4026b1e009 --- /dev/null +++ b/ui/app/models/license.js @@ -0,0 +1,29 @@ +import DS from 'ember-data'; +const { attr } = DS; + +/* sample response +{ + "data": { + "expiration_time": "2017-11-14T16:34:36.546753-05:00", + "features": [ + "UI", + "HSM", + "Performance Replication", + "DR Replication" + ], + "license_id": "temporary", + "start_time": "2017-11-14T16:04:36.546753-05:00" + }, + "warnings": [ + "time left on license is 29m33s" + ] +} +*/ + +export default DS.Model.extend({ + expirationTime: attr('string'), + features: attr('array'), + licenseId: attr('string'), + startTime: attr('string'), + text: attr('string'), +}); diff --git a/ui/app/router.js b/ui/app/router.js index 9f1c0e8127..5660a3e2c2 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -12,6 +12,7 @@ Router.map(function() { this.route('auth'); this.route('init'); this.route('logout'); + this.route('license'); this.route('settings', function() { this.route('index', { path: '/' }); this.route('seal'); diff --git a/ui/app/routes/vault/cluster/access/control-group-accessor.js b/ui/app/routes/vault/cluster/access/control-group-accessor.js index 9faf89c33a..b6d6c31428 100644 --- a/ui/app/routes/vault/cluster/access/control-group-accessor.js +++ b/ui/app/routes/vault/cluster/access/control-group-accessor.js @@ -6,9 +6,11 @@ export default Route.extend(UnloadModel, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model(params) { diff --git a/ui/app/routes/vault/cluster/access/control-groups.js b/ui/app/routes/vault/cluster/access/control-groups.js index 07ab02d4c1..caf86865dd 100644 --- a/ui/app/routes/vault/cluster/access/control-groups.js +++ b/ui/app/routes/vault/cluster/access/control-groups.js @@ -6,9 +6,11 @@ export default Route.extend(UnloadModel, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model() { diff --git a/ui/app/routes/vault/cluster/access/namespaces/create.js b/ui/app/routes/vault/cluster/access/namespaces/create.js index 1934e09ba1..039b41fdad 100644 --- a/ui/app/routes/vault/cluster/access/namespaces/create.js +++ b/ui/app/routes/vault/cluster/access/namespaces/create.js @@ -5,9 +5,11 @@ import UnloadModel from 'vault/mixins/unload-model-route'; export default Route.extend(UnloadModel, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model() { return this.get('version.hasNamespaces') ? this.store.createRecord('namespace') : null; diff --git a/ui/app/routes/vault/cluster/access/namespaces/index.js b/ui/app/routes/vault/cluster/access/namespaces/index.js index b61da3106e..fbebdb819b 100644 --- a/ui/app/routes/vault/cluster/access/namespaces/index.js +++ b/ui/app/routes/vault/cluster/access/namespaces/index.js @@ -6,9 +6,11 @@ export default Route.extend(UnloadModel, { version: service(), beforeModel() { this.store.unloadAll('namespace'); - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model() { return this.get('version.hasNamespaces') diff --git a/ui/app/routes/vault/cluster/license.js b/ui/app/routes/vault/cluster/license.js new file mode 100644 index 0000000000..d1822ea6e5 --- /dev/null +++ b/ui/app/routes/vault/cluster/license.js @@ -0,0 +1,22 @@ +import Route from '@ember/routing/route'; +import ClusterRoute from 'vault/mixins/cluster-route'; +import { inject as service } from '@ember/service'; + +export default Route.extend(ClusterRoute, { + version: service(), + beforeModel() { + if (this.version.isOSS) { + this.transitionTo('vault.cluster'); + } + }, + + model() { + return this.store.queryRecord('license', {}); + }, + + actions: { + doRefresh() { + this.refresh(); + }, + }, +}); diff --git a/ui/app/routes/vault/cluster/policies.js b/ui/app/routes/vault/cluster/policies.js index 0018f752ab..9c08f3b608 100644 --- a/ui/app/routes/vault/cluster/policies.js +++ b/ui/app/routes/vault/cluster/policies.js @@ -8,9 +8,11 @@ export default Route.extend(ClusterRoute, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model(params) { diff --git a/ui/app/routes/vault/cluster/policy.js b/ui/app/routes/vault/cluster/policy.js index 7e7aa9e6f6..22c32f3f18 100644 --- a/ui/app/routes/vault/cluster/policy.js +++ b/ui/app/routes/vault/cluster/policy.js @@ -7,9 +7,11 @@ const ALLOWED_TYPES = ['acl', 'egp', 'rgp']; export default Route.extend(ClusterRoute, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model(params) { let policyType = params.type; diff --git a/ui/app/routes/vault/cluster/replication.js b/ui/app/routes/vault/cluster/replication.js index 54d0b563e2..900927c3ad 100644 --- a/ui/app/routes/vault/cluster/replication.js +++ b/ui/app/routes/vault/cluster/replication.js @@ -8,9 +8,11 @@ export default Route.extend(ClusterRoute, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model() { diff --git a/ui/app/routes/vault/cluster/settings/control-groups.js b/ui/app/routes/vault/cluster/settings/control-groups.js index 7e861e28ef..06ef92d35b 100644 --- a/ui/app/routes/vault/cluster/settings/control-groups.js +++ b/ui/app/routes/vault/cluster/settings/control-groups.js @@ -6,9 +6,11 @@ export default Route.extend(UnloadModel, { version: service(), beforeModel() { - return this.get('version').fetchFeatures().then(() => { - return this._super(...arguments); - }); + return this.get('version') + .fetchFeatures() + .then(() => { + return this._super(...arguments); + }); }, model() { diff --git a/ui/app/services/version.js b/ui/app/services/version.js index a846506c7d..8f3301287d 100644 --- a/ui/app/services/version.js +++ b/ui/app/services/version.js @@ -51,7 +51,9 @@ export default Service.extend({ if (this.get('version')) { return; } - let response = yield this.get('store').adapterFor('cluster').health(); + let response = yield this.get('store') + .adapterFor('cluster') + .health(); this.setVersion(response); return; }), @@ -61,7 +63,9 @@ export default Service.extend({ return; } try { - let response = yield this.get('store').adapterFor('cluster').features(); + let response = yield this.get('store') + .adapterFor('cluster') + .features(); this.setFeatures(response); return; } catch (err) { diff --git a/ui/app/services/wizard.js b/ui/app/services/wizard.js index 95e1db7563..4b892f9857 100644 --- a/ui/app/services/wizard.js +++ b/ui/app/services/wizard.js @@ -234,7 +234,11 @@ export default Service.extend(DEFAULTS, { } this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState')); let nextFeature = - this.get('featureList').length > 1 ? this.get('featureList').objectAt(1).capitalize() : 'Finish'; + this.get('featureList').length > 1 + ? this.get('featureList') + .objectAt(1) + .capitalize() + : 'Finish'; this.set('nextFeature', nextFeature); let next; if (this.get('currentMachine') === 'secrets' && this.get('featureState') === 'display') { diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs index 726754e867..829f74c361 100644 --- a/ui/app/templates/components/auth-info.hbs +++ b/ui/app/templates/components/auth-info.hbs @@ -51,6 +51,13 @@ Restart guide + {{#unless isOSS}} +
  • + {{#link-to "vault.cluster.license" activeClusterName id="license"}} + License + {{/link-to}} +
  • + {{/unless}}
  • {{#link-to "vault.cluster.logout" activeClusterName id="logout"}} Sign out diff --git a/ui/app/templates/components/license-info.hbs b/ui/app/templates/components/license-info.hbs new file mode 100644 index 0000000000..d958249da0 --- /dev/null +++ b/ui/app/templates/components/license-info.hbs @@ -0,0 +1,77 @@ + + +

    License

    +
    +
    + +{{#if isTemporary}} +
    + + Your temporary license expires {{moment-from-now expirationTime}} and your vault will seal. Please enter a valid license below. + + Temporary License +
    +
    +
    + +
    + {{input id="license-id" value=text autocomplete="off" class="input" data-test-text-input="data-test-text-input"}} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +{{else}} +
    + Details + {{#if showForm}} +
    +
    + +
    + {{input id="license-id" value=text autocomplete="off" class="input" data-test-text-input="data-test-text-input"}} +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {{else}} +
    + {{info-table-row label="License ID" value=licenseId}} + {{#info-table-row label="Valid from" value=startTime}} + {{moment-format model.startTime 'MMM DD, YYYY hh:mm:ss A'}} to {{moment-format expirationTime 'MMM DD, YYYY hh:mm:ss A'}} + {{/info-table-row}} +
    +
    +
    + +
    +
    + {{/if}} +
    + {{/if}} +
    + Features +
    + {{#each featuresInfo as |info|}} + {{#info-table-row label=info.name value=(if info.active "Active" "Not Active") data-test-feature-row="data-test-feature-row"}} + {{#if info.active}} + Active + {{else}} + Not Active + {{/if}} + {{/info-table-row}} + {{/each}} +
    +
    diff --git a/ui/app/templates/vault/cluster/license.hbs b/ui/app/templates/vault/cluster/license.hbs new file mode 100644 index 0000000000..2da2b08cc4 --- /dev/null +++ b/ui/app/templates/vault/cluster/license.hbs @@ -0,0 +1,9 @@ + diff --git a/ui/tests/integration/components/license-test.js b/ui/tests/integration/components/license-test.js new file mode 100644 index 0000000000..7474c8340a --- /dev/null +++ b/ui/tests/integration/components/license-test.js @@ -0,0 +1,109 @@ +import moment from 'moment'; +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'; + +const component = create(license); + +module('Integration | Component | license info', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function() { + component.setContext(this); + }); + + hooks.afterEach(function() { + component.removeContext(); + }); + + 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) { + this.set('licenseId', 'temporary'); + this.set('expirationTime', moment(moment.now()).add(30, 'minutes')); + this.set('startTime', moment.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, 12, 'it renders 12 features'); + assert.equal(component.featureRows[0].featureName, 'HSM', 'it renders HSM feature'); + assert.equal(component.featureRows[0].featureStatus, 'Active', 'it renders Active for HSM feature'); + assert.equal( + component.featureRows[1].featureName, + 'Performance Replication', + 'it renders Performance Replication feature name' + ); + assert.equal( + component.featureRows[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) { + this.set('licenseId', 'temporary'); + this.set('expirationTime', moment(moment.now()).add(30, 'minutes')); + this.set('startTime', moment.now()); + this.set('features', ['HSM', 'Namespaces']); + await render( + hbs`` + ); + assert.equal(component.featureRows.length, 12, 'it renders 12 features'); + let activeFeatures = component.featureRows.filter(f => f.featureStatus === 'Active'); + assert.equal(activeFeatures.length, 2); + }); + + test('it renders properly for non-temporary license', async function(assert) { + this.set('licenseId', 'test'); + this.set('expirationTime', moment(moment.now()).add(30, 'minutes')); + this.set('startTime', moment.now()); + this.set('features', ['HSM', 'Namespaces']); + await render( + 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'); + }); + + test('it shows and hides license form when enter and cancel buttons are clicked', async function(assert) { + this.set('licenseId', 'test'); + this.set('expirationTime', moment(moment.now()).add(30, 'minutes')); + this.set('startTime', moment.now()); + this.set('features', ['HSM', 'Namespaces']); + await render( + 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) { + this.set('licenseId', 'temporary'); + this.set('expirationTime', moment(moment.now()).add(30, 'minutes')); + this.set('startTime', moment.now()); + this.set('features', ['HSM', 'Namespaces']); + this.set('saveModel', sinon.spy()); + await render( + hbs`` + ); + await component.text('ABCDE12345'); + await component.saveButton(); + assert.ok(this.get('saveModel').calledOnce); + }); +}); diff --git a/ui/tests/pages/components/license-info.js b/ui/tests/pages/components/license-info.js new file mode 100644 index 0000000000..813ce1fb6f --- /dev/null +++ b/ui/tests/pages/components/license-info.js @@ -0,0 +1,18 @@ +import { clickable, fillable, text, isPresent, 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]'), + featureRows: collection('[data-test-feature-row]', { + featureName: text('[data-test-row-label]'), + featureStatus: text('[data-test-feature-status]'), + }), +};