mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-10 08:37:00 +02:00
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:
parent
ea6d22c2a7
commit
3079b45e6b
3
changelog/16959.txt
Normal file
3
changelog/16959.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
```release-note:feature
|
||||||
|
ui: adds HCP link status banner
|
||||||
|
```
|
29
ui/app/components/link-status.js
Normal file
29
ui/app/components/link-status.js
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { computed } from '@ember/object';
|
|||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
router: service(),
|
router: service(),
|
||||||
|
currentCluster: service(),
|
||||||
'data-test-navheader': true,
|
'data-test-navheader': true,
|
||||||
attributeBindings: ['data-test-navheader'],
|
attributeBindings: ['data-test-navheader'],
|
||||||
classNameBindings: 'consoleFullscreen:panel-fullscreen',
|
classNameBindings: 'consoleFullscreen:panel-fullscreen',
|
||||||
|
@ -43,6 +43,7 @@ export default Model.extend({
|
|||||||
sealProgress: alias('leaderNode.progress'),
|
sealProgress: alias('leaderNode.progress'),
|
||||||
sealType: alias('leaderNode.type'),
|
sealType: alias('leaderNode.type'),
|
||||||
storageType: alias('leaderNode.storageType'),
|
storageType: alias('leaderNode.storageType'),
|
||||||
|
hcpLinkStatus: alias('leaderNode.hcpLinkStatus'),
|
||||||
hasProgress: gte('sealProgress', 1),
|
hasProgress: gte('sealProgress', 1),
|
||||||
usingRaft: equal('storageType', 'raft'),
|
usingRaft: equal('storageType', 'raft'),
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ export default Model.extend({
|
|||||||
version: attr('string'),
|
version: attr('string'),
|
||||||
type: attr('string'),
|
type: attr('string'),
|
||||||
storageType: attr('string'),
|
storageType: attr('string'),
|
||||||
|
hcpLinkStatus: attr('string'),
|
||||||
|
|
||||||
//https://www.vaultproject.io/docs/http/sys-leader.html
|
//https://www.vaultproject.io/docs/http/sys-leader.html
|
||||||
haEnabled: attr('boolean'),
|
haEnabled: attr('boolean'),
|
||||||
|
@ -1,13 +1,45 @@
|
|||||||
.navbar {
|
.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;
|
background-color: $black;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
left: 0;
|
|
||||||
padding: $spacing-xs $spacing-s $spacing-xs 0;
|
padding: $spacing-xs $spacing-s $spacing-xs 0;
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
|
22
ui/app/templates/components/link-status.hbs
Normal file
22
ui/app/templates/components/link-status.hbs
Normal 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 can’t connect to HashiCorp Cloud Portal. Check your config file to ensure that credentials are correct.
|
||||||
|
{{else if (eq @status "500")}}
|
||||||
|
Vault’s 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}}
|
@ -1,38 +1,42 @@
|
|||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-brand" data-test-navheader-home>
|
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
|
||||||
{{yield (hash home=(component "nav-header/home"))}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#unless this.navDrawerOpen}}
|
<div class="navbar-actions">
|
||||||
<button type="button" class="navbar-drawer-toggle is-hidden-tablet" {{action "toggleNavDrawer"}}>
|
<div class="navbar-brand" data-test-navheader-home>
|
||||||
<Icon @name="more-vertical" />
|
{{yield (hash home=(component "nav-header/home"))}}
|
||||||
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>
|
</div>
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
<div
|
{{#unless this.navDrawerOpen}}
|
||||||
class="navbar-drawer-overlay{{if this.navDrawerOpen ' is-active'}}"
|
<button type="button" class="navbar-drawer-toggle is-hidden-tablet" {{action "toggleNavDrawer"}}>
|
||||||
role="button"
|
<Icon @name="more-vertical" />
|
||||||
onclick={{action "toggleNavDrawer" (not this.navDrawerOpen)}}
|
Menu
|
||||||
></div>
|
</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>
|
</nav>
|
||||||
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />
|
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />
|
26
ui/mirage/handlers/hcp-link.js
Normal file
26
ui/mirage/handlers/hcp-link.js
Normal 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' }));
|
||||||
|
}
|
@ -7,5 +7,6 @@ import clients from './clients';
|
|||||||
import db from './db';
|
import db from './db';
|
||||||
import kms from './kms';
|
import kms from './kms';
|
||||||
import mfaConfig from './mfa-config';
|
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 };
|
||||||
|
36
ui/tests/integration/components/link-status-test.js
Normal file
36
ui/tests/integration/components/link-status-test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user