mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
UI/license banners (#11759)
This commit is contained in:
3
changelog/11759.txt
Normal file
3
changelog/11759.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: show site-wide banners for license warnings if applicable
|
||||||
|
```
|
||||||
27
ui/app/components/license-banners.js
Normal file
27
ui/app/components/license-banners.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @module LicenseBanners
|
||||||
|
* LicenseBanners components are used to display Vault-specific license expiry messages
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* <LicenseBanners @expiry={expiryDate} />
|
||||||
|
* ```
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,11 @@ export default Model.extend({
|
|||||||
status: attr('string'),
|
status: attr('string'),
|
||||||
standby: attr('boolean'),
|
standby: attr('boolean'),
|
||||||
type: attr('string'),
|
type: attr('string'),
|
||||||
|
license: attr('object'),
|
||||||
|
|
||||||
|
/* Licensing concerns */
|
||||||
|
licenseExpiry: alias('license.expiry'),
|
||||||
|
licenseState: alias('license.state'),
|
||||||
|
|
||||||
needsInit: computed('nodes', 'nodes.@each.initialized', function() {
|
needsInit: computed('nodes', 'nodes.@each.initialized', function() {
|
||||||
// needs init if no nodes are initialized
|
// needs init if no nodes are initialized
|
||||||
|
|||||||
6
ui/app/styles/components/license-banners.scss
Normal file
6
ui/app/styles/components/license-banners.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.license-banner-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1344px;
|
||||||
|
margin: $spacing-l auto 0;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
@import './components/input-hint';
|
@import './components/input-hint';
|
||||||
@import './components/kmip-role-edit';
|
@import './components/kmip-role-edit';
|
||||||
@import './components/known-secondaries-card.scss';
|
@import './components/known-secondaries-card.scss';
|
||||||
|
@import './components/license-banners';
|
||||||
@import './components/linked-block';
|
@import './components/linked-block';
|
||||||
@import './components/list-item-row';
|
@import './components/list-item-row';
|
||||||
@import './components/list-pagination';
|
@import './components/list-pagination';
|
||||||
|
|||||||
@@ -174,6 +174,9 @@
|
|||||||
.has-top-margin-s {
|
.has-top-margin-s {
|
||||||
margin-top: $spacing-s;
|
margin-top: $spacing-s;
|
||||||
}
|
}
|
||||||
|
.has-top-margin-l {
|
||||||
|
margin-top: $spacing-l;
|
||||||
|
}
|
||||||
.has-top-margin-xl {
|
.has-top-margin-xl {
|
||||||
margin-top: $spacing-xl;
|
margin-top: $spacing-xl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,3 +132,7 @@
|
|||||||
.has-text-highlight {
|
.has-text-highlight {
|
||||||
color: $yellow-500;
|
color: $yellow-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message.message-marginless {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
23
ui/app/templates/components/license-banners.hbs
Normal file
23
ui/app/templates/components/license-banners.hbs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{{#if this.licenseExpired}}
|
||||||
|
<div class="license-banner-wrapper" data-test-license-banner data-test-license-banner-expired>
|
||||||
|
<AlertBanner
|
||||||
|
@type="danger"
|
||||||
|
@title="License expired"
|
||||||
|
@message="Your Vault license expired on {{date-format @expiry "MMM d, yyyy"}}. Add a new license to your configuration and restart Vault."
|
||||||
|
@marginless={{true}}
|
||||||
|
>
|
||||||
|
<a href="https://learn.hashicorp.com/tutorials/nomad/hashicorp-enterprise-license" target="_blank" rel="noreferrer noopener">Read documentation</a>
|
||||||
|
</AlertBanner>
|
||||||
|
</div>
|
||||||
|
{{else if (lte this.licenseExpiringInDays 30)}}
|
||||||
|
<div class="license-banner-wrapper" data-test-license-banner data-test-license-banner-warning>
|
||||||
|
<AlertBanner
|
||||||
|
@type="warning"
|
||||||
|
@title="Vault license expiring"
|
||||||
|
@message="Your Vault license will expire in {{this.licenseExpiringInDays}} days at {{date-format @expiry "hh:mm:ss a"}} on {{date-format @expiry "MMM d, yyyy"}}. Add a new license to your configuration."
|
||||||
|
@marginless={{true}}
|
||||||
|
>
|
||||||
|
<a href="https://learn.hashicorp.com/tutorials/nomad/hashicorp-enterprise-license" target="_blank" rel="noreferrer noopener">Read documentation</a>
|
||||||
|
</AlertBanner>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
@@ -101,6 +101,7 @@
|
|||||||
</Nav.items>
|
</Nav.items>
|
||||||
</NavHeader>
|
</NavHeader>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<LicenseBanners @expiry={{activeCluster.licenseExpiry}} />
|
||||||
<div class="global-flash">
|
<div class="global-flash">
|
||||||
{{#each flashMessages.queue as |flash|}}
|
{{#each flashMessages.queue as |flash|}}
|
||||||
{{#flash-message data-test-flash-message=true flash=flash as |customComponent flash close|}}
|
{{#flash-message data-test-flash-message=true flash=flash as |customComponent flash close|}}
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ export default Component.extend({
|
|||||||
secondIconType: null,
|
secondIconType: null,
|
||||||
progressBar: null,
|
progressBar: null,
|
||||||
yieldWithoutColumn: false,
|
yieldWithoutColumn: false,
|
||||||
|
marginless: false,
|
||||||
classNameBindings: ['containerClass'],
|
classNameBindings: ['containerClass'],
|
||||||
|
|
||||||
containerClass: computed('type', function() {
|
containerClass: computed('type', 'marginless', function() {
|
||||||
return 'message ' + messageTypes([this.type]).class;
|
const base = this.marginless ? 'message message-marginless ' : 'message ';
|
||||||
|
return base + messageTypes([this.type]).class;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
alertType: computed('type', function() {
|
alertType: computed('type', function() {
|
||||||
|
|||||||
@@ -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();
|
this.passthrough();
|
||||||
}
|
}
|
||||||
|
|||||||
87
ui/tests/acceptance/enterprise-license-banner-test.js
Normal file
87
ui/tests/acceptance/enterprise-license-banner-test.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
39
ui/tests/integration/components/license-banners-test.js
Normal file
39
ui/tests/integration/components/license-banners-test.js
Normal file
@@ -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`<LicenseBanners />`);
|
||||||
|
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`<LicenseBanners @expiry={{expiry}} />`);
|
||||||
|
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`<LicenseBanners @expiry={{expiry}} />`);
|
||||||
|
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`<LicenseBanners @expiry={{expiry}} />`);
|
||||||
|
assert.dom('[data-test-license-banner]').doesNotExist('License banner does not render');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user