HCP Link Status (#16959)

* adds LinkStatus component to NavHeader to display banner with HCP link status

* adds changelog entry

* adds period to connected status message

* updates hcp link status to current cluster polling to automatically update state
This commit is contained in:
Jordan Reimer 2022-09-07 10:21:23 -06:00 committed by GitHub
parent ea6d22c2a7
commit 3079b45e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 193 additions and 37 deletions

3
changelog/16959.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ui: adds HCP link status banner
```

View File

@ -0,0 +1,29 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
/**
* @module LinkStatus
* LinkStatus components are used to indicate link status to the hashicorp cloud platform
*
* @example
* ```js
* <LinkStatus @status={{this.currentCluser.cluster.hcpLinkStatus}} />
* ```
*
* @param {string} status - cluster.hcpLinkStatus value from currentCluster service
*/
export default class LinkStatus extends Component {
@service store;
@service version;
get showBanner() {
// enterprise only feature at this time but will expand to OSS in future release
// there are plans to handle connection failure states -- only alert if connected until further states are returned
return this.version.isEnterprise && this.args.status === 'connected';
}
get bannerClass() {
return this.args.status === 'connected' ? 'connected' : 'warning';
}
}

View File

@ -4,6 +4,7 @@ import { computed } from '@ember/object';
export default Component.extend({
router: service(),
currentCluster: service(),
'data-test-navheader': true,
attributeBindings: ['data-test-navheader'],
classNameBindings: 'consoleFullscreen:panel-fullscreen',

View File

@ -43,6 +43,7 @@ export default Model.extend({
sealProgress: alias('leaderNode.progress'),
sealType: alias('leaderNode.type'),
storageType: alias('leaderNode.storageType'),
hcpLinkStatus: alias('leaderNode.hcpLinkStatus'),
hasProgress: gte('sealProgress', 1),
usingRaft: equal('storageType', 'raft'),

View File

@ -24,6 +24,7 @@ export default Model.extend({
version: attr('string'),
type: attr('string'),
storageType: attr('string'),
hcpLinkStatus: attr('string'),
//https://www.vaultproject.io/docs/http/sys-leader.html
haEnabled: attr('boolean'),

View File

@ -1,13 +1,45 @@
.navbar {
left: 0;
position: fixed;
right: 0;
top: 0;
@include from($mobile) {
display: block;
}
}
.navbar-status {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: $size-7;
font-weight: $font-weight-semibold;
&.connected {
background-color: $ui-gray-800;
color: #c2c5cb;
a {
color: #c2c5cb;
}
}
&.warning {
background-color: #fcf6ea;
color: #975b06;
a {
color: #975b06;
}
}
}
.navbar-actions {
background-color: $black;
display: flex;
height: $header-height;
justify-content: flex-start;
left: 0;
padding: $spacing-xs $spacing-s $spacing-xs 0;
position: fixed;
right: 0;
top: 0;
}
.navbar-brand {

View File

@ -0,0 +1,22 @@
{{#if this.showBanner}}
<div class="navbar-status {{this.bannerClass}}">
<Icon @name="info" />
<p data-test-link-status>
{{#if (eq @status "connected")}}
This self-managed Vault is linked to the
<a href="https://portal.cloud.hashicorp.com/sign-in" target="_blank" rel="noopener noreferrer">
HashiCorp Cloud Platform.
</a>
{{else if (eq @status "401")}}
{{! roughing in 401 and 500 connection statuses -- update strings once they are exposed by the API }}
Vault cant connect to HashiCorp Cloud Portal. Check your config file to ensure that credentials are correct.
{{else if (eq @status "500")}}
Vaults connection to HashiCorp Cloud Portal is down. Check
<a href="https://status.hashicorp.com" target="_blank" rel="noopener noreferrer">
the status page
</a>
for details.
{{/if}}
</p>
</div>
{{/if}}

View File

@ -1,38 +1,42 @@
<nav class="navbar">
<div class="navbar-brand" data-test-navheader-home>
{{yield (hash home=(component "nav-header/home"))}}
</div>
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
{{#unless this.navDrawerOpen}}
<button type="button" class="navbar-drawer-toggle is-hidden-tablet" {{action "toggleNavDrawer"}}>
<Icon @name="more-vertical" />
Menu
</button>
{{/unless}}
{{#unless this.hideLinks}}
<div class="navbar-drawer{{if this.navDrawerOpen ' is-active'}}">
<div class="navbar-drawer-scroll">
<div data-test-navheader-main>
{{yield (hash main=(component "nav-header/main") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
<div class="navbar-end" data-test-navheader-items>
{{yield (hash items=(component "nav-header/items") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
</div>
{{#if this.navDrawerOpen}}
<button class="navbar-drawer-toggle is-hidden-tablet" type="button" {{action "toggleNavDrawer" false}}>
<Icon @name="x" />
</button>
{{/if}}
<div class="navbar-actions">
<div class="navbar-brand" data-test-navheader-home>
{{yield (hash home=(component "nav-header/home"))}}
</div>
{{/unless}}
<div
class="navbar-drawer-overlay{{if this.navDrawerOpen ' is-active'}}"
role="button"
onclick={{action "toggleNavDrawer" (not this.navDrawerOpen)}}
></div>
{{#unless this.navDrawerOpen}}
<button type="button" class="navbar-drawer-toggle is-hidden-tablet" {{action "toggleNavDrawer"}}>
<Icon @name="more-vertical" />
Menu
</button>
{{/unless}}
{{#unless this.hideLinks}}
<div class="navbar-drawer{{if this.navDrawerOpen ' is-active'}}">
<div class="navbar-drawer-scroll">
<div data-test-navheader-main>
{{yield (hash main=(component "nav-header/main") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
<div class="navbar-end" data-test-navheader-items>
{{yield (hash items=(component "nav-header/items") closeDrawer=(action "toggleNavDrawer" false))}}
</div>
</div>
{{#if this.navDrawerOpen}}
<button class="navbar-drawer-toggle is-hidden-tablet" type="button" {{action "toggleNavDrawer" false}}>
<Icon @name="x" />
</button>
{{/if}}
</div>
{{/unless}}
<div
class="navbar-drawer-overlay{{if this.navDrawerOpen ' is-active'}}"
role="button"
onclick={{action "toggleNavDrawer" (not this.navDrawerOpen)}}
></div>
</div>
</nav>
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />

View File

@ -0,0 +1,26 @@
export default function (server) {
const handleResponse = (req, props) => {
const xhr = req.passthrough();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status < 300) {
// XMLHttpRequest response prop only has a getter -- redefine as writable and set value
Object.defineProperty(xhr, 'response', {
writable: true,
value: JSON.stringify({
...JSON.parse(xhr.responseText),
...props,
}),
});
}
};
};
server.get('sys/seal-status', (schema, req) => {
// randomly return one of the various states to test polling
// 401 and 500 are stubs -- update with actual API values once determined
const hcp_link_status = ['connected', 'disconnected', '401', '500'][Math.floor(Math.random() * 2)];
return handleResponse(req, { hcp_link_status });
});
// enterprise only feature initially
server.get('sys/health', (schema, req) => handleResponse(req, { version: '1.12.0-dev1+ent' }));
}

View File

@ -7,5 +7,6 @@ import clients from './clients';
import db from './db';
import kms from './kms';
import mfaConfig from './mfa-config';
import hcpLink from './hcp-link';
export { base, activity, mfaLogin, mfaConfig, clients, db, kms };
export { base, activity, mfaLogin, mfaConfig, clients, db, kms, hcpLink };

View File

@ -0,0 +1,36 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Integration | Component | link-status', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
// this can be removed once feature is released for OSS
hooks.beforeEach(function () {
this.owner.lookup('service:version').set('isEnterprise', true);
});
test('it does not render disconnected status', async function (assert) {
await render(hbs`<LinkStatus @status="disconnected" />`);
assert.dom('.navbar-status').doesNotExist('Banner is hidden for disconnected state');
});
test('it renders connected status', async function (assert) {
await render(hbs`<LinkStatus @status="connected" />`);
assert.dom('.navbar-status').hasClass('connected', 'Correct class renders for connected state');
assert
.dom('[data-test-link-status]')
.hasText(
'This self-managed Vault is linked to the HashiCorp Cloud Platform.',
'Copy renders for connected state'
);
assert
.dom('[data-test-link-status] a')
.hasAttribute('href', 'https://portal.cloud.hashicorp.com/sign-in', 'HCP sign in link renders');
});
});