From f9ccd941addbaac8f9c47fef63100b556db0dcc5 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:30:26 -0500 Subject: [PATCH] UI/license banners (#11759) --- changelog/11759.txt | 3 + ui/app/components/license-banners.js | 27 ++++++ ui/app/models/cluster.js | 5 ++ ui/app/styles/components/license-banners.scss | 6 ++ ui/app/styles/core.scss | 1 + ui/app/styles/core/helpers.scss | 3 + ui/app/styles/core/message.scss | 4 + .../templates/components/license-banners.hbs | 23 +++++ ui/app/templates/vault/cluster.hbs | 1 + ui/lib/core/addon/components/alert-banner.js | 6 +- ui/mirage/config.js | 20 +++++ .../enterprise-license-banner-test.js | 87 +++++++++++++++++++ .../components/license-banners-test.js | 39 +++++++++ 13 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 changelog/11759.txt create mode 100644 ui/app/components/license-banners.js create mode 100644 ui/app/styles/components/license-banners.scss create mode 100644 ui/app/templates/components/license-banners.hbs create mode 100644 ui/tests/acceptance/enterprise-license-banner-test.js create mode 100644 ui/tests/integration/components/license-banners-test.js diff --git a/changelog/11759.txt b/changelog/11759.txt new file mode 100644 index 000000000..0b0776a65 --- /dev/null +++ b/changelog/11759.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: show site-wide banners for license warnings if applicable +``` diff --git a/ui/app/components/license-banners.js b/ui/app/components/license-banners.js new file mode 100644 index 000000000..91f4d2b15 --- /dev/null +++ b/ui/app/components/license-banners.js @@ -0,0 +1,27 @@ +/** + * @module LicenseBanners + * LicenseBanners components are used to display Vault-specific license expiry messages + * + * @example + * ```js + * + * ``` + * @param {string} expiry - RFC3339 date timestamp + */ + +import Component from '@glimmer/component'; +import isAfter from 'date-fns/isAfter'; +import differenceInDays from 'date-fns/differenceInDays'; + +export default class LicenseBanners extends Component { + get licenseExpired() { + if (!this.args.expiry) return false; + return isAfter(new Date(), new Date(this.args.expiry)); + } + + get licenseExpiringInDays() { + // Anything more than 30 does not render a warning + if (!this.args.expiry) return 99; + return differenceInDays(new Date(this.args.expiry), new Date()); + } +} diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index 39b692a81..c54a94c19 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -12,6 +12,11 @@ export default Model.extend({ status: attr('string'), standby: attr('boolean'), type: attr('string'), + license: attr('object'), + + /* Licensing concerns */ + licenseExpiry: alias('license.expiry'), + licenseState: alias('license.state'), needsInit: computed('nodes', 'nodes.@each.initialized', function() { // needs init if no nodes are initialized diff --git a/ui/app/styles/components/license-banners.scss b/ui/app/styles/components/license-banners.scss new file mode 100644 index 000000000..2c12f1800 --- /dev/null +++ b/ui/app/styles/components/license-banners.scss @@ -0,0 +1,6 @@ +.license-banner-wrapper { + width: 100%; + max-width: 1344px; + margin: $spacing-l auto 0; + padding: 0 1.5rem; +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index f0b4fb898..31a825cb9 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -64,6 +64,7 @@ @import './components/input-hint'; @import './components/kmip-role-edit'; @import './components/known-secondaries-card.scss'; +@import './components/license-banners'; @import './components/linked-block'; @import './components/list-item-row'; @import './components/list-pagination'; diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss index 74715cbf0..2af3cb019 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -174,6 +174,9 @@ .has-top-margin-s { margin-top: $spacing-s; } +.has-top-margin-l { + margin-top: $spacing-l; +} .has-top-margin-xl { margin-top: $spacing-xl; } diff --git a/ui/app/styles/core/message.scss b/ui/app/styles/core/message.scss index 72760fac0..c6348b287 100644 --- a/ui/app/styles/core/message.scss +++ b/ui/app/styles/core/message.scss @@ -132,3 +132,7 @@ .has-text-highlight { color: $yellow-500; } + +.message.message-marginless { + margin: 0; +} diff --git a/ui/app/templates/components/license-banners.hbs b/ui/app/templates/components/license-banners.hbs new file mode 100644 index 000000000..87cf705fe --- /dev/null +++ b/ui/app/templates/components/license-banners.hbs @@ -0,0 +1,23 @@ +{{#if this.licenseExpired}} +
+ + Read documentation + +
+{{else if (lte this.licenseExpiringInDays 30)}} +
+ + Read documentation + +
+{{/if}} diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs index eca0ca96c..620286715 100644 --- a/ui/app/templates/vault/cluster.hbs +++ b/ui/app/templates/vault/cluster.hbs @@ -101,6 +101,7 @@ {{/if}} +
{{#each flashMessages.queue as |flash|}} {{#flash-message data-test-flash-message=true flash=flash as |customComponent flash close|}} diff --git a/ui/lib/core/addon/components/alert-banner.js b/ui/lib/core/addon/components/alert-banner.js index 30e9b3f91..d6970c973 100644 --- a/ui/lib/core/addon/components/alert-banner.js +++ b/ui/lib/core/addon/components/alert-banner.js @@ -28,10 +28,12 @@ export default Component.extend({ secondIconType: null, progressBar: null, yieldWithoutColumn: false, + marginless: false, classNameBindings: ['containerClass'], - containerClass: computed('type', function() { - return 'message ' + messageTypes([this.type]).class; + containerClass: computed('type', 'marginless', function() { + const base = this.marginless ? 'message message-marginless ' : 'message '; + return base + messageTypes([this.type]).class; }), alertType: computed('type', function() { diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 5fb7e4a01..d70eae4ac 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -29,5 +29,25 @@ export default function() { }; }); + this.get('/sys/health', function() { + return { + initialized: true, + sealed: false, + standby: false, + license: { + expiry: '2021-05-12T23:20:50.52Z', + state: 'stored', + }, + performance_standby: false, + replication_performance_mode: 'disabled', + replication_dr_mode: 'disabled', + server_time_utc: 1622562585, + version: '1.9.0+ent', + cluster_name: 'vault-cluster-e779cd7c', + cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b', + last_wal: 121, + }; + }); + this.passthrough(); } diff --git a/ui/tests/acceptance/enterprise-license-banner-test.js b/ui/tests/acceptance/enterprise-license-banner-test.js new file mode 100644 index 000000000..55ce4bbc2 --- /dev/null +++ b/ui/tests/acceptance/enterprise-license-banner-test.js @@ -0,0 +1,87 @@ +import { module, test } from 'qunit'; +import { visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import Pretender from 'pretender'; +import formatRFC3339 from 'date-fns/formatRFC3339'; +import { addDays, subDays } from 'date-fns'; + +const generateHealthResponse = state => { + let expiry; + switch (state) { + case 'expired': + expiry = subDays(new Date(), 2); + break; + case 'expiring': + expiry = addDays(new Date(), 10); + break; + default: + expiry = addDays(new Date(), 33); + break; + } + return { + initialized: true, + sealed: false, + standby: false, + license: { + expiry: formatRFC3339(expiry), + state: 'stored', + }, + performance_standby: false, + replication_performance_mode: 'disabled', + replication_dr_mode: 'disabled', + server_time_utc: 1622562585, + version: '1.9.0+ent', + cluster_name: 'vault-cluster-e779cd7c', + cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b', + last_wal: 121, + }; +}; + +module('Acceptance | Enterprise | License banner warnings', function(hooks) { + setupApplicationTest(hooks); + + test('it shows no license banner if license expires in > 30 days', async function(assert) { + const healthResp = generateHealthResponse(); + this.server = new Pretender(function() { + this.get('/v1/sys/health', response => { + return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; + }); + this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); + // this.get('/v1/sys/health', this.passthrough); + this.get('/v1/sys/seal-status', this.passthrough); + this.get('/v1/sys/license/features', this.passthrough); + }); + await visit('/vault/auth'); + assert.dom('[data-test-license-banner]').doesNotExist('license banner does not show'); + this.server.shutdown(); + }); + test('it shows license banner warning if license expires within 30 days', async function(assert) { + const healthResp = generateHealthResponse('expiring'); + this.server = new Pretender(function() { + this.get('/v1/sys/health', response => { + return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; + }); + this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); + this.get('/v1/sys/seal-status', this.passthrough); + this.get('/v1/sys/license/features', this.passthrough); + }); + await visit('/vault/auth'); + assert.dom('[data-test-license-banner-warning]').exists('license warning shows'); + this.server.shutdown(); + }); + + test('it shows license banner alert if license has already expired', async function(assert) { + const healthResp = generateHealthResponse('expired'); + this.server = new Pretender(function() { + this.get('/v1/sys/health', response => { + return [response, { 'Content-Type': 'application/json' }, JSON.stringify(healthResp)]; + }); + this.get('/v1/sys/internal/ui/feature-flags', this.passthrough); + this.get('/v1/sys/seal-status', this.passthrough); + this.get('/v1/sys/license/features', this.passthrough); + }); + await visit('/vault/auth'); + assert.dom('[data-test-license-banner-expired]').exists('expired license message shows'); + this.server.shutdown(); + }); +}); diff --git a/ui/tests/integration/components/license-banners-test.js b/ui/tests/integration/components/license-banners-test.js new file mode 100644 index 000000000..707c7a1dc --- /dev/null +++ b/ui/tests/integration/components/license-banners-test.js @@ -0,0 +1,39 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import subDays from 'date-fns/subDays'; +import addDays from 'date-fns/addDays'; +import formatRFC3339 from 'date-fns/formatRFC3339'; + +module('Integration | Component | license-banners', function(hooks) { + setupRenderingTest(hooks); + + test('it does not render if no expiry', async function(assert) { + await render(hbs``); + assert.dom('[data-test-license-banner]').doesNotExist('License banner does not render'); + }); + + test('it renders an error if expiry is before now', async function(assert) { + const yesterday = subDays(new Date(), 1); + this.set('expiry', formatRFC3339(yesterday)); + await render(hbs``); + assert.dom('[data-test-license-banner-expired]').exists('Expired license banner renders'); + assert.dom('.message-title').hasText('License expired', 'Shows correct title on alert'); + }); + + test('it renders a warning if expiry is within 30 days', async function(assert) { + const nextMonth = addDays(new Date(), 30); + this.set('expiry', formatRFC3339(nextMonth)); + await render(hbs``); + assert.dom('[data-test-license-banner-warning]').exists('Warning license banner renders'); + assert.dom('.message-title').hasText('Vault license expiring', 'Shows correct title on alert'); + }); + + test('it does not render a banner if expiry is outside 30 days', async function(assert) { + const outside30 = addDays(new Date(), 32); + this.set('expiry', formatRFC3339(outside30)); + await render(hbs``); + assert.dom('[data-test-license-banner]').doesNotExist('License banner does not render'); + }); +});