UI/license banners (#11759)

This commit is contained in:
Chelsea Shaw 2021-06-03 15:30:26 -05:00 committed by GitHub
parent c6c0424a8e
commit f9ccd941ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 223 additions and 2 deletions

3
changelog/11759.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: show site-wide banners for license warnings if applicable
```

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

View File

@ -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

View File

@ -0,0 +1,6 @@
.license-banner-wrapper {
width: 100%;
max-width: 1344px;
margin: $spacing-l auto 0;
padding: 0 1.5rem;
}

View File

@ -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';

View File

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

View File

@ -132,3 +132,7 @@
.has-text-highlight {
color: $yellow-500;
}
.message.message-marginless {
margin: 0;
}

View 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}}

View File

@ -101,6 +101,7 @@
</Nav.items>
</NavHeader>
{{/if}}
<LicenseBanners @expiry={{activeCluster.licenseExpiry}} />
<div class="global-flash">
{{#each flashMessages.queue as |flash|}}
{{#flash-message data-test-flash-message=true flash=flash as |customComponent flash close|}}

View File

@ -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() {

View File

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

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

View 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');
});
});