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}}
+
+{{else if (lte this.licenseExpiringInDays 30)}}
+
+{{/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');
+ });
+});